@vox-ai-app/storage 1.0.1

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/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@vox-ai-app/storage",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Local message and config persistence for Vox",
7
+ "main": "./src/index.js",
8
+ "private": false,
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./db": "./src/db.js",
12
+ "./messages": "./src/messages.js",
13
+ "./config": "./src/config.js",
14
+ "./tasks": "./src/tasks.js"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/vox-ai-app/vox.git"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "LICENSE",
27
+ "README.md"
28
+ ],
29
+ "dependencies": {
30
+ "better-sqlite3": "^12.0.0"
31
+ }
32
+ }
package/src/config.js ADDED
@@ -0,0 +1,62 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ function resolveConfigPath(configPath) {
5
+ const normalized = String(configPath || '').trim()
6
+ if (!normalized) {
7
+ throw new Error('A config path is required.')
8
+ }
9
+ return path.resolve(normalized)
10
+ }
11
+
12
+ function readConfigFile(configPath) {
13
+ const resolvedPath = resolveConfigPath(configPath)
14
+ if (!existsSync(resolvedPath)) {
15
+ return {}
16
+ }
17
+
18
+ try {
19
+ const raw = readFileSync(resolvedPath, 'utf8')
20
+ if (!raw.trim()) return {}
21
+ const parsed = JSON.parse(raw)
22
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
23
+ return {}
24
+ }
25
+ return parsed
26
+ } catch {
27
+ return {}
28
+ }
29
+ }
30
+
31
+ function writeConfigFile(configPath, value) {
32
+ const resolvedPath = resolveConfigPath(configPath)
33
+ mkdirSync(path.dirname(resolvedPath), { recursive: true })
34
+ const tempPath = `${resolvedPath}.tmp`
35
+ writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
36
+ renameSync(tempPath, resolvedPath)
37
+ }
38
+
39
+ export function configGet(configPath, key) {
40
+ const config = readConfigFile(configPath)
41
+ return config[String(key)]
42
+ }
43
+
44
+ export function configSet(configPath, key, value) {
45
+ const config = readConfigFile(configPath)
46
+ config[String(key)] = value
47
+ writeConfigFile(configPath, config)
48
+ return value
49
+ }
50
+
51
+ export function configDelete(configPath, key) {
52
+ const config = readConfigFile(configPath)
53
+ const normalizedKey = String(key)
54
+ const existed = Object.prototype.hasOwnProperty.call(config, normalizedKey)
55
+ delete config[normalizedKey]
56
+ writeConfigFile(configPath, config)
57
+ return existed
58
+ }
59
+
60
+ export function configGetAll(configPath) {
61
+ return { ...readConfigFile(configPath) }
62
+ }
package/src/db.js ADDED
@@ -0,0 +1,145 @@
1
+ import Database from 'better-sqlite3'
2
+ import { mkdirSync } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ const dbs = new Map()
6
+
7
+ function resolveDbPath(dbPath) {
8
+ const normalized = String(dbPath || '').trim()
9
+ if (!normalized) {
10
+ throw new Error('A database path is required.')
11
+ }
12
+ return path.resolve(normalized)
13
+ }
14
+
15
+ function prepareDb(db) {
16
+ db.pragma('journal_mode = WAL')
17
+ 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
+ }
124
+ }
125
+
126
+ export function openDb(dbPath) {
127
+ const resolvedPath = resolveDbPath(dbPath)
128
+ const existing = dbs.get(resolvedPath)
129
+ if (existing) return existing
130
+
131
+ mkdirSync(path.dirname(resolvedPath), { recursive: true })
132
+
133
+ const db = new Database(resolvedPath)
134
+ prepareDb(db)
135
+ dbs.set(resolvedPath, db)
136
+ return db
137
+ }
138
+
139
+ export function closeDb(dbPath) {
140
+ const resolvedPath = resolveDbPath(dbPath)
141
+ const db = dbs.get(resolvedPath)
142
+ if (!db) return
143
+ db.close()
144
+ dbs.delete(resolvedPath)
145
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './db.js'
2
+ export * from './messages.js'
3
+ export * from './config.js'
4
+ export * from './tasks.js'
@@ -0,0 +1,213 @@
1
+ export const DEFAULT_CONVERSATION_ID = 'main'
2
+
3
+ function getConversationId(conversationId) {
4
+ const normalized = String(conversationId || '').trim()
5
+ return normalized || DEFAULT_CONVERSATION_ID
6
+ }
7
+
8
+ function normalizeLimit(limit) {
9
+ const parsed = Number.parseInt(limit, 10)
10
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null
11
+ }
12
+
13
+ function normalizeBeforeId(beforeId) {
14
+ const parsed = Number.parseInt(beforeId, 10)
15
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null
16
+ }
17
+
18
+ function mapRow(row) {
19
+ if (!row) return null
20
+ return {
21
+ id: row.id,
22
+ conversationId: row.conversation_id,
23
+ role: row.role,
24
+ content: row.content,
25
+ createdAt: row.created_at
26
+ }
27
+ }
28
+
29
+ export function ensureConversation(db, conversationId = DEFAULT_CONVERSATION_ID) {
30
+ const id = getConversationId(conversationId)
31
+ const now = new Date().toISOString()
32
+
33
+ db.prepare(
34
+ `
35
+ INSERT INTO conversations (id, created_at, updated_at)
36
+ VALUES (?, ?, ?)
37
+ ON CONFLICT(id) DO NOTHING
38
+ `
39
+ ).run(id, now, now)
40
+
41
+ return db
42
+ .prepare(
43
+ `
44
+ SELECT id, created_at, updated_at
45
+ FROM conversations
46
+ WHERE id = ?
47
+ `
48
+ )
49
+ .get(id)
50
+ }
51
+
52
+ export function touchConversation(db, conversationId = DEFAULT_CONVERSATION_ID) {
53
+ const id = getConversationId(conversationId)
54
+ const now = new Date().toISOString()
55
+
56
+ db.prepare(
57
+ `
58
+ INSERT INTO conversations (id, created_at, updated_at)
59
+ VALUES (?, ?, ?)
60
+ ON CONFLICT(id) DO UPDATE SET updated_at = excluded.updated_at
61
+ `
62
+ ).run(id, now, now)
63
+
64
+ return db
65
+ .prepare(
66
+ `
67
+ SELECT id, created_at, updated_at
68
+ FROM conversations
69
+ WHERE id = ?
70
+ `
71
+ )
72
+ .get(id)
73
+ }
74
+
75
+ export function appendMessage(db, role, content, conversationId = DEFAULT_CONVERSATION_ID) {
76
+ const id = getConversationId(conversationId)
77
+ const now = new Date().toISOString()
78
+ const normalizedRole = String(role || '').trim() || 'user'
79
+ const normalizedContent = String(content ?? '')
80
+
81
+ touchConversation(db, id)
82
+
83
+ const result = db
84
+ .prepare(
85
+ `
86
+ INSERT INTO messages (conversation_id, role, content, created_at)
87
+ VALUES (?, ?, ?, ?)
88
+ `
89
+ )
90
+ .run(id, normalizedRole, normalizedContent, now)
91
+
92
+ return mapRow(
93
+ db
94
+ .prepare(
95
+ `
96
+ SELECT id, conversation_id, role, content, created_at
97
+ FROM messages
98
+ WHERE id = ?
99
+ `
100
+ )
101
+ .get(result.lastInsertRowid)
102
+ )
103
+ }
104
+
105
+ export function getMessages(db, conversationId = DEFAULT_CONVERSATION_ID, limit) {
106
+ const id = getConversationId(conversationId)
107
+ const normalizedLimit = normalizeLimit(limit)
108
+
109
+ if (!normalizedLimit) {
110
+ return db
111
+ .prepare(
112
+ `
113
+ SELECT id, conversation_id, role, content, created_at
114
+ FROM messages
115
+ WHERE conversation_id = ?
116
+ ORDER BY id ASC
117
+ `
118
+ )
119
+ .all(id)
120
+ .map(mapRow)
121
+ }
122
+
123
+ return db
124
+ .prepare(
125
+ `
126
+ SELECT id, conversation_id, role, content, created_at
127
+ FROM (
128
+ SELECT id, conversation_id, role, content, created_at
129
+ FROM messages
130
+ WHERE conversation_id = ?
131
+ ORDER BY id DESC
132
+ LIMIT ?
133
+ )
134
+ ORDER BY id ASC
135
+ `
136
+ )
137
+ .all(id, normalizedLimit)
138
+ .map(mapRow)
139
+ }
140
+
141
+ export function getMessagesBeforeId(
142
+ db,
143
+ beforeId,
144
+ conversationId = DEFAULT_CONVERSATION_ID,
145
+ limit = 50
146
+ ) {
147
+ const id = getConversationId(conversationId)
148
+ const normalizedBeforeId = normalizeBeforeId(beforeId)
149
+ const normalizedLimit = normalizeLimit(limit) || 50
150
+
151
+ if (!normalizedBeforeId) {
152
+ return []
153
+ }
154
+
155
+ return db
156
+ .prepare(
157
+ `
158
+ SELECT id, conversation_id, role, content, created_at
159
+ FROM (
160
+ SELECT id, conversation_id, role, content, created_at
161
+ FROM messages
162
+ WHERE conversation_id = ? AND id < ?
163
+ ORDER BY id DESC
164
+ LIMIT ?
165
+ )
166
+ ORDER BY id ASC
167
+ `
168
+ )
169
+ .all(id, normalizedBeforeId, normalizedLimit)
170
+ .map(mapRow)
171
+ }
172
+
173
+ export function clearMessages(db, conversationId = DEFAULT_CONVERSATION_ID) {
174
+ const id = getConversationId(conversationId)
175
+ ensureConversation(db, id)
176
+ return db
177
+ .prepare(
178
+ `
179
+ DELETE FROM messages
180
+ WHERE conversation_id = ?
181
+ `
182
+ )
183
+ .run(id)
184
+ }
185
+
186
+ export function saveSummaryCheckpoint(
187
+ db,
188
+ summary,
189
+ checkpointId,
190
+ conversationId = DEFAULT_CONVERSATION_ID
191
+ ) {
192
+ const id = getConversationId(conversationId)
193
+ ensureConversation(db, id)
194
+ db.prepare(
195
+ `UPDATE conversations SET context_summary = ?, context_checkpoint_id = ?, updated_at = ? WHERE id = ?`
196
+ ).run(summary, checkpointId, new Date().toISOString(), id)
197
+ }
198
+
199
+ export function loadSummaryCheckpoint(db, conversationId = DEFAULT_CONVERSATION_ID) {
200
+ const id = getConversationId(conversationId)
201
+ const row = db
202
+ .prepare(`SELECT context_summary, context_checkpoint_id FROM conversations WHERE id = ?`)
203
+ .get(id)
204
+ if (!row || !row.context_summary) return null
205
+ return { summary: row.context_summary, checkpointId: row.context_checkpoint_id }
206
+ }
207
+
208
+ export function clearSummaryCheckpoint(db, conversationId = DEFAULT_CONVERSATION_ID) {
209
+ const id = getConversationId(conversationId)
210
+ db.prepare(
211
+ `UPDATE conversations SET context_summary = NULL, context_checkpoint_id = NULL, updated_at = ? WHERE id = ?`
212
+ ).run(new Date().toISOString(), id)
213
+ }
package/src/tasks.js ADDED
@@ -0,0 +1,219 @@
1
+ function parseJson(value, fallback) {
2
+ if (value === null || value === undefined || value === '') {
3
+ return fallback
4
+ }
5
+
6
+ try {
7
+ return JSON.parse(value)
8
+ } catch {
9
+ return fallback
10
+ }
11
+ }
12
+
13
+ function stringifyJson(value, fallback = null) {
14
+ if (value === null || value === undefined) {
15
+ return fallback
16
+ }
17
+
18
+ try {
19
+ return JSON.stringify(value)
20
+ } catch {
21
+ return fallback
22
+ }
23
+ }
24
+
25
+ function mapTask(row) {
26
+ if (!row) return null
27
+
28
+ return {
29
+ taskId: row.task_id,
30
+ instructions: row.instructions,
31
+ context: row.context,
32
+ status: row.status,
33
+ createdAt: row.created_at,
34
+ updatedAt: row.updated_at,
35
+ currentPlan: row.current_plan,
36
+ message: row.message,
37
+ result: row.result,
38
+ completedAt: row.completed_at,
39
+ failedAt: row.failed_at
40
+ }
41
+ }
42
+
43
+ function mapActivity(row) {
44
+ if (!row) return null
45
+
46
+ return {
47
+ id: row.id,
48
+ taskId: row.task_id,
49
+ type: row.type,
50
+ name: row.name || null,
51
+ rawResult: parseJson(row.raw_result, row.raw_result),
52
+ timestamp: row.timestamp,
53
+ data: parseJson(row.data, {})
54
+ }
55
+ }
56
+
57
+ export function upsertTask(db, task) {
58
+ const taskId = String(task?.taskId || '').trim()
59
+ if (!taskId) {
60
+ throw new Error('taskId is required.')
61
+ }
62
+
63
+ const createdAt = String(task?.createdAt || new Date().toISOString())
64
+ const updatedAt = String(task?.updatedAt || createdAt)
65
+
66
+ db.prepare(
67
+ `
68
+ INSERT INTO tasks (
69
+ task_id,
70
+ instructions,
71
+ context,
72
+ status,
73
+ created_at,
74
+ updated_at,
75
+ current_plan,
76
+ message,
77
+ result,
78
+ completed_at,
79
+ failed_at
80
+ )
81
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
82
+ ON CONFLICT(task_id) DO UPDATE SET
83
+ instructions = excluded.instructions,
84
+ context = excluded.context,
85
+ status = excluded.status,
86
+ updated_at = excluded.updated_at,
87
+ current_plan = excluded.current_plan,
88
+ message = excluded.message,
89
+ result = excluded.result,
90
+ completed_at = excluded.completed_at,
91
+ failed_at = excluded.failed_at
92
+ `
93
+ ).run(
94
+ taskId,
95
+ String(task?.instructions || ''),
96
+ String(task?.context || ''),
97
+ String(task?.status || 'queued'),
98
+ createdAt,
99
+ updatedAt,
100
+ String(task?.currentPlan || ''),
101
+ String(task?.message || ''),
102
+ task?.result === null || task?.result === undefined ? null : String(task.result),
103
+ String(task?.completedAt || ''),
104
+ String(task?.failedAt || '')
105
+ )
106
+
107
+ return getTask(db, taskId)
108
+ }
109
+
110
+ export function getTask(db, taskId) {
111
+ return mapTask(
112
+ db
113
+ .prepare(
114
+ `
115
+ SELECT
116
+ task_id,
117
+ instructions,
118
+ context,
119
+ status,
120
+ created_at,
121
+ updated_at,
122
+ current_plan,
123
+ message,
124
+ result,
125
+ completed_at,
126
+ failed_at
127
+ FROM tasks
128
+ WHERE task_id = ?
129
+ `
130
+ )
131
+ .get(String(taskId || '').trim())
132
+ )
133
+ }
134
+
135
+ export function loadTasks(db) {
136
+ return db
137
+ .prepare(
138
+ `
139
+ SELECT
140
+ task_id,
141
+ instructions,
142
+ context,
143
+ status,
144
+ created_at,
145
+ updated_at,
146
+ current_plan,
147
+ message,
148
+ result,
149
+ completed_at,
150
+ failed_at
151
+ FROM tasks
152
+ ORDER BY created_at DESC, task_id DESC
153
+ `
154
+ )
155
+ .all()
156
+ .map(mapTask)
157
+ }
158
+
159
+ export function appendTaskActivity(db, activity) {
160
+ const id = String(activity?.id || '').trim()
161
+ const taskId = String(activity?.taskId || '').trim()
162
+ if (!id || !taskId) {
163
+ throw new Error('Task activity requires id and taskId.')
164
+ }
165
+
166
+ db.prepare(
167
+ `
168
+ INSERT INTO task_activity (id, task_id, type, name, raw_result, timestamp, data)
169
+ VALUES (?, ?, ?, ?, ?, ?, ?)
170
+ ON CONFLICT(id) DO UPDATE SET
171
+ task_id = excluded.task_id,
172
+ type = excluded.type,
173
+ name = excluded.name,
174
+ raw_result = excluded.raw_result,
175
+ timestamp = excluded.timestamp,
176
+ data = excluded.data
177
+ `
178
+ ).run(
179
+ id,
180
+ taskId,
181
+ String(activity?.type || ''),
182
+ activity?.name ? String(activity.name) : null,
183
+ stringifyJson(
184
+ activity?.rawResult,
185
+ activity?.rawResult === undefined ? null : String(activity.rawResult)
186
+ ),
187
+ String(activity?.timestamp || new Date().toISOString()),
188
+ stringifyJson(activity?.data || {}, '{}')
189
+ )
190
+
191
+ return id
192
+ }
193
+
194
+ export function loadTaskActivity(db, taskId) {
195
+ return db
196
+ .prepare(
197
+ `
198
+ SELECT id, task_id, type, name, raw_result, timestamp, data
199
+ FROM task_activity
200
+ WHERE task_id = ?
201
+ ORDER BY timestamp ASC, id ASC
202
+ `
203
+ )
204
+ .all(String(taskId || '').trim())
205
+ .map(mapActivity)
206
+ }
207
+
208
+ export function loadAllTaskActivity(db) {
209
+ return db
210
+ .prepare(
211
+ `
212
+ SELECT id, task_id, type, name, raw_result, timestamp, data
213
+ FROM task_activity
214
+ ORDER BY timestamp ASC, id ASC
215
+ `
216
+ )
217
+ .all()
218
+ .map(mapActivity)
219
+ }