@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.
@@ -0,0 +1,219 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ export const DEFAULT_CONVERSATION_ID = 'main'
4
+
5
+ function getConversationId(conversationId) {
6
+ if (!conversationId) return DEFAULT_CONVERSATION_ID
7
+ const normalized = String(conversationId).trim()
8
+ if (!normalized) throw new Error('conversationId cannot be an empty string')
9
+ return normalized
10
+ }
11
+
12
+ function normalizeLimit(limit) {
13
+ const parsed = Number.parseInt(limit, 10)
14
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null
15
+ }
16
+
17
+ function mapRow(row) {
18
+ if (!row) return null
19
+ return {
20
+ id: row.id,
21
+ sortOrder: row.sort_order,
22
+ conversationId: row.conversation_id,
23
+ role: row.role,
24
+ content: row.content,
25
+ tokens: row.tokens ?? null,
26
+ createdAt: row.created_at,
27
+ updatedAt: row.updated_at
28
+ }
29
+ }
30
+
31
+ export function ensureConversation(db, conversationId = DEFAULT_CONVERSATION_ID) {
32
+ const id = getConversationId(conversationId)
33
+ const now = new Date().toISOString()
34
+
35
+ db.prepare(
36
+ `INSERT INTO conversations (id, created_at, updated_at)
37
+ VALUES (?, ?, ?)
38
+ ON CONFLICT(id) DO NOTHING`
39
+ ).run(id, now, now)
40
+
41
+ return db
42
+ .prepare(
43
+ `SELECT id, title, user_info, context_summary, context_checkpoint_id, created_at, updated_at FROM conversations WHERE id = ?`
44
+ )
45
+ .get(id)
46
+ }
47
+
48
+ export function touchConversation(db, conversationId = DEFAULT_CONVERSATION_ID) {
49
+ const id = getConversationId(conversationId)
50
+ const now = new Date().toISOString()
51
+
52
+ db.prepare(
53
+ `INSERT INTO conversations (id, created_at, updated_at)
54
+ VALUES (?, ?, ?)
55
+ ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at`
56
+ ).run(id, now, now)
57
+
58
+ return db
59
+ .prepare(
60
+ `SELECT id, title, user_info, context_summary, context_checkpoint_id, created_at, updated_at FROM conversations WHERE id = ?`
61
+ )
62
+ .get(id)
63
+ }
64
+
65
+ export function appendMessage(
66
+ db,
67
+ role,
68
+ content,
69
+ conversationId = DEFAULT_CONVERSATION_ID,
70
+ tokens = null
71
+ ) {
72
+ const convId = getConversationId(conversationId)
73
+ const msgId = randomUUID()
74
+ const now = new Date().toISOString()
75
+ if (!role) throw new Error('role is required')
76
+ const normalizedRole = String(role).trim()
77
+ if (!normalizedRole) throw new Error('role cannot be empty')
78
+ const normalizedContent = String(content ?? '')
79
+
80
+ touchConversation(db, convId)
81
+
82
+ db.prepare(
83
+ `INSERT INTO messages (id, conversation_id, role, content, tokens, created_at, updated_at)
84
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
85
+ ).run(msgId, convId, normalizedRole, normalizedContent, tokens, now, now)
86
+
87
+ return mapRow(
88
+ db
89
+ .prepare(
90
+ `SELECT sort_order, id, conversation_id, role, content, tokens, created_at, updated_at
91
+ FROM messages WHERE id = ?`
92
+ )
93
+ .get(msgId)
94
+ )
95
+ }
96
+
97
+ export function getMessages(db, conversationId = DEFAULT_CONVERSATION_ID, limit) {
98
+ const id = getConversationId(conversationId)
99
+ const normalizedLimit = normalizeLimit(limit)
100
+
101
+ if (!normalizedLimit) {
102
+ return db
103
+ .prepare(
104
+ `SELECT sort_order, id, conversation_id, role, content, tokens, created_at, updated_at
105
+ FROM messages
106
+ WHERE conversation_id = ?
107
+ ORDER BY sort_order ASC`
108
+ )
109
+ .all(id)
110
+ .map(mapRow)
111
+ }
112
+
113
+ return db
114
+ .prepare(
115
+ `SELECT sort_order, id, conversation_id, role, content, tokens, created_at, updated_at
116
+ FROM (
117
+ SELECT sort_order, id, conversation_id, role, content, tokens, created_at, updated_at
118
+ FROM messages
119
+ WHERE conversation_id = ?
120
+ ORDER BY sort_order DESC
121
+ LIMIT ?
122
+ )
123
+ ORDER BY sort_order ASC`
124
+ )
125
+ .all(id, normalizedLimit)
126
+ .map(mapRow)
127
+ }
128
+
129
+ export function getMessagesBeforeId(
130
+ db,
131
+ beforeId,
132
+ conversationId = DEFAULT_CONVERSATION_ID,
133
+ limit = 50
134
+ ) {
135
+ const convId = getConversationId(conversationId)
136
+ const normalizedLimit = normalizeLimit(limit)
137
+ if (!normalizedLimit) throw new Error('limit must be a positive integer')
138
+
139
+ if (!beforeId) return []
140
+ const normalizedBeforeId = String(beforeId).trim()
141
+ if (!normalizedBeforeId) return []
142
+
143
+ const anchor = db
144
+ .prepare(`SELECT sort_order FROM messages WHERE id = ? AND conversation_id = ?`)
145
+ .get(normalizedBeforeId, convId)
146
+
147
+ if (!anchor) return []
148
+
149
+ return db
150
+ .prepare(
151
+ `SELECT sort_order, id, conversation_id, role, content, tokens, created_at, updated_at
152
+ FROM (
153
+ SELECT sort_order, id, conversation_id, role, content, tokens, created_at, updated_at
154
+ FROM messages
155
+ WHERE conversation_id = ? AND sort_order < ?
156
+ ORDER BY sort_order DESC
157
+ LIMIT ?
158
+ )
159
+ ORDER BY sort_order ASC`
160
+ )
161
+ .all(convId, anchor.sort_order, normalizedLimit)
162
+ .map(mapRow)
163
+ }
164
+
165
+ export function clearMessages(db, conversationId = DEFAULT_CONVERSATION_ID) {
166
+ const id = getConversationId(conversationId)
167
+ ensureConversation(db, id)
168
+ return db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(id)
169
+ }
170
+
171
+ export function saveSummaryCheckpoint(
172
+ db,
173
+ summary,
174
+ checkpointId,
175
+ conversationId = DEFAULT_CONVERSATION_ID
176
+ ) {
177
+ const id = getConversationId(conversationId)
178
+ ensureConversation(db, id)
179
+ db.prepare(
180
+ `UPDATE conversations SET context_summary = ?, context_checkpoint_id = ?, updated_at = ? WHERE id = ?`
181
+ ).run(summary || null, checkpointId || null, new Date().toISOString(), id)
182
+ }
183
+
184
+ export function loadSummaryCheckpoint(db, conversationId = DEFAULT_CONVERSATION_ID) {
185
+ const id = getConversationId(conversationId)
186
+ const row = db
187
+ .prepare(`SELECT context_summary, context_checkpoint_id FROM conversations WHERE id = ?`)
188
+ .get(id)
189
+ if (!row || !row.context_summary) return null
190
+ return { summary: row.context_summary, checkpointId: row.context_checkpoint_id }
191
+ }
192
+
193
+ export function clearSummaryCheckpoint(db, conversationId = DEFAULT_CONVERSATION_ID) {
194
+ const id = getConversationId(conversationId)
195
+ db.prepare(
196
+ `UPDATE conversations SET context_summary = NULL, context_checkpoint_id = NULL, updated_at = ? WHERE id = ?`
197
+ ).run(new Date().toISOString(), id)
198
+ }
199
+
200
+ export function getConversationUserInfo(db, conversationId = DEFAULT_CONVERSATION_ID) {
201
+ const id = getConversationId(conversationId)
202
+ const row = db.prepare(`SELECT user_info FROM conversations WHERE id = ?`).get(id)
203
+ if (!row) return {}
204
+ try {
205
+ return JSON.parse(row.user_info)
206
+ } catch {
207
+ return {}
208
+ }
209
+ }
210
+
211
+ export function setConversationUserInfo(db, userInfo, conversationId = DEFAULT_CONVERSATION_ID) {
212
+ const id = getConversationId(conversationId)
213
+ ensureConversation(db, id)
214
+ db.prepare(`UPDATE conversations SET user_info = ?, updated_at = ? WHERE id = ?`).run(
215
+ JSON.stringify(userInfo || {}),
216
+ new Date().toISOString(),
217
+ id
218
+ )
219
+ }
@@ -0,0 +1,54 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ function mapPattern(row) {
4
+ if (!row) return null
5
+ return {
6
+ id: row.id,
7
+ taskId: row.task_id || null,
8
+ trigger: row.trigger,
9
+ solution: row.solution,
10
+ createdAt: row.created_at,
11
+ updatedAt: row.updated_at
12
+ }
13
+ }
14
+
15
+ export function insertPattern(db, pattern) {
16
+ const id = pattern.id || randomUUID()
17
+ const now = new Date().toISOString()
18
+
19
+ db.prepare(
20
+ `INSERT OR REPLACE INTO knowledge_patterns (id, task_id, trigger, solution, created_at, updated_at)
21
+ VALUES (?, ?, ?, ?, ?, ?)`
22
+ ).run(id, pattern.taskId || null, String(pattern.trigger), String(pattern.solution), now, now)
23
+
24
+ return getPattern(db, id)
25
+ }
26
+
27
+ export function getPattern(db, id) {
28
+ return mapPattern(db.prepare(`SELECT * FROM knowledge_patterns WHERE id = ?`).get(id))
29
+ }
30
+
31
+ export function searchPatternsFts(db, query) {
32
+ if (!query?.trim()) return []
33
+ const terms = query
34
+ .trim()
35
+ .split(/\s+/)
36
+ .filter(Boolean)
37
+ .map((t) => `"${t.replace(/"/g, '')}"`)
38
+ .join(' ')
39
+ try {
40
+ return db
41
+ .prepare(
42
+ `SELECT p.id, p.task_id, p.trigger, p.solution, p.created_at, p.updated_at
43
+ FROM patterns_fts f
44
+ JOIN knowledge_patterns p ON p.id = f.pattern_id
45
+ WHERE f.patterns_fts MATCH ?
46
+ ORDER BY bm25(f.patterns_fts)
47
+ LIMIT 5`
48
+ )
49
+ .all(terms)
50
+ .map(mapPattern)
51
+ } catch {
52
+ return []
53
+ }
54
+ }
@@ -0,0 +1,58 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ function mapSchedule(row) {
4
+ if (!row) return null
5
+ return {
6
+ id: row.id,
7
+ cronExpr: row.cron_expr,
8
+ timezone: row.timezone || null,
9
+ prompt: row.prompt,
10
+ channel: row.channel || null,
11
+ isEnabled: !!row.is_enabled,
12
+ once: !!row.once,
13
+ createdAt: row.created_at,
14
+ updatedAt: row.updated_at
15
+ }
16
+ }
17
+
18
+ export function saveSchedule(db, schedule) {
19
+ const id = schedule.id || randomUUID()
20
+ const now = new Date().toISOString()
21
+
22
+ db.prepare(
23
+ `INSERT INTO schedules (id, cron_expr, timezone, prompt, channel, is_enabled, once, created_at, updated_at)
24
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
25
+ ON CONFLICT(id) DO UPDATE SET
26
+ cron_expr = excluded.cron_expr,
27
+ timezone = excluded.timezone,
28
+ prompt = excluded.prompt,
29
+ channel = excluded.channel,
30
+ is_enabled = excluded.is_enabled,
31
+ once = excluded.once,
32
+ updated_at = excluded.updated_at`
33
+ ).run(
34
+ id,
35
+ String(schedule.cronExpr || schedule.expr),
36
+ schedule.timezone || schedule.tz || null,
37
+ String(schedule.prompt),
38
+ schedule.channel || null,
39
+ schedule.isEnabled === false ? 0 : 1,
40
+ schedule.once ? 1 : 0,
41
+ now,
42
+ now
43
+ )
44
+
45
+ return getSchedule(db, id)
46
+ }
47
+
48
+ export function removeSchedule(db, id) {
49
+ return db.prepare(`DELETE FROM schedules WHERE id = ?`).run(id)
50
+ }
51
+
52
+ export function getSchedule(db, id) {
53
+ return mapSchedule(db.prepare(`SELECT * FROM schedules WHERE id = ?`).get(id))
54
+ }
55
+
56
+ export function listSchedules(db) {
57
+ return db.prepare(`SELECT * FROM schedules ORDER BY created_at ASC`).all().map(mapSchedule)
58
+ }
@@ -0,0 +1,38 @@
1
+ export function getSetting(db, key) {
2
+ const row = db.prepare(`SELECT value FROM settings WHERE key = ?`).get(key)
3
+ return row ? row.value : undefined
4
+ }
5
+
6
+ export function getSettingJson(db, key, fallback = null) {
7
+ const raw = getSetting(db, key)
8
+ if (raw === undefined) return fallback
9
+ try {
10
+ return JSON.parse(raw)
11
+ } catch {
12
+ return fallback
13
+ }
14
+ }
15
+
16
+ export function setSetting(db, key, value) {
17
+ const now = new Date().toISOString()
18
+ const raw = JSON.stringify(value)
19
+ db.prepare(
20
+ `INSERT INTO settings (key, value, updated_at)
21
+ VALUES (?, ?, ?)
22
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
23
+ ).run(key, raw, now)
24
+ return value
25
+ }
26
+
27
+ export function deleteSetting(db, key) {
28
+ return db.prepare(`DELETE FROM settings WHERE key = ?`).run(key).changes > 0
29
+ }
30
+
31
+ export function getAllSettings(db) {
32
+ const rows = db.prepare(`SELECT key, value FROM settings`).all()
33
+ const result = {}
34
+ for (const row of rows) {
35
+ result[row.key] = row.value
36
+ }
37
+ return result
38
+ }
@@ -0,0 +1,195 @@
1
+ function parseJson(value, fallback) {
2
+ if (value === null || value === undefined || value === '') return fallback
3
+ try {
4
+ return JSON.parse(value)
5
+ } catch {
6
+ return fallback
7
+ }
8
+ }
9
+
10
+ function stringifyJson(value, fallback = null) {
11
+ if (value === null || value === undefined) return fallback
12
+ try {
13
+ return JSON.stringify(value)
14
+ } catch {
15
+ return fallback
16
+ }
17
+ }
18
+
19
+ function mapTask(row) {
20
+ if (!row) return null
21
+ return {
22
+ id: row.id,
23
+ instructions: row.instructions,
24
+ context: row.context,
25
+ status: row.status,
26
+ createdAt: row.created_at,
27
+ updatedAt: row.updated_at,
28
+ currentPlan: row.current_plan,
29
+ result: row.result,
30
+ error: row.error || null,
31
+ abortReason: row.abort_reason || null,
32
+ provider: row.provider || null,
33
+ model: row.model || null,
34
+ contextInjected: !!row.context_injected,
35
+ completedAt: row.completed_at || null
36
+ }
37
+ }
38
+
39
+ function mapActivity(row) {
40
+ if (!row) return null
41
+ return {
42
+ id: row.id,
43
+ taskId: row.task_id,
44
+ type: row.type,
45
+ name: row.name || null,
46
+ args: parseJson(row.args, row.args),
47
+ result: parseJson(row.result, row.result),
48
+ plan: row.plan || null,
49
+ createdAt: row.created_at,
50
+ data: parseJson(row.data, {})
51
+ }
52
+ }
53
+
54
+ export function upsertTask(db, task) {
55
+ const id = String(task?.id || '').trim()
56
+ if (!id) throw new Error('task id is required.')
57
+
58
+ const createdAt = String(task?.createdAt || new Date().toISOString())
59
+ const updatedAt = String(task?.updatedAt || new Date().toISOString())
60
+
61
+ db.prepare(
62
+ `INSERT INTO tasks (
63
+ id, instructions, context, status, created_at, updated_at,
64
+ current_plan, result, error, abort_reason, provider, model,
65
+ context_injected, completed_at
66
+ )
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
68
+ ON CONFLICT(id) DO UPDATE SET
69
+ instructions = excluded.instructions,
70
+ context = excluded.context,
71
+ status = excluded.status,
72
+ updated_at = excluded.updated_at,
73
+ current_plan = excluded.current_plan,
74
+ result = excluded.result,
75
+ error = excluded.error,
76
+ abort_reason = excluded.abort_reason,
77
+ provider = excluded.provider,
78
+ model = excluded.model,
79
+ context_injected = excluded.context_injected,
80
+ completed_at = excluded.completed_at`
81
+ ).run(
82
+ id,
83
+ String(task?.instructions || ''),
84
+ String(task?.context || ''),
85
+ String(task?.status || 'queued'),
86
+ createdAt,
87
+ updatedAt,
88
+ String(task?.currentPlan || ''),
89
+ task?.result == null ? null : String(task.result),
90
+ task?.error == null ? null : String(task.error),
91
+ task?.abortReason == null ? null : String(task.abortReason),
92
+ task?.provider == null ? null : String(task.provider),
93
+ task?.model == null ? null : String(task.model),
94
+ task?.contextInjected ? 1 : 0,
95
+ task?.completedAt == null ? null : String(task.completedAt)
96
+ )
97
+
98
+ return getTask(db, id)
99
+ }
100
+
101
+ export function getTask(db, id) {
102
+ return mapTask(
103
+ db
104
+ .prepare(
105
+ `SELECT id, instructions, context, status, created_at, updated_at,
106
+ current_plan, result, error, abort_reason, provider, model,
107
+ context_injected, completed_at
108
+ FROM tasks WHERE id = ?`
109
+ )
110
+ .get(String(id || '').trim())
111
+ )
112
+ }
113
+
114
+ export function loadTasks(db) {
115
+ return db
116
+ .prepare(
117
+ `SELECT id, instructions, context, status, created_at, updated_at,
118
+ current_plan, result, error, abort_reason, provider, model,
119
+ context_injected, completed_at
120
+ FROM tasks
121
+ ORDER BY created_at DESC, id DESC`
122
+ )
123
+ .all()
124
+ .map(mapTask)
125
+ }
126
+
127
+ export function appendTaskActivity(db, activity) {
128
+ const id = String(activity?.id ?? '').trim()
129
+ const taskId = String(activity?.taskId ?? '').trim()
130
+ if (!id || !taskId) throw new Error('Task activity requires id and taskId.')
131
+
132
+ const VALID_TYPES = new Set([
133
+ 'tool_call',
134
+ 'tool_result',
135
+ 'text',
136
+ 'thought',
137
+ 'journal',
138
+ 'spawn',
139
+ 'error'
140
+ ])
141
+ const type = String(activity?.type ?? '')
142
+ if (!VALID_TYPES.has(type)) throw new Error(`Invalid task activity type: "${type}"`)
143
+
144
+ db.prepare(
145
+ `INSERT INTO task_activity (id, task_id, type, name, args, result, plan, data, created_at)
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
147
+ ON CONFLICT(id) DO UPDATE SET
148
+ task_id = excluded.task_id,
149
+ type = excluded.type,
150
+ name = excluded.name,
151
+ args = excluded.args,
152
+ result = excluded.result,
153
+ plan = excluded.plan,
154
+ data = excluded.data,
155
+ created_at = excluded.created_at`
156
+ ).run(
157
+ id,
158
+ taskId,
159
+ type,
160
+ activity?.name ? String(activity.name) : null,
161
+ activity?.args ? String(activity.args) : null,
162
+ stringifyJson(
163
+ activity?.result,
164
+ activity?.result === undefined ? null : String(activity.result)
165
+ ),
166
+ activity?.plan ? String(activity.plan) : null,
167
+ stringifyJson(activity?.data ?? {}, '{}'),
168
+ String(activity?.createdAt ?? new Date().toISOString())
169
+ )
170
+
171
+ return id
172
+ }
173
+
174
+ export function loadTaskActivity(db, taskId) {
175
+ return db
176
+ .prepare(
177
+ `SELECT id, task_id, type, name, args, result, plan, data, created_at
178
+ FROM task_activity
179
+ WHERE task_id = ?
180
+ ORDER BY created_at ASC, id ASC`
181
+ )
182
+ .all(String(taskId || '').trim())
183
+ .map(mapActivity)
184
+ }
185
+
186
+ export function loadAllTaskActivity(db) {
187
+ return db
188
+ .prepare(
189
+ `SELECT id, task_id, type, name, args, result, plan, data, created_at
190
+ FROM task_activity
191
+ ORDER BY created_at ASC, id ASC`
192
+ )
193
+ .all()
194
+ .map(mapActivity)
195
+ }
@@ -0,0 +1,26 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ export function setToolSecret(db, toolId, key, encryptedValue) {
4
+ const now = new Date().toISOString()
5
+ const id = randomUUID()
6
+
7
+ db.prepare(
8
+ `INSERT INTO tool_secrets (id, tool_id, key, encrypted_value, created_at, updated_at)
9
+ VALUES (?, ?, ?, ?, ?, ?)
10
+ ON CONFLICT(tool_id, key) DO UPDATE SET
11
+ encrypted_value = excluded.encrypted_value,
12
+ updated_at = excluded.updated_at`
13
+ ).run(id, toolId, key, encryptedValue, now, now)
14
+ }
15
+
16
+ export function getToolSecrets(db, toolId) {
17
+ return db.prepare(`SELECT key, encrypted_value FROM tool_secrets WHERE tool_id = ?`).all(toolId)
18
+ }
19
+
20
+ export function deleteToolSecret(db, toolId, key) {
21
+ return db.prepare(`DELETE FROM tool_secrets WHERE tool_id = ? AND key = ?`).run(toolId, key)
22
+ }
23
+
24
+ export function deleteAllToolSecrets(db, toolId) {
25
+ return db.prepare(`DELETE FROM tool_secrets WHERE tool_id = ?`).run(toolId)
26
+ }
@@ -0,0 +1,104 @@
1
+ import { randomUUID } from 'crypto'
2
+
3
+ function mapTool(row) {
4
+ if (!row) return null
5
+ return {
6
+ id: row.id,
7
+ name: row.name,
8
+ description: row.description,
9
+ parameters: JSON.parse(row.parameters),
10
+ sourceType: row.source_type,
11
+ sourceCode: row.source_code || null,
12
+ webhookUrl: row.webhook_url || null,
13
+ webhookHeaders: JSON.parse(row.webhook_headers),
14
+ isEnabled: !!row.is_enabled,
15
+ tags: JSON.parse(row.tags),
16
+ version: row.version,
17
+ createdAt: row.created_at,
18
+ updatedAt: row.updated_at
19
+ }
20
+ }
21
+
22
+ export function createTool(db, tool) {
23
+ const id = tool.id || randomUUID()
24
+ const now = new Date().toISOString()
25
+
26
+ db.prepare(
27
+ `INSERT INTO tools (id, name, description, parameters, source_type, source_code,
28
+ webhook_url, webhook_headers, is_enabled, tags, version, created_at, updated_at)
29
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
30
+ ).run(
31
+ id,
32
+ String(tool.name),
33
+ String(tool.description || ''),
34
+ JSON.stringify(tool.parameters || { type: 'object', properties: {} }),
35
+ String(tool.sourceType || 'js_function'),
36
+ tool.sourceCode || null,
37
+ tool.webhookUrl || null,
38
+ JSON.stringify(tool.webhookHeaders || {}),
39
+ tool.isEnabled === false ? 0 : 1,
40
+ JSON.stringify(tool.tags || []),
41
+ tool.version || 1,
42
+ now,
43
+ now
44
+ )
45
+
46
+ return getTool(db, id)
47
+ }
48
+
49
+ export function updateTool(db, id, updates) {
50
+ const existing = getTool(db, id)
51
+ if (!existing) return null
52
+
53
+ const now = new Date().toISOString()
54
+ const merged = { ...existing, ...updates, updatedAt: now }
55
+
56
+ db.prepare(
57
+ `UPDATE tools SET name = ?, description = ?, parameters = ?, source_type = ?,
58
+ source_code = ?, webhook_url = ?, webhook_headers = ?, is_enabled = ?,
59
+ tags = ?, version = ?, updated_at = ?
60
+ WHERE id = ?`
61
+ ).run(
62
+ String(merged.name),
63
+ String(merged.description || ''),
64
+ JSON.stringify(merged.parameters || { type: 'object', properties: {} }),
65
+ String(merged.sourceType || 'js_function'),
66
+ merged.sourceCode || null,
67
+ merged.webhookUrl || null,
68
+ JSON.stringify(merged.webhookHeaders || {}),
69
+ merged.isEnabled === false ? 0 : 1,
70
+ JSON.stringify(merged.tags || []),
71
+ merged.version || 1,
72
+ now,
73
+ id
74
+ )
75
+
76
+ return getTool(db, id)
77
+ }
78
+
79
+ export function deleteTool(db, id) {
80
+ return db.prepare(`DELETE FROM tools WHERE id = ?`).run(id)
81
+ }
82
+
83
+ export function getTool(db, id) {
84
+ return mapTool(db.prepare(`SELECT * FROM tools WHERE id = ?`).get(id))
85
+ }
86
+
87
+ export function getToolByName(db, name) {
88
+ return mapTool(db.prepare(`SELECT * FROM tools WHERE name = ?`).get(name))
89
+ }
90
+
91
+ export function listTools(db, enabledOnly = false) {
92
+ if (enabledOnly) {
93
+ return db.prepare(`SELECT * FROM tools WHERE is_enabled = 1`).all().map(mapTool)
94
+ }
95
+ return db.prepare(`SELECT * FROM tools ORDER BY name ASC`).all().map(mapTool)
96
+ }
97
+
98
+ export function upsertTool(db, tool) {
99
+ const existing = tool.id ? getTool(db, tool.id) : getToolByName(db, tool.name)
100
+ if (existing) {
101
+ return updateTool(db, existing.id, tool)
102
+ }
103
+ return createTool(db, tool)
104
+ }