@agentuity/local 3.0.0-alpha.0

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 (56) hide show
  1. package/dist/bun/db.d.ts +4 -0
  2. package/dist/bun/db.d.ts.map +1 -0
  3. package/dist/bun/db.js +281 -0
  4. package/dist/bun/db.js.map +1 -0
  5. package/dist/bun/email.d.ts +24 -0
  6. package/dist/bun/email.d.ts.map +1 -0
  7. package/dist/bun/email.js +58 -0
  8. package/dist/bun/email.js.map +1 -0
  9. package/dist/bun/index.d.ts +14 -0
  10. package/dist/bun/index.d.ts.map +1 -0
  11. package/dist/bun/index.js +14 -0
  12. package/dist/bun/index.js.map +1 -0
  13. package/dist/bun/kv.d.ts +17 -0
  14. package/dist/bun/kv.d.ts.map +1 -0
  15. package/dist/bun/kv.js +133 -0
  16. package/dist/bun/kv.js.map +1 -0
  17. package/dist/bun/queue.d.ts +10 -0
  18. package/dist/bun/queue.d.ts.map +1 -0
  19. package/dist/bun/queue.js +96 -0
  20. package/dist/bun/queue.js.map +1 -0
  21. package/dist/bun/stream.d.ts +12 -0
  22. package/dist/bun/stream.d.ts.map +1 -0
  23. package/dist/bun/stream.js +266 -0
  24. package/dist/bun/stream.js.map +1 -0
  25. package/dist/bun/task.d.ts +55 -0
  26. package/dist/bun/task.d.ts.map +1 -0
  27. package/dist/bun/task.js +1248 -0
  28. package/dist/bun/task.js.map +1 -0
  29. package/dist/bun/util.d.ts +18 -0
  30. package/dist/bun/util.d.ts.map +1 -0
  31. package/dist/bun/util.js +44 -0
  32. package/dist/bun/util.js.map +1 -0
  33. package/dist/bun/vector.d.ts +17 -0
  34. package/dist/bun/vector.d.ts.map +1 -0
  35. package/dist/bun/vector.js +303 -0
  36. package/dist/bun/vector.js.map +1 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +14 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/runtime.d.ts +20 -0
  42. package/dist/runtime.d.ts.map +1 -0
  43. package/dist/runtime.js +44 -0
  44. package/dist/runtime.js.map +1 -0
  45. package/package.json +42 -0
  46. package/src/bun/db.ts +353 -0
  47. package/src/bun/email.ts +91 -0
  48. package/src/bun/index.ts +14 -0
  49. package/src/bun/kv.ts +174 -0
  50. package/src/bun/queue.ts +145 -0
  51. package/src/bun/stream.ts +358 -0
  52. package/src/bun/task.ts +1711 -0
  53. package/src/bun/util.ts +55 -0
  54. package/src/bun/vector.ts +438 -0
  55. package/src/index.ts +36 -0
  56. package/src/runtime.ts +56 -0
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@agentuity/local",
3
+ "version": "3.0.0-alpha.0",
4
+ "license": "Apache-2.0",
5
+ "author": "Agentuity employees and contributors",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "src",
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
21
+ "build": "bunx tsc --build",
22
+ "typecheck": "bunx tsc --noEmit",
23
+ "prepublishOnly": "bun run clean && bun run build"
24
+ },
25
+ "dependencies": {
26
+ "@agentuity/core": "3.0.0-alpha.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "latest",
30
+ "bun-types": "latest",
31
+ "typescript": "^5.9.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "sideEffects": false,
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/agentuity/sdk.git",
40
+ "directory": "packages/local"
41
+ }
42
+ }
package/src/bun/db.ts ADDED
@@ -0,0 +1,353 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { mkdirSync, existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ let dbInstance: Database | null = null;
7
+
8
+ export function getLocalDB(): Database {
9
+ if (dbInstance) {
10
+ return dbInstance;
11
+ }
12
+
13
+ const configDir = join(homedir(), '.config', 'agentuity');
14
+
15
+ if (!existsSync(configDir)) {
16
+ mkdirSync(configDir, { recursive: true });
17
+ }
18
+
19
+ const dbPath = join(configDir, 'local.db');
20
+ dbInstance = new Database(dbPath);
21
+
22
+ initializeTables(dbInstance);
23
+ cleanupOrphanedProjects(dbInstance);
24
+
25
+ return dbInstance;
26
+ }
27
+
28
+ function initializeTables(db: Database): void {
29
+ // KeyValue Storage table
30
+ db.run(`
31
+ CREATE TABLE IF NOT EXISTS kv_storage (
32
+ project_path TEXT NOT NULL,
33
+ name TEXT NOT NULL,
34
+ key TEXT NOT NULL,
35
+ value BLOB NOT NULL,
36
+ content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
37
+ expires_at INTEGER,
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL,
40
+ PRIMARY KEY (project_path, name, key)
41
+ )
42
+ `);
43
+
44
+ db.run(`
45
+ CREATE INDEX IF NOT EXISTS idx_kv_expires
46
+ ON kv_storage(expires_at)
47
+ WHERE expires_at IS NOT NULL
48
+ `);
49
+
50
+ // Stream Storage table
51
+ db.run(`
52
+ CREATE TABLE IF NOT EXISTS stream_storage (
53
+ project_path TEXT NOT NULL,
54
+ id TEXT PRIMARY KEY,
55
+ name TEXT NOT NULL,
56
+ metadata TEXT,
57
+ content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
58
+ data BLOB,
59
+ size_bytes INTEGER NOT NULL DEFAULT 0,
60
+ created_at INTEGER NOT NULL
61
+ )
62
+ `);
63
+
64
+ db.run(`
65
+ CREATE INDEX IF NOT EXISTS idx_stream_name
66
+ ON stream_storage(project_path, name)
67
+ `);
68
+
69
+ db.run(`
70
+ CREATE INDEX IF NOT EXISTS idx_stream_metadata
71
+ ON stream_storage(metadata)
72
+ `);
73
+
74
+ // Vector Storage table
75
+ db.run(`
76
+ CREATE TABLE IF NOT EXISTS vector_storage (
77
+ project_path TEXT NOT NULL,
78
+ name TEXT NOT NULL,
79
+ id TEXT PRIMARY KEY,
80
+ key TEXT NOT NULL,
81
+ embedding TEXT NOT NULL,
82
+ document TEXT,
83
+ metadata TEXT,
84
+ created_at INTEGER NOT NULL,
85
+ updated_at INTEGER NOT NULL,
86
+ UNIQUE (project_path, name, key)
87
+ )
88
+ `);
89
+
90
+ db.run(`
91
+ CREATE INDEX IF NOT EXISTS idx_vector_lookup
92
+ ON vector_storage(project_path, name, key)
93
+ `);
94
+
95
+ db.run(`
96
+ CREATE INDEX IF NOT EXISTS idx_vector_name
97
+ ON vector_storage(project_path, name)
98
+ `);
99
+
100
+ // Task Storage table
101
+ db.run(`
102
+ CREATE TABLE IF NOT EXISTS task_storage (
103
+ project_path TEXT NOT NULL,
104
+ id TEXT NOT NULL,
105
+ title TEXT NOT NULL,
106
+ description TEXT,
107
+ metadata TEXT,
108
+ priority TEXT NOT NULL DEFAULT 'none',
109
+ parent_id TEXT,
110
+ type TEXT NOT NULL,
111
+ status TEXT NOT NULL DEFAULT 'open',
112
+ open_date TEXT,
113
+ in_progress_date TEXT,
114
+ closed_date TEXT,
115
+ created_id TEXT NOT NULL,
116
+ assigned_id TEXT,
117
+ closed_id TEXT,
118
+ deleted INTEGER NOT NULL DEFAULT 0,
119
+ created_at INTEGER NOT NULL,
120
+ updated_at INTEGER NOT NULL,
121
+ PRIMARY KEY (project_path, id)
122
+ )
123
+ `);
124
+
125
+ // Migration: add deleted column for existing databases
126
+ try {
127
+ db.run('ALTER TABLE task_storage ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0');
128
+ } catch {
129
+ // Column already exists
130
+ }
131
+
132
+ // Task Changelog table
133
+ db.run(`
134
+ CREATE TABLE IF NOT EXISTS task_changelog_storage (
135
+ project_path TEXT NOT NULL,
136
+ id TEXT NOT NULL,
137
+ task_id TEXT NOT NULL,
138
+ field TEXT NOT NULL,
139
+ old_value TEXT,
140
+ new_value TEXT,
141
+ created_at INTEGER NOT NULL,
142
+ PRIMARY KEY (project_path, id)
143
+ )
144
+ `);
145
+
146
+ db.run(`
147
+ CREATE INDEX IF NOT EXISTS idx_task_changelog_lookup
148
+ ON task_changelog_storage(project_path, task_id)
149
+ `);
150
+
151
+ // Task Comment table
152
+ db.run(`
153
+ CREATE TABLE IF NOT EXISTS task_comment_storage (
154
+ project_path TEXT NOT NULL,
155
+ id TEXT NOT NULL,
156
+ task_id TEXT NOT NULL,
157
+ user_id TEXT NOT NULL,
158
+ body TEXT NOT NULL,
159
+ created_at INTEGER NOT NULL,
160
+ updated_at INTEGER NOT NULL,
161
+ PRIMARY KEY (project_path, id)
162
+ )
163
+ `);
164
+
165
+ db.run(`
166
+ CREATE INDEX IF NOT EXISTS idx_task_comment_lookup
167
+ ON task_comment_storage(project_path, task_id)
168
+ `);
169
+
170
+ // Task Tag table
171
+ db.run(`
172
+ CREATE TABLE IF NOT EXISTS task_tag_storage (
173
+ project_path TEXT NOT NULL,
174
+ id TEXT NOT NULL,
175
+ name TEXT NOT NULL,
176
+ color TEXT,
177
+ created_at INTEGER NOT NULL,
178
+ PRIMARY KEY (project_path, id)
179
+ )
180
+ `);
181
+
182
+ // Task-Tag association table
183
+ db.run(`
184
+ CREATE TABLE IF NOT EXISTS task_tag_association_storage (
185
+ project_path TEXT NOT NULL,
186
+ task_id TEXT NOT NULL,
187
+ tag_id TEXT NOT NULL,
188
+ PRIMARY KEY (project_path, task_id, tag_id)
189
+ )
190
+ `);
191
+
192
+ db.run(`
193
+ CREATE INDEX IF NOT EXISTS idx_task_tag_assoc_task
194
+ ON task_tag_association_storage(project_path, task_id)
195
+ `);
196
+
197
+ db.run(`
198
+ CREATE INDEX IF NOT EXISTS idx_task_tag_assoc_tag
199
+ ON task_tag_association_storage(project_path, tag_id)
200
+ `);
201
+
202
+ // Task User table
203
+ db.run(`
204
+ CREATE TABLE IF NOT EXISTS task_user_storage (
205
+ project_path TEXT NOT NULL,
206
+ id TEXT NOT NULL,
207
+ name TEXT NOT NULL,
208
+ type TEXT NOT NULL DEFAULT 'human',
209
+ created_at INTEGER NOT NULL,
210
+ PRIMARY KEY (project_path, id)
211
+ )
212
+ `);
213
+
214
+ // Task Project table
215
+ db.run(`
216
+ CREATE TABLE IF NOT EXISTS task_project_storage (
217
+ project_path TEXT NOT NULL,
218
+ id TEXT NOT NULL,
219
+ name TEXT NOT NULL,
220
+ created_at INTEGER NOT NULL,
221
+ PRIMARY KEY (project_path, id)
222
+ )
223
+ `);
224
+ }
225
+
226
+ function cleanupOrphanedProjects(db: Database): void {
227
+ // Get the current project path to exclude from cleanup
228
+ const currentProjectPath = process.cwd();
229
+
230
+ // Query all tables for unique project paths
231
+ const kvPaths = db.query('SELECT DISTINCT project_path FROM kv_storage').all() as Array<{
232
+ project_path: string;
233
+ }>;
234
+ const streamPaths = db.query('SELECT DISTINCT project_path FROM stream_storage').all() as Array<{
235
+ project_path: string;
236
+ }>;
237
+ const vectorPaths = db.query('SELECT DISTINCT project_path FROM vector_storage').all() as Array<{
238
+ project_path: string;
239
+ }>;
240
+ const taskPaths = db.query('SELECT DISTINCT project_path FROM task_storage').all() as Array<{
241
+ project_path: string;
242
+ }>;
243
+ const taskChangelogPaths = db
244
+ .query('SELECT DISTINCT project_path FROM task_changelog_storage')
245
+ .all() as Array<{
246
+ project_path: string;
247
+ }>;
248
+ const taskCommentPaths = db
249
+ .query('SELECT DISTINCT project_path FROM task_comment_storage')
250
+ .all() as Array<{
251
+ project_path: string;
252
+ }>;
253
+ const taskTagPaths = db
254
+ .query('SELECT DISTINCT project_path FROM task_tag_storage')
255
+ .all() as Array<{
256
+ project_path: string;
257
+ }>;
258
+ const taskTagAssocPaths = db
259
+ .query('SELECT DISTINCT project_path FROM task_tag_association_storage')
260
+ .all() as Array<{
261
+ project_path: string;
262
+ }>;
263
+ const taskUserPaths = db
264
+ .query('SELECT DISTINCT project_path FROM task_user_storage')
265
+ .all() as Array<{
266
+ project_path: string;
267
+ }>;
268
+ const taskProjectPaths = db
269
+ .query('SELECT DISTINCT project_path FROM task_project_storage')
270
+ .all() as Array<{
271
+ project_path: string;
272
+ }>;
273
+
274
+ // Combine and deduplicate all project paths
275
+ const allPaths = new Set<string>();
276
+ [
277
+ ...kvPaths,
278
+ ...streamPaths,
279
+ ...vectorPaths,
280
+ ...taskPaths,
281
+ ...taskChangelogPaths,
282
+ ...taskCommentPaths,
283
+ ...taskTagPaths,
284
+ ...taskTagAssocPaths,
285
+ ...taskUserPaths,
286
+ ...taskProjectPaths,
287
+ ].forEach((row) => {
288
+ allPaths.add(row.project_path);
289
+ });
290
+
291
+ // Check which paths no longer exist and are not the current project
292
+ const pathsToDelete: string[] = [];
293
+ for (const path of allPaths) {
294
+ if (path !== currentProjectPath && !existsSync(path)) {
295
+ pathsToDelete.push(path);
296
+ }
297
+ }
298
+
299
+ // Delete data for removed projects
300
+ if (pathsToDelete.length > 0) {
301
+ const placeholders = pathsToDelete.map(() => '?').join(', ');
302
+
303
+ // Delete from all tables
304
+ const deleteKv = db.prepare(`DELETE FROM kv_storage WHERE project_path IN (${placeholders})`);
305
+ const deleteStream = db.prepare(
306
+ `DELETE FROM stream_storage WHERE project_path IN (${placeholders})`
307
+ );
308
+ const deleteVector = db.prepare(
309
+ `DELETE FROM vector_storage WHERE project_path IN (${placeholders})`
310
+ );
311
+ const deleteTasks = db.prepare(
312
+ `DELETE FROM task_storage WHERE project_path IN (${placeholders})`
313
+ );
314
+ const deleteTaskChangelog = db.prepare(
315
+ `DELETE FROM task_changelog_storage WHERE project_path IN (${placeholders})`
316
+ );
317
+ const deleteTaskComments = db.prepare(
318
+ `DELETE FROM task_comment_storage WHERE project_path IN (${placeholders})`
319
+ );
320
+ const deleteTaskTags = db.prepare(
321
+ `DELETE FROM task_tag_storage WHERE project_path IN (${placeholders})`
322
+ );
323
+ const deleteTaskTagAssoc = db.prepare(
324
+ `DELETE FROM task_tag_association_storage WHERE project_path IN (${placeholders})`
325
+ );
326
+ const deleteTaskUsers = db.prepare(
327
+ `DELETE FROM task_user_storage WHERE project_path IN (${placeholders})`
328
+ );
329
+ const deleteTaskProjects = db.prepare(
330
+ `DELETE FROM task_project_storage WHERE project_path IN (${placeholders})`
331
+ );
332
+
333
+ deleteKv.run(...pathsToDelete);
334
+ deleteStream.run(...pathsToDelete);
335
+ deleteVector.run(...pathsToDelete);
336
+ deleteTasks.run(...pathsToDelete);
337
+ deleteTaskChangelog.run(...pathsToDelete);
338
+ deleteTaskComments.run(...pathsToDelete);
339
+ deleteTaskTags.run(...pathsToDelete);
340
+ deleteTaskTagAssoc.run(...pathsToDelete);
341
+ deleteTaskUsers.run(...pathsToDelete);
342
+ deleteTaskProjects.run(...pathsToDelete);
343
+
344
+ console.log(`[LocalDB] Cleaned up data for ${pathsToDelete.length} orphaned project(s)`);
345
+ }
346
+ }
347
+
348
+ export function closeLocalDB(): void {
349
+ if (dbInstance) {
350
+ dbInstance.close();
351
+ dbInstance = null;
352
+ }
353
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ StructuredError,
3
+ type EmailService,
4
+ type EmailAddress,
5
+ type EmailDestination,
6
+ type EmailConnectionConfig,
7
+ type EmailInbound,
8
+ type EmailOutbound,
9
+ type EmailSendParams,
10
+ type EmailActivityParams,
11
+ type EmailActivityResult,
12
+ } from '@agentuity/core';
13
+
14
+ const ERROR_MESSAGE =
15
+ 'Email service is not available in local development mode. Deploy to Agentuity Cloud to use email.';
16
+
17
+ const LocalEmailNotAvailableError = StructuredError('LocalEmailNotAvailableError', ERROR_MESSAGE);
18
+
19
+ /**
20
+ * Local development stub for the email service.
21
+ * All methods throw a descriptive error directing users to deploy to Agentuity Cloud.
22
+ */
23
+ export class LocalEmailStorage implements EmailService {
24
+ async createAddress(_localPart: string): Promise<EmailAddress> {
25
+ throw new LocalEmailNotAvailableError();
26
+ }
27
+
28
+ async listAddresses(): Promise<EmailAddress[]> {
29
+ throw new LocalEmailNotAvailableError();
30
+ }
31
+
32
+ async getAddress(_id: string): Promise<EmailAddress | null> {
33
+ throw new LocalEmailNotAvailableError();
34
+ }
35
+
36
+ async getConnectionConfig(_id: string): Promise<EmailConnectionConfig | null> {
37
+ throw new LocalEmailNotAvailableError();
38
+ }
39
+
40
+ async deleteAddress(_id: string): Promise<void> {
41
+ throw new LocalEmailNotAvailableError();
42
+ }
43
+
44
+ async createDestination(
45
+ _addressId: string,
46
+ _type: string,
47
+ _config: Record<string, unknown>
48
+ ): Promise<EmailDestination> {
49
+ throw new LocalEmailNotAvailableError();
50
+ }
51
+
52
+ async listDestinations(_addressId: string): Promise<EmailDestination[]> {
53
+ throw new LocalEmailNotAvailableError();
54
+ }
55
+
56
+ async deleteDestination(_addressId: string, _destinationId: string): Promise<void> {
57
+ throw new LocalEmailNotAvailableError();
58
+ }
59
+
60
+ async send(_params: EmailSendParams): Promise<EmailOutbound> {
61
+ throw new LocalEmailNotAvailableError();
62
+ }
63
+
64
+ async listInbound(_addressId?: string): Promise<EmailInbound[]> {
65
+ throw new LocalEmailNotAvailableError();
66
+ }
67
+
68
+ async getInbound(_id: string): Promise<EmailInbound | null> {
69
+ throw new LocalEmailNotAvailableError();
70
+ }
71
+
72
+ async deleteInbound(_id: string): Promise<void> {
73
+ throw new LocalEmailNotAvailableError();
74
+ }
75
+
76
+ async listOutbound(_addressId?: string): Promise<EmailOutbound[]> {
77
+ throw new LocalEmailNotAvailableError();
78
+ }
79
+
80
+ async getOutbound(_id: string): Promise<EmailOutbound | null> {
81
+ throw new LocalEmailNotAvailableError();
82
+ }
83
+
84
+ async deleteOutbound(_id: string): Promise<void> {
85
+ throw new LocalEmailNotAvailableError();
86
+ }
87
+
88
+ async getActivity(_params?: EmailActivityParams): Promise<EmailActivityResult> {
89
+ throw new LocalEmailNotAvailableError();
90
+ }
91
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bun-specific local storage implementations
3
+ *
4
+ * Uses Bun's built-in SQLite for local development storage.
5
+ */
6
+
7
+ export { getLocalDB, closeLocalDB } from './db';
8
+ export { LocalKeyValueStorage } from './kv';
9
+ export { LocalStreamStorage } from './stream';
10
+ export { LocalVectorStorage } from './vector';
11
+ export { LocalQueueStorage } from './queue';
12
+ export { LocalEmailStorage } from './email';
13
+ export { LocalTaskStorage } from './task';
14
+ export { now, normalizeProjectPath } from './util';
package/src/bun/kv.ts ADDED
@@ -0,0 +1,174 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type {
3
+ KeyValueStorage,
4
+ DataResult,
5
+ DataResultNotFound,
6
+ KeyValueStorageSetParams,
7
+ KeyValueStats,
8
+ KeyValueItemWithMetadata,
9
+ CreateNamespaceParams,
10
+ GetAllStatsParams,
11
+ KeyValueStatsPaginated,
12
+ } from '@agentuity/core';
13
+ import { now } from './util';
14
+
15
+ export class LocalKeyValueStorage implements KeyValueStorage {
16
+ #db: Database;
17
+ #projectPath: string;
18
+
19
+ constructor(db: Database, projectPath: string) {
20
+ this.#db = db;
21
+ this.#projectPath = projectPath;
22
+ }
23
+
24
+ async get<T>(name: string, key: string): Promise<DataResult<T>> {
25
+ const query = this.#db.query(`
26
+ SELECT value, content_type, expires_at
27
+ FROM kv_storage
28
+ WHERE project_path = ? AND name = ? AND key = ?
29
+ `);
30
+
31
+ const row = query.get(this.#projectPath, name, key) as {
32
+ value: Buffer;
33
+ content_type: string;
34
+ expires_at: number | null;
35
+ } | null;
36
+
37
+ if (!row) {
38
+ return { exists: false } as DataResultNotFound;
39
+ }
40
+
41
+ // Check expiration
42
+ if (row.expires_at && row.expires_at < now()) {
43
+ // Delete expired row
44
+ await this.delete(name, key);
45
+ return { exists: false } as DataResultNotFound;
46
+ }
47
+
48
+ // Deserialize based on content type
49
+ let data: T;
50
+ if (row.content_type === 'application/json') {
51
+ try {
52
+ const text = row.value.toString('utf-8');
53
+ data = JSON.parse(text);
54
+ } catch {
55
+ // If JSON parse fails, return the raw buffer as Uint8Array
56
+ data = new Uint8Array(row.value) as T;
57
+ }
58
+ } else if (row.content_type.startsWith('text/')) {
59
+ data = row.value.toString('utf-8') as T;
60
+ } else {
61
+ data = new Uint8Array(row.value) as T;
62
+ }
63
+
64
+ return {
65
+ data,
66
+ contentType: row.content_type,
67
+ exists: true,
68
+ // Include expiresAt if set (convert from Unix timestamp to ISO string)
69
+ ...(row.expires_at && { expiresAt: new Date(row.expires_at).toISOString() }),
70
+ };
71
+ }
72
+
73
+ async set<T = unknown>(
74
+ name: string,
75
+ key: string,
76
+ value: T,
77
+ params?: KeyValueStorageSetParams
78
+ ): Promise<void> {
79
+ // Serialize value
80
+ let buffer: Buffer;
81
+ let contentType = params?.contentType || 'application/octet-stream';
82
+
83
+ if (typeof value === 'string') {
84
+ buffer = Buffer.from(value, 'utf-8');
85
+ if (!params?.contentType) {
86
+ contentType = 'text/plain';
87
+ }
88
+ } else if (value instanceof Uint8Array) {
89
+ buffer = Buffer.from(value);
90
+ } else if (value instanceof ArrayBuffer) {
91
+ buffer = Buffer.from(new Uint8Array(value));
92
+ } else if (
93
+ typeof value === 'number' ||
94
+ typeof value === 'boolean' ||
95
+ typeof value === 'object'
96
+ ) {
97
+ // Use JSON for numbers, booleans, and objects to preserve type on round-trip
98
+ buffer = Buffer.from(JSON.stringify(value), 'utf-8');
99
+ contentType = 'application/json';
100
+ } else {
101
+ // Fallback for other types
102
+ buffer = Buffer.from(String(value), 'utf-8');
103
+ }
104
+
105
+ // Calculate expiration
106
+ // TTL handling: null or 0 = no expiration, positive = TTL in seconds
107
+ // undefined = use default (7 days for consistency with cloud namespace default)
108
+ let expiresAt: number | null = null;
109
+ if (params?.ttl === undefined) {
110
+ // Default to 7 days (matching cloud namespace default behavior)
111
+ expiresAt = now() + 7 * 24 * 60 * 60 * 1000;
112
+ } else if (params.ttl !== null && params.ttl > 0) {
113
+ expiresAt = now() + params.ttl * 1000;
114
+ }
115
+ // else: ttl is null or 0, so expiresAt stays null (no expiration)
116
+ const timestamp = now();
117
+
118
+ // UPSERT
119
+ const stmt = this.#db.prepare(`
120
+ INSERT INTO kv_storage (project_path, name, key, value, content_type, expires_at, created_at, updated_at)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
122
+ ON CONFLICT(project_path, name, key)
123
+ DO UPDATE SET
124
+ value = excluded.value,
125
+ content_type = excluded.content_type,
126
+ expires_at = excluded.expires_at,
127
+ updated_at = excluded.updated_at
128
+ `);
129
+
130
+ stmt.run(this.#projectPath, name, key, buffer, contentType, expiresAt, timestamp, timestamp);
131
+ }
132
+
133
+ async delete(name: string, key: string): Promise<void> {
134
+ const stmt = this.#db.prepare(`
135
+ DELETE FROM kv_storage
136
+ WHERE project_path = ? AND name = ? AND key = ?
137
+ `);
138
+
139
+ stmt.run(this.#projectPath, name, key);
140
+ }
141
+
142
+ async getStats(_name: string): Promise<KeyValueStats> {
143
+ throw new Error('getStats not implemented for local storage');
144
+ }
145
+
146
+ async getAllStats(
147
+ _params?: GetAllStatsParams
148
+ ): Promise<Record<string, KeyValueStats> | KeyValueStatsPaginated> {
149
+ throw new Error('getAllStats not implemented for local storage');
150
+ }
151
+
152
+ async getNamespaces(): Promise<string[]> {
153
+ throw new Error('getNamespaces not implemented for local storage');
154
+ }
155
+
156
+ async search<T = unknown>(
157
+ _name: string,
158
+ _keyword: string
159
+ ): Promise<Record<string, KeyValueItemWithMetadata<T>>> {
160
+ throw new Error('search not implemented for local storage');
161
+ }
162
+
163
+ async getKeys(_name: string): Promise<string[]> {
164
+ throw new Error('getKeys not implemented for local storage');
165
+ }
166
+
167
+ async deleteNamespace(_name: string): Promise<void> {
168
+ throw new Error('deleteNamespace not implemented for local storage');
169
+ }
170
+
171
+ async createNamespace(_name: string, _params?: CreateNamespaceParams): Promise<void> {
172
+ throw new Error('createNamespace not implemented for local storage');
173
+ }
174
+ }