@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 +109 -0
- package/package.json +10 -4
- package/src/db.js +6 -107
- package/src/index.js +9 -3
- package/src/migrations/001_initial_schema.js +217 -0
- package/src/migrations/002_task_activity_types.js +45 -0
- package/src/migrations/runner.js +31 -0
- package/src/repos/mcp-servers.js +81 -0
- package/src/repos/messages.js +219 -0
- package/src/repos/patterns.js +54 -0
- package/src/repos/schedules.js +58 -0
- package/src/repos/settings.js +38 -0
- package/src/repos/tasks.js +195 -0
- package/src/repos/tool-secrets.js +26 -0
- package/src/repos/tools.js +104 -0
- package/src/repos/vectors.js +105 -0
- package/src/config.js +0 -62
- package/src/messages.js +0 -213
- package/src/tasks.js +0 -219
|
@@ -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
|
+
}
|