@vox-ai-app/storage 1.0.2 → 1.0.3

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 ADDED
@@ -0,0 +1,109 @@
1
+ # @vox-ai-app/storage
2
+
3
+ Local persistence for Vox: conversations, messages, tasks, settings, tool registry, MCP servers, schedules, secrets, patterns, and vector embeddings. Built on SQLite via `better-sqlite3` with WAL mode and automatic migrations.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @vox-ai-app/storage
9
+ ```
10
+
11
+ ## Exports
12
+
13
+ | Export | Contents |
14
+ | ---------------------------------- | ------------------------------- |
15
+ | `@vox-ai-app/storage` | All exports |
16
+ | `@vox-ai-app/storage/db` | Database lifecycle (open/close) |
17
+ | `@vox-ai-app/storage/messages` | Conversations and messages |
18
+ | `@vox-ai-app/storage/tasks` | Task and task activity storage |
19
+ | `@vox-ai-app/storage/tools` | Custom tool definitions |
20
+ | `@vox-ai-app/storage/settings` | Key-value settings persistence |
21
+ | `@vox-ai-app/storage/mcp-servers` | MCP server configurations |
22
+ | `@vox-ai-app/storage/schedules` | Scheduled job persistence |
23
+ | `@vox-ai-app/storage/tool-secrets` | Encrypted tool secret storage |
24
+ | `@vox-ai-app/storage/patterns` | Conversation pattern storage |
25
+ | `@vox-ai-app/storage/vectors` | Vector embedding storage |
26
+
27
+ ## Database
28
+
29
+ ```js
30
+ import { openDb, closeDb } from '@vox-ai-app/storage/db'
31
+
32
+ const db = openDb('/path/to/storage.db')
33
+ closeDb('/path/to/storage.db')
34
+ ```
35
+
36
+ The database uses WAL journal mode and foreign keys. Schema is managed via migrations in `src/migrations/`.
37
+
38
+ ## Messages
39
+
40
+ ```js
41
+ import {
42
+ ensureConversation,
43
+ appendMessage,
44
+ getMessages,
45
+ getMessagesBeforeId,
46
+ clearMessages,
47
+ saveSummaryCheckpoint,
48
+ loadSummaryCheckpoint
49
+ } from '@vox-ai-app/storage/messages'
50
+
51
+ ensureConversation(db, 'main')
52
+ appendMessage(db, 'user', 'Hello', 'main')
53
+ appendMessage(db, 'assistant', 'Hi there!', 'main')
54
+
55
+ const messages = getMessages(db, 'main', 50)
56
+ const older = getMessagesBeforeId(db, messages[0].id, 'main', 20)
57
+
58
+ saveSummaryCheckpoint(db, 'summary text', 42, 'main')
59
+ const { summary, checkpointId } = loadSummaryCheckpoint(db, 'main')
60
+
61
+ clearMessages(db, 'main')
62
+ ```
63
+
64
+ ## Tasks
65
+
66
+ ```js
67
+ import {
68
+ upsertTask,
69
+ getTask,
70
+ loadTasks,
71
+ appendTaskActivity,
72
+ loadTaskActivity
73
+ } from '@vox-ai-app/storage/tasks'
74
+
75
+ upsertTask(db, {
76
+ taskId: 'abc-123',
77
+ instructions: 'Summarize the document',
78
+ status: 'running'
79
+ })
80
+
81
+ const task = getTask(db, 'abc-123')
82
+ const allTasks = loadTasks(db)
83
+
84
+ appendTaskActivity(db, {
85
+ id: 'act-1',
86
+ taskId: 'abc-123',
87
+ type: 'tool_call',
88
+ name: 'read_local_file',
89
+ timestamp: new Date().toISOString(),
90
+ data: { path: '~/doc.md' }
91
+ })
92
+
93
+ const activity = loadTaskActivity(db, 'abc-123')
94
+ ```
95
+
96
+ ## Settings
97
+
98
+ ```js
99
+ import { getSetting, setSetting, getAllSettings, deleteSetting } from '@vox-ai-app/storage/settings'
100
+
101
+ setSetting(db, 'theme', 'dark')
102
+ const theme = getSetting(db, 'theme')
103
+ const all = getAllSettings(db)
104
+ deleteSetting(db, 'theme')
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vox-ai-app/storage",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Local message and config persistence for Vox",
@@ -9,9 +9,15 @@
9
9
  "exports": {
10
10
  ".": "./src/index.js",
11
11
  "./db": "./src/db.js",
12
- "./messages": "./src/messages.js",
13
- "./config": "./src/config.js",
14
- "./tasks": "./src/tasks.js"
12
+ "./messages": "./src/repos/messages.js",
13
+ "./tasks": "./src/repos/tasks.js",
14
+ "./tools": "./src/repos/tools.js",
15
+ "./settings": "./src/repos/settings.js",
16
+ "./mcp-servers": "./src/repos/mcp-servers.js",
17
+ "./schedules": "./src/repos/schedules.js",
18
+ "./tool-secrets": "./src/repos/tool-secrets.js",
19
+ "./patterns": "./src/repos/patterns.js",
20
+ "./vectors": "./src/repos/vectors.js"
15
21
  },
16
22
  "publishConfig": {
17
23
  "access": "public",
package/src/db.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import Database from 'better-sqlite3'
2
2
  import { mkdirSync } from 'node:fs'
3
3
  import path from 'node:path'
4
+ import { runMigrations } from './migrations/runner.js'
5
+ import * as initialSchema from './migrations/001_initial_schema.js'
6
+ import * as taskActivityTypes from './migrations/002_task_activity_types.js'
4
7
 
5
8
  const dbs = new Map()
6
9
 
10
+ const migrations = [initialSchema, taskActivityTypes]
11
+
7
12
  function resolveDbPath(dbPath) {
8
13
  const normalized = String(dbPath || '').trim()
9
14
  if (!normalized) {
@@ -11,116 +16,10 @@ function resolveDbPath(dbPath) {
11
16
  }
12
17
  return path.resolve(normalized)
13
18
  }
14
-
15
19
  function prepareDb(db) {
16
20
  db.pragma('journal_mode = WAL')
17
21
  db.pragma('foreign_keys = ON')
18
-
19
- db.exec(`
20
- CREATE TABLE IF NOT EXISTS conversations (
21
- id TEXT PRIMARY KEY,
22
- created_at TEXT NOT NULL,
23
- updated_at TEXT NOT NULL
24
- );
25
-
26
- CREATE TABLE IF NOT EXISTS messages (
27
- id INTEGER PRIMARY KEY AUTOINCREMENT,
28
- conversation_id TEXT NOT NULL,
29
- role TEXT NOT NULL,
30
- content TEXT NOT NULL,
31
- created_at TEXT NOT NULL,
32
- FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
33
- );
34
-
35
- CREATE INDEX IF NOT EXISTS idx_messages_conversation_id_id
36
- ON messages (conversation_id, id);
37
-
38
- CREATE TABLE IF NOT EXISTS tasks (
39
- task_id TEXT PRIMARY KEY,
40
- instructions TEXT NOT NULL DEFAULT '',
41
- context TEXT NOT NULL DEFAULT '',
42
- status TEXT NOT NULL DEFAULT 'queued',
43
- created_at TEXT NOT NULL,
44
- updated_at TEXT NOT NULL,
45
- current_plan TEXT NOT NULL DEFAULT '',
46
- message TEXT NOT NULL DEFAULT '',
47
- result TEXT,
48
- completed_at TEXT NOT NULL DEFAULT '',
49
- failed_at TEXT NOT NULL DEFAULT ''
50
- );
51
-
52
- CREATE INDEX IF NOT EXISTS idx_tasks_created_at
53
- ON tasks (created_at DESC, task_id DESC);
54
-
55
- CREATE TABLE IF NOT EXISTS task_activity (
56
- id TEXT PRIMARY KEY,
57
- task_id TEXT NOT NULL,
58
- type TEXT NOT NULL,
59
- name TEXT,
60
- raw_result TEXT,
61
- timestamp TEXT NOT NULL,
62
- data TEXT NOT NULL DEFAULT '{}',
63
- FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE
64
- );
65
-
66
- CREATE INDEX IF NOT EXISTS idx_task_activity_timestamp
67
- ON task_activity (timestamp ASC, id ASC);
68
-
69
- CREATE INDEX IF NOT EXISTS idx_task_activity_task_id
70
- ON task_activity (task_id, timestamp ASC, id ASC);
71
- `)
72
-
73
- try {
74
- db.exec(`ALTER TABLE tasks ADD COLUMN reported INTEGER NOT NULL DEFAULT 0`)
75
- } catch {
76
- /* */
77
- }
78
-
79
- try {
80
- db.exec(`ALTER TABLE conversations ADD COLUMN context_summary TEXT`)
81
- } catch {
82
- /* */
83
- }
84
-
85
- try {
86
- db.exec(`ALTER TABLE conversations ADD COLUMN context_checkpoint_id INTEGER`)
87
- } catch {
88
- /* */
89
- }
90
-
91
- try {
92
- db.exec(`
93
- CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
94
- task_id UNINDEXED,
95
- instructions,
96
- result,
97
- tokenize = 'unicode61'
98
- )
99
- `)
100
- } catch {
101
- /* table may already exist */
102
- }
103
-
104
- try {
105
- db.exec(`
106
- CREATE TABLE IF NOT EXISTS knowledge_patterns (
107
- id TEXT PRIMARY KEY,
108
- trigger TEXT NOT NULL,
109
- solution TEXT NOT NULL,
110
- created_at TEXT NOT NULL
111
- )
112
- `)
113
- db.exec(`
114
- CREATE VIRTUAL TABLE IF NOT EXISTS patterns_fts USING fts5(
115
- pattern_id UNINDEXED,
116
- trigger,
117
- solution,
118
- tokenize = 'unicode61'
119
- )
120
- `)
121
- } catch {
122
- /* tables may already exist */
123
- }
22
+ runMigrations(db, migrations)
124
23
  }
125
24
 
126
25
  export function openDb(dbPath) {
package/src/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  export * from './db.js'
2
- export * from './messages.js'
3
- export * from './config.js'
4
- export * from './tasks.js'
2
+ export * from './repos/messages.js'
3
+ export * from './repos/tasks.js'
4
+ export * from './repos/tools.js'
5
+ export * from './repos/settings.js'
6
+ export * from './repos/mcp-servers.js'
7
+ export * from './repos/schedules.js'
8
+ export * from './repos/tool-secrets.js'
9
+ export * from './repos/patterns.js'
10
+ export * from './repos/vectors.js'
@@ -0,0 +1,217 @@
1
+ export const name = '001_initial_schema'
2
+
3
+ export function up(db) {
4
+ db.exec(`
5
+ CREATE TABLE conversations (
6
+ id TEXT PRIMARY KEY,
7
+ title TEXT,
8
+ user_info TEXT NOT NULL DEFAULT '{}',
9
+ context_summary TEXT,
10
+ context_checkpoint_id TEXT,
11
+ created_at TEXT NOT NULL,
12
+ updated_at TEXT NOT NULL
13
+ );
14
+
15
+ CREATE TABLE messages (
16
+ sort_order INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ id TEXT NOT NULL UNIQUE,
18
+ conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
19
+ role TEXT NOT NULL CHECK(role IN ('user','assistant','system','tool','tool_result')),
20
+ content TEXT NOT NULL,
21
+ tokens INTEGER,
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+
26
+ CREATE INDEX idx_messages_conversation
27
+ ON messages(conversation_id, sort_order);
28
+
29
+ CREATE TABLE tasks (
30
+ id TEXT PRIMARY KEY,
31
+ instructions TEXT NOT NULL DEFAULT '',
32
+ context TEXT NOT NULL DEFAULT '',
33
+ status TEXT NOT NULL DEFAULT 'queued'
34
+ CHECK(status IN ('queued','running','completed','incomplete','failed','aborted')),
35
+ current_plan TEXT NOT NULL DEFAULT '',
36
+ result TEXT,
37
+ error TEXT,
38
+ abort_reason TEXT,
39
+ provider TEXT,
40
+ model TEXT,
41
+ context_injected INTEGER NOT NULL DEFAULT 0,
42
+ created_at TEXT NOT NULL,
43
+ updated_at TEXT NOT NULL,
44
+ completed_at TEXT
45
+ );
46
+
47
+ CREATE INDEX idx_tasks_created
48
+ ON tasks(created_at DESC, id DESC);
49
+
50
+ CREATE INDEX idx_tasks_status
51
+ ON tasks(status, created_at DESC);
52
+
53
+ CREATE TABLE task_activity (
54
+ id TEXT PRIMARY KEY,
55
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
56
+ type TEXT NOT NULL CHECK(type IN ('tool','spawn','success','error','info','step')),
57
+ name TEXT,
58
+ result TEXT,
59
+ data TEXT NOT NULL DEFAULT '{}',
60
+ created_at TEXT NOT NULL
61
+ );
62
+
63
+ CREATE INDEX idx_task_activity_task
64
+ ON task_activity(task_id, created_at ASC, id ASC);
65
+
66
+ CREATE VIRTUAL TABLE tasks_fts USING fts5(
67
+ task_id UNINDEXED,
68
+ instructions,
69
+ result,
70
+ tokenize = 'unicode61'
71
+ );
72
+
73
+ CREATE TRIGGER tasks_fts_insert AFTER INSERT ON tasks BEGIN
74
+ INSERT INTO tasks_fts(task_id, instructions, result)
75
+ VALUES (NEW.id, NEW.instructions, COALESCE(NEW.result, ''));
76
+ END;
77
+
78
+ CREATE TRIGGER tasks_fts_update AFTER UPDATE OF instructions, result ON tasks BEGIN
79
+ DELETE FROM tasks_fts WHERE task_id = OLD.id;
80
+ INSERT INTO tasks_fts(task_id, instructions, result)
81
+ VALUES (NEW.id, NEW.instructions, COALESCE(NEW.result, ''));
82
+ END;
83
+
84
+ CREATE TRIGGER tasks_fts_delete AFTER DELETE ON tasks BEGIN
85
+ DELETE FROM tasks_fts WHERE task_id = OLD.id;
86
+ END;
87
+
88
+ CREATE VIRTUAL TABLE messages_fts USING fts5(
89
+ message_id UNINDEXED,
90
+ role UNINDEXED,
91
+ content,
92
+ tokenize = 'unicode61'
93
+ );
94
+
95
+ CREATE TRIGGER messages_fts_insert AFTER INSERT ON messages BEGIN
96
+ INSERT INTO messages_fts(message_id, role, content)
97
+ VALUES (NEW.id, NEW.role, NEW.content);
98
+ END;
99
+
100
+ CREATE TRIGGER messages_fts_update AFTER UPDATE OF content ON messages BEGIN
101
+ DELETE FROM messages_fts WHERE message_id = OLD.id;
102
+ INSERT INTO messages_fts(message_id, role, content)
103
+ VALUES (NEW.id, NEW.role, NEW.content);
104
+ END;
105
+
106
+ CREATE TRIGGER messages_fts_delete AFTER DELETE ON messages BEGIN
107
+ DELETE FROM messages_fts WHERE message_id = OLD.id;
108
+ END;
109
+
110
+ CREATE TABLE tools (
111
+ id TEXT PRIMARY KEY,
112
+ name TEXT NOT NULL UNIQUE CHECK(length(name) BETWEEN 1 AND 64),
113
+ description TEXT NOT NULL DEFAULT '',
114
+ parameters TEXT NOT NULL DEFAULT '{"type":"object","properties":{}}',
115
+ source_type TEXT NOT NULL CHECK(source_type IN ('js_function','http_webhook','desktop')),
116
+ source_code TEXT,
117
+ webhook_url TEXT,
118
+ webhook_headers TEXT NOT NULL DEFAULT '{}',
119
+ is_enabled INTEGER NOT NULL DEFAULT 1,
120
+ tags TEXT NOT NULL DEFAULT '[]',
121
+ version INTEGER NOT NULL DEFAULT 1,
122
+ created_at TEXT NOT NULL,
123
+ updated_at TEXT NOT NULL
124
+ );
125
+
126
+ CREATE INDEX idx_tools_enabled
127
+ ON tools(is_enabled) WHERE is_enabled = 1;
128
+
129
+ CREATE TABLE tool_secrets (
130
+ id TEXT PRIMARY KEY,
131
+ tool_id TEXT NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
132
+ key TEXT NOT NULL,
133
+ encrypted_value TEXT NOT NULL,
134
+ created_at TEXT NOT NULL,
135
+ updated_at TEXT NOT NULL,
136
+ UNIQUE(tool_id, key)
137
+ );
138
+
139
+ CREATE INDEX idx_tool_secrets_tool
140
+ ON tool_secrets(tool_id);
141
+
142
+ CREATE TABLE mcp_servers (
143
+ id TEXT PRIMARY KEY,
144
+ name TEXT NOT NULL,
145
+ transport TEXT NOT NULL CHECK(transport IN ('stdio','sse','http')),
146
+ command TEXT,
147
+ args TEXT NOT NULL DEFAULT '[]',
148
+ url TEXT,
149
+ env TEXT NOT NULL DEFAULT '{}',
150
+ is_enabled INTEGER NOT NULL DEFAULT 1,
151
+ created_at TEXT NOT NULL,
152
+ updated_at TEXT NOT NULL
153
+ );
154
+
155
+ CREATE TABLE settings (
156
+ key TEXT PRIMARY KEY,
157
+ value TEXT NOT NULL,
158
+ updated_at TEXT NOT NULL
159
+ );
160
+
161
+ CREATE TABLE schedules (
162
+ id TEXT PRIMARY KEY,
163
+ cron_expr TEXT NOT NULL,
164
+ timezone TEXT,
165
+ prompt TEXT NOT NULL,
166
+ channel TEXT,
167
+ is_enabled INTEGER NOT NULL DEFAULT 1,
168
+ once INTEGER NOT NULL DEFAULT 0,
169
+ created_at TEXT NOT NULL,
170
+ updated_at TEXT NOT NULL
171
+ );
172
+
173
+ CREATE TABLE vectors (
174
+ id TEXT NOT NULL,
175
+ collection TEXT NOT NULL,
176
+ embedding BLOB NOT NULL,
177
+ metadata TEXT NOT NULL DEFAULT '{}',
178
+ created_at TEXT NOT NULL,
179
+ updated_at TEXT NOT NULL,
180
+ PRIMARY KEY (id, collection)
181
+ );
182
+
183
+ CREATE INDEX idx_vectors_collection
184
+ ON vectors(collection);
185
+
186
+ CREATE TABLE knowledge_patterns (
187
+ id TEXT PRIMARY KEY,
188
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
189
+ trigger TEXT NOT NULL,
190
+ solution TEXT NOT NULL,
191
+ created_at TEXT NOT NULL,
192
+ updated_at TEXT NOT NULL
193
+ );
194
+
195
+ CREATE VIRTUAL TABLE patterns_fts USING fts5(
196
+ pattern_id UNINDEXED,
197
+ trigger,
198
+ solution,
199
+ tokenize = 'unicode61'
200
+ );
201
+
202
+ CREATE TRIGGER patterns_fts_insert AFTER INSERT ON knowledge_patterns BEGIN
203
+ INSERT INTO patterns_fts(pattern_id, trigger, solution)
204
+ VALUES (NEW.id, NEW.trigger, NEW.solution);
205
+ END;
206
+
207
+ CREATE TRIGGER patterns_fts_update AFTER UPDATE OF trigger, solution ON knowledge_patterns BEGIN
208
+ DELETE FROM patterns_fts WHERE pattern_id = OLD.id;
209
+ INSERT INTO patterns_fts(pattern_id, trigger, solution)
210
+ VALUES (NEW.id, NEW.trigger, NEW.solution);
211
+ END;
212
+
213
+ CREATE TRIGGER patterns_fts_delete AFTER DELETE ON knowledge_patterns BEGIN
214
+ DELETE FROM patterns_fts WHERE pattern_id = OLD.id;
215
+ END;
216
+ `)
217
+ }
@@ -0,0 +1,45 @@
1
+ export const name = '002_task_activity_types'
2
+
3
+ export function up(db) {
4
+ db.exec(`
5
+ CREATE TABLE task_activity_new (
6
+ id TEXT PRIMARY KEY,
7
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
8
+ type TEXT NOT NULL CHECK(type IN (
9
+ 'tool_call','tool_result','text','thought',
10
+ 'journal','spawn','error'
11
+ )),
12
+ name TEXT,
13
+ args TEXT,
14
+ result TEXT,
15
+ plan TEXT,
16
+ data TEXT NOT NULL DEFAULT '{}',
17
+ created_at TEXT NOT NULL
18
+ );
19
+
20
+ INSERT INTO task_activity_new (id, task_id, type, name, args, result, plan, data, created_at)
21
+ SELECT
22
+ id,
23
+ task_id,
24
+ CASE type
25
+ WHEN 'tool' THEN 'tool_call'
26
+ WHEN 'step' THEN 'journal'
27
+ WHEN 'info' THEN 'text'
28
+ WHEN 'success' THEN 'text'
29
+ ELSE type
30
+ END,
31
+ name,
32
+ NULL,
33
+ result,
34
+ NULL,
35
+ data,
36
+ created_at
37
+ FROM task_activity;
38
+
39
+ DROP TABLE task_activity;
40
+ ALTER TABLE task_activity_new RENAME TO task_activity;
41
+
42
+ CREATE INDEX idx_task_activity_task
43
+ ON task_activity(task_id, created_at ASC, id ASC);
44
+ `)
45
+ }
@@ -0,0 +1,31 @@
1
+ export function runMigrations(db, migrations) {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS _migrations (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ name TEXT NOT NULL UNIQUE,
6
+ applied_at TEXT NOT NULL
7
+ )
8
+ `)
9
+
10
+ const applied = new Set(
11
+ db
12
+ .prepare('SELECT name FROM _migrations')
13
+ .all()
14
+ .map((r) => r.name)
15
+ )
16
+
17
+ const pending = migrations
18
+ .slice()
19
+ .sort((a, b) => a.name.localeCompare(b.name))
20
+ .filter((m) => !applied.has(m.name))
21
+
22
+ for (const migration of pending) {
23
+ db.transaction(() => {
24
+ migration.up(db)
25
+ db.prepare('INSERT INTO _migrations (name, applied_at) VALUES (?, ?)').run(
26
+ migration.name,
27
+ new Date().toISOString()
28
+ )
29
+ })()
30
+ }
31
+ }
@@ -0,0 +1,81 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ function mapServer(row) {
4
+ if (!row) return null
5
+ return {
6
+ id: row.id,
7
+ name: row.name,
8
+ transport: row.transport,
9
+ command: row.command || null,
10
+ args: JSON.parse(row.args),
11
+ url: row.url || null,
12
+ env: JSON.parse(row.env),
13
+ isEnabled: !!row.is_enabled,
14
+ createdAt: row.created_at,
15
+ updatedAt: row.updated_at
16
+ }
17
+ }
18
+
19
+ export function createMcpServer(db, server) {
20
+ const id = server.id || randomUUID()
21
+ const now = new Date().toISOString()
22
+
23
+ db.prepare(
24
+ `INSERT INTO mcp_servers (id, name, transport, command, args, url, env, is_enabled, created_at, updated_at)
25
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
26
+ ).run(
27
+ id,
28
+ String(server.name),
29
+ String(server.transport || 'stdio'),
30
+ server.command || null,
31
+ JSON.stringify(server.args || []),
32
+ server.url || null,
33
+ JSON.stringify(server.env || {}),
34
+ server.isEnabled === false ? 0 : 1,
35
+ now,
36
+ now
37
+ )
38
+
39
+ return getMcpServer(db, id)
40
+ }
41
+
42
+ export function updateMcpServer(db, id, updates) {
43
+ const existing = getMcpServer(db, id)
44
+ if (!existing) return null
45
+
46
+ const now = new Date().toISOString()
47
+ const merged = { ...existing, ...updates }
48
+
49
+ db.prepare(
50
+ `UPDATE mcp_servers SET name = ?, transport = ?, command = ?, args = ?, url = ?,
51
+ env = ?, is_enabled = ?, updated_at = ?
52
+ WHERE id = ?`
53
+ ).run(
54
+ String(merged.name),
55
+ String(merged.transport || 'stdio'),
56
+ merged.command || null,
57
+ JSON.stringify(merged.args || []),
58
+ merged.url || null,
59
+ JSON.stringify(merged.env || {}),
60
+ merged.isEnabled === false ? 0 : 1,
61
+ now,
62
+ id
63
+ )
64
+
65
+ return getMcpServer(db, id)
66
+ }
67
+
68
+ export function deleteMcpServer(db, id) {
69
+ return db.prepare(`DELETE FROM mcp_servers WHERE id = ?`).run(id)
70
+ }
71
+
72
+ export function getMcpServer(db, id) {
73
+ return mapServer(db.prepare(`SELECT * FROM mcp_servers WHERE id = ?`).get(id))
74
+ }
75
+
76
+ export function listMcpServers(db, enabledOnly = false) {
77
+ if (enabledOnly) {
78
+ return db.prepare(`SELECT * FROM mcp_servers WHERE is_enabled = 1`).all().map(mapServer)
79
+ }
80
+ return db.prepare(`SELECT * FROM mcp_servers ORDER BY name ASC`).all().map(mapServer)
81
+ }