@agentuity/runtime 0.0.43 → 0.0.44

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 (130) hide show
  1. package/AGENTS.md +11 -9
  2. package/README.md +4 -4
  3. package/dist/_context.d.ts +12 -4
  4. package/dist/_context.d.ts.map +1 -1
  5. package/dist/_server.d.ts +7 -4
  6. package/dist/_server.d.ts.map +1 -1
  7. package/dist/_services.d.ts +13 -2
  8. package/dist/_services.d.ts.map +1 -1
  9. package/dist/_util.d.ts +1 -1
  10. package/dist/_util.d.ts.map +1 -1
  11. package/dist/_waituntil.d.ts +1 -3
  12. package/dist/_waituntil.d.ts.map +1 -1
  13. package/dist/agent.d.ts +41 -14
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/app.d.ts +90 -8
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/eval.d.ts +79 -0
  18. package/dist/eval.d.ts.map +1 -0
  19. package/dist/index.d.ts +6 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/io/email.d.ts +77 -0
  22. package/dist/io/email.d.ts.map +1 -0
  23. package/dist/logger/console.d.ts +7 -1
  24. package/dist/logger/console.d.ts.map +1 -1
  25. package/dist/logger/user.d.ts.map +1 -1
  26. package/dist/otel/config.d.ts +3 -1
  27. package/dist/otel/config.d.ts.map +1 -1
  28. package/dist/otel/console.d.ts +2 -1
  29. package/dist/otel/console.d.ts.map +1 -1
  30. package/dist/otel/exporters/index.d.ts +4 -0
  31. package/dist/otel/exporters/index.d.ts.map +1 -0
  32. package/dist/otel/exporters/jsonl-log-exporter.d.ts +36 -0
  33. package/dist/otel/exporters/jsonl-log-exporter.d.ts.map +1 -0
  34. package/dist/otel/exporters/jsonl-metric-exporter.d.ts +40 -0
  35. package/dist/otel/exporters/jsonl-metric-exporter.d.ts.map +1 -0
  36. package/dist/otel/exporters/jsonl-trace-exporter.d.ts +36 -0
  37. package/dist/otel/exporters/jsonl-trace-exporter.d.ts.map +1 -0
  38. package/dist/otel/http.d.ts.map +1 -1
  39. package/dist/otel/logger.d.ts +8 -6
  40. package/dist/otel/logger.d.ts.map +1 -1
  41. package/dist/otel/otel.d.ts +8 -2
  42. package/dist/otel/otel.d.ts.map +1 -1
  43. package/dist/router.d.ts +4 -1
  44. package/dist/router.d.ts.map +1 -1
  45. package/dist/services/evalrun/composite.d.ts +21 -0
  46. package/dist/services/evalrun/composite.d.ts.map +1 -0
  47. package/dist/services/evalrun/http.d.ts +24 -0
  48. package/dist/services/evalrun/http.d.ts.map +1 -0
  49. package/dist/services/evalrun/index.d.ts +5 -0
  50. package/dist/services/evalrun/index.d.ts.map +1 -0
  51. package/dist/services/evalrun/json.d.ts +21 -0
  52. package/dist/services/evalrun/json.d.ts.map +1 -0
  53. package/dist/services/evalrun/local.d.ts +19 -0
  54. package/dist/services/evalrun/local.d.ts.map +1 -0
  55. package/dist/services/local/_db.d.ts +4 -0
  56. package/dist/services/local/_db.d.ts.map +1 -0
  57. package/dist/services/local/_router.d.ts +3 -0
  58. package/dist/services/local/_router.d.ts.map +1 -0
  59. package/dist/services/local/_util.d.ts +18 -0
  60. package/dist/services/local/_util.d.ts.map +1 -0
  61. package/dist/services/local/index.d.ts +8 -0
  62. package/dist/services/local/index.d.ts.map +1 -0
  63. package/dist/services/local/keyvalue.d.ts +10 -0
  64. package/dist/services/local/keyvalue.d.ts.map +1 -0
  65. package/dist/services/local/objectstore.d.ts +11 -0
  66. package/dist/services/local/objectstore.d.ts.map +1 -0
  67. package/dist/services/local/stream.d.ts +10 -0
  68. package/dist/services/local/stream.d.ts.map +1 -0
  69. package/dist/services/local/vector.d.ts +13 -0
  70. package/dist/services/local/vector.d.ts.map +1 -0
  71. package/dist/services/session/composite.d.ts +21 -0
  72. package/dist/services/session/composite.d.ts.map +1 -0
  73. package/dist/services/session/http.d.ts +23 -0
  74. package/dist/services/session/http.d.ts.map +1 -0
  75. package/dist/services/session/index.d.ts +5 -0
  76. package/dist/services/session/index.d.ts.map +1 -0
  77. package/dist/services/session/json.d.ts +22 -0
  78. package/dist/services/session/json.d.ts.map +1 -0
  79. package/dist/services/session/local.d.ts +19 -0
  80. package/dist/services/session/local.d.ts.map +1 -0
  81. package/dist/session.d.ts +70 -0
  82. package/dist/session.d.ts.map +1 -0
  83. package/package.json +10 -6
  84. package/src/_config.ts +1 -1
  85. package/src/_context.ts +19 -16
  86. package/src/_server.ts +284 -42
  87. package/src/_services.ts +147 -34
  88. package/src/_util.ts +2 -3
  89. package/src/_waituntil.ts +5 -153
  90. package/src/agent.ts +667 -65
  91. package/src/app.ts +159 -13
  92. package/src/eval.ts +95 -0
  93. package/src/index.ts +6 -1
  94. package/src/io/email.ts +173 -0
  95. package/src/logger/console.ts +196 -17
  96. package/src/logger/user.ts +7 -3
  97. package/src/otel/config.ts +7 -44
  98. package/src/otel/console.ts +8 -4
  99. package/src/otel/exporters/README.md +217 -0
  100. package/src/otel/exporters/index.ts +3 -0
  101. package/src/otel/exporters/jsonl-log-exporter.ts +113 -0
  102. package/src/otel/exporters/jsonl-metric-exporter.ts +120 -0
  103. package/src/otel/exporters/jsonl-trace-exporter.ts +121 -0
  104. package/src/otel/http.ts +3 -1
  105. package/src/otel/logger.ts +87 -37
  106. package/src/otel/otel.ts +43 -22
  107. package/src/router.ts +44 -4
  108. package/src/services/evalrun/composite.ts +34 -0
  109. package/src/services/evalrun/http.ts +112 -0
  110. package/src/services/evalrun/index.ts +4 -0
  111. package/src/services/evalrun/json.ts +46 -0
  112. package/src/services/evalrun/local.ts +28 -0
  113. package/src/services/local/README.md +1576 -0
  114. package/src/services/local/_db.ts +182 -0
  115. package/src/services/local/_router.ts +86 -0
  116. package/src/services/local/_util.ts +49 -0
  117. package/src/services/local/index.ts +7 -0
  118. package/src/services/local/keyvalue.ts +118 -0
  119. package/src/services/local/objectstore.ts +152 -0
  120. package/src/services/local/stream.ts +296 -0
  121. package/src/services/local/vector.ts +264 -0
  122. package/src/services/session/composite.ts +33 -0
  123. package/src/services/session/http.ts +64 -0
  124. package/src/services/session/index.ts +4 -0
  125. package/src/services/session/json.ts +42 -0
  126. package/src/services/session/local.ts +28 -0
  127. package/src/session.ts +284 -0
  128. package/dist/_unauthenticated.d.ts +0 -26
  129. package/dist/_unauthenticated.d.ts.map +0 -1
  130. package/src/_unauthenticated.ts +0 -126
@@ -0,0 +1,182 @@
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
+ // Object Storage table
51
+ db.run(`
52
+ CREATE TABLE IF NOT EXISTS object_storage (
53
+ project_path TEXT NOT NULL,
54
+ bucket TEXT NOT NULL,
55
+ key TEXT NOT NULL,
56
+ data BLOB NOT NULL,
57
+ content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
58
+ content_encoding TEXT,
59
+ cache_control TEXT,
60
+ content_disposition TEXT,
61
+ content_language TEXT,
62
+ metadata TEXT,
63
+ created_at INTEGER NOT NULL,
64
+ updated_at INTEGER NOT NULL,
65
+ PRIMARY KEY (project_path, bucket, key)
66
+ )
67
+ `);
68
+
69
+ // Stream Storage table
70
+ db.run(`
71
+ CREATE TABLE IF NOT EXISTS stream_storage (
72
+ project_path TEXT NOT NULL,
73
+ id TEXT PRIMARY KEY,
74
+ name TEXT NOT NULL,
75
+ metadata TEXT,
76
+ content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
77
+ data BLOB,
78
+ size_bytes INTEGER NOT NULL DEFAULT 0,
79
+ created_at INTEGER NOT NULL
80
+ )
81
+ `);
82
+
83
+ db.run(`
84
+ CREATE INDEX IF NOT EXISTS idx_stream_name
85
+ ON stream_storage(project_path, name)
86
+ `);
87
+
88
+ db.run(`
89
+ CREATE INDEX IF NOT EXISTS idx_stream_metadata
90
+ ON stream_storage(metadata)
91
+ `);
92
+
93
+ // Vector Storage table
94
+ db.run(`
95
+ CREATE TABLE IF NOT EXISTS vector_storage (
96
+ project_path TEXT NOT NULL,
97
+ name TEXT NOT NULL,
98
+ id TEXT PRIMARY KEY,
99
+ key TEXT NOT NULL,
100
+ embedding TEXT NOT NULL,
101
+ document TEXT,
102
+ metadata TEXT,
103
+ created_at INTEGER NOT NULL,
104
+ updated_at INTEGER NOT NULL,
105
+ UNIQUE (project_path, name, key)
106
+ )
107
+ `);
108
+
109
+ db.run(`
110
+ CREATE INDEX IF NOT EXISTS idx_vector_lookup
111
+ ON vector_storage(project_path, name, key)
112
+ `);
113
+
114
+ db.run(`
115
+ CREATE INDEX IF NOT EXISTS idx_vector_name
116
+ ON vector_storage(project_path, name)
117
+ `);
118
+ }
119
+
120
+ function cleanupOrphanedProjects(db: Database): void {
121
+ // Get the current project path to exclude from cleanup
122
+ const currentProjectPath = process.cwd();
123
+
124
+ // Query all tables for unique project paths
125
+ const kvPaths = db.query('SELECT DISTINCT project_path FROM kv_storage').all() as Array<{
126
+ project_path: string;
127
+ }>;
128
+ const objectPaths = db.query('SELECT DISTINCT project_path FROM object_storage').all() as Array<{
129
+ project_path: string;
130
+ }>;
131
+ const streamPaths = db.query('SELECT DISTINCT project_path FROM stream_storage').all() as Array<{
132
+ project_path: string;
133
+ }>;
134
+ const vectorPaths = db.query('SELECT DISTINCT project_path FROM vector_storage').all() as Array<{
135
+ project_path: string;
136
+ }>;
137
+
138
+ // Combine and deduplicate all project paths
139
+ const allPaths = new Set<string>();
140
+ [...kvPaths, ...objectPaths, ...streamPaths, ...vectorPaths].forEach((row) => {
141
+ allPaths.add(row.project_path);
142
+ });
143
+
144
+ // Check which paths no longer exist and are not the current project
145
+ const pathsToDelete: string[] = [];
146
+ for (const path of allPaths) {
147
+ if (path !== currentProjectPath && !existsSync(path)) {
148
+ pathsToDelete.push(path);
149
+ }
150
+ }
151
+
152
+ // Delete data for removed projects
153
+ if (pathsToDelete.length > 0) {
154
+ const placeholders = pathsToDelete.map(() => '?').join(', ');
155
+
156
+ // Delete from all tables
157
+ const deleteKv = db.prepare(`DELETE FROM kv_storage WHERE project_path IN (${placeholders})`);
158
+ const deleteObject = db.prepare(
159
+ `DELETE FROM object_storage WHERE project_path IN (${placeholders})`
160
+ );
161
+ const deleteStream = db.prepare(
162
+ `DELETE FROM stream_storage WHERE project_path IN (${placeholders})`
163
+ );
164
+ const deleteVector = db.prepare(
165
+ `DELETE FROM vector_storage WHERE project_path IN (${placeholders})`
166
+ );
167
+
168
+ deleteKv.run(...pathsToDelete);
169
+ deleteObject.run(...pathsToDelete);
170
+ deleteStream.run(...pathsToDelete);
171
+ deleteVector.run(...pathsToDelete);
172
+
173
+ console.log(`[LocalDB] Cleaned up data for ${pathsToDelete.length} orphaned project(s)`);
174
+ }
175
+ }
176
+
177
+ export function closeLocalDB(): void {
178
+ if (dbInstance) {
179
+ dbInstance.close();
180
+ dbInstance = null;
181
+ }
182
+ }
@@ -0,0 +1,86 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import { createRouter } from '../../router';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export function createLocalStorageRouter(db: Database, projectPath: string): any {
6
+ const router = createRouter();
7
+
8
+ // so we can detect if we're running in local mode easily
9
+ router.get('/_agentuity/local/health', (c) => c.text('OK'));
10
+
11
+ // Serve objects: GET /_agentuity/local/object/:bucket/:key
12
+ router.get('/_agentuity/local/object/:bucket/:key', async (c) => {
13
+ const bucket = c.req.param('bucket');
14
+ const key = c.req.param('key');
15
+
16
+ const query = db.query(`
17
+ SELECT data, content_type, content_encoding, cache_control,
18
+ content_disposition, content_language
19
+ FROM object_storage
20
+ WHERE project_path = ? AND bucket = ? AND key = ?
21
+ `);
22
+
23
+ const row = query.get(projectPath, bucket, key) as {
24
+ data: Buffer;
25
+ content_type: string;
26
+ content_encoding: string | null;
27
+ cache_control: string | null;
28
+ content_disposition: string | null;
29
+ content_language: string | null;
30
+ } | null;
31
+
32
+ if (!row) {
33
+ return c.notFound();
34
+ }
35
+
36
+ // Set headers
37
+ const headers: Record<string, string> = {
38
+ 'Content-Type': row.content_type,
39
+ };
40
+
41
+ if (row.content_encoding) {
42
+ headers['Content-Encoding'] = row.content_encoding;
43
+ }
44
+ if (row.cache_control) {
45
+ headers['Cache-Control'] = row.cache_control;
46
+ }
47
+ if (row.content_disposition) {
48
+ headers['Content-Disposition'] = row.content_disposition;
49
+ }
50
+ if (row.content_language) {
51
+ headers['Content-Language'] = row.content_language;
52
+ }
53
+
54
+ return c.body(new Uint8Array(row.data), 200, headers);
55
+ });
56
+
57
+ // Serve streams: GET /_agentuity/local/stream/:id
58
+ router.get('/_agentuity/local/stream/:id', async (c) => {
59
+ const id = c.req.param('id');
60
+
61
+ const query = db.query(`
62
+ SELECT data, content_type
63
+ FROM stream_storage
64
+ WHERE project_path = ? AND id = ?
65
+ `);
66
+
67
+ const row = query.get(projectPath, id) as {
68
+ data: Buffer | null;
69
+ content_type: string;
70
+ } | null;
71
+
72
+ if (!row) {
73
+ return c.notFound();
74
+ }
75
+
76
+ if (!row.data) {
77
+ return c.json({ error: 'Stream not finalized' }, 400);
78
+ }
79
+
80
+ return c.body(new Uint8Array(row.data), 200, {
81
+ 'Content-Type': row.content_type,
82
+ });
83
+ });
84
+
85
+ return router;
86
+ }
@@ -0,0 +1,49 @@
1
+ import { resolve } from 'node:path';
2
+
3
+ /**
4
+ * Normalize a project path to an absolute path for consistent DB keys
5
+ */
6
+ export function normalizeProjectPath(cwd: string = process.cwd()): string {
7
+ return resolve(cwd);
8
+ }
9
+
10
+ /**
11
+ * Simple character-based embedding for local vector search
12
+ * Not production-quality, but good enough for local dev/testing
13
+ */
14
+ export function simpleEmbedding(text: string, dimensions = 128): number[] {
15
+ const vec = new Array(dimensions).fill(0);
16
+ const normalized = text.toLowerCase();
17
+
18
+ for (let i = 0; i < normalized.length; i++) {
19
+ const charCode = normalized.charCodeAt(i);
20
+ vec[i % dimensions] += Math.sin(charCode * (i + 1));
21
+ vec[(i * 2) % dimensions] += Math.cos(charCode);
22
+ }
23
+
24
+ // Normalize vector
25
+ const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
26
+ return magnitude > 0 ? vec.map((v) => v / magnitude) : vec;
27
+ }
28
+
29
+ /**
30
+ * Calculate cosine similarity between two vectors
31
+ */
32
+ export function cosineSimilarity(a: number[], b: number[]): number {
33
+ if (a.length !== b.length) {
34
+ throw new Error('Vectors must have the same dimension');
35
+ }
36
+
37
+ const dot = a.reduce((sum, ai, i) => sum + ai * (b[i] ?? 0), 0);
38
+ const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
39
+ const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
40
+
41
+ return normA > 0 && normB > 0 ? dot / (normA * normB) : 0;
42
+ }
43
+
44
+ /**
45
+ * Get current timestamp in milliseconds
46
+ */
47
+ export function now(): number {
48
+ return Date.now();
49
+ }
@@ -0,0 +1,7 @@
1
+ export { getLocalDB, closeLocalDB } from './_db';
2
+ export { normalizeProjectPath, simpleEmbedding, cosineSimilarity } from './_util';
3
+ export { createLocalStorageRouter } from './_router';
4
+ export { LocalKeyValueStorage } from './keyvalue';
5
+ export { LocalObjectStorage } from './objectstore';
6
+ export { LocalStreamStorage } from './stream';
7
+ export { LocalVectorStorage } from './vector';
@@ -0,0 +1,118 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type {
3
+ KeyValueStorage,
4
+ DataResult,
5
+ DataResultNotFound,
6
+ KeyValueStorageSetParams,
7
+ } from '@agentuity/core';
8
+ import { now } from './_util';
9
+
10
+ export class LocalKeyValueStorage implements KeyValueStorage {
11
+ #db: Database;
12
+ #projectPath: string;
13
+
14
+ constructor(db: Database, projectPath: string) {
15
+ this.#db = db;
16
+ this.#projectPath = projectPath;
17
+ }
18
+
19
+ async get<T>(name: string, key: string): Promise<DataResult<T>> {
20
+ const query = this.#db.query(`
21
+ SELECT value, content_type, expires_at
22
+ FROM kv_storage
23
+ WHERE project_path = ? AND name = ? AND key = ?
24
+ `);
25
+
26
+ const row = query.get(this.#projectPath, name, key) as {
27
+ value: Buffer;
28
+ content_type: string;
29
+ expires_at: number | null;
30
+ } | null;
31
+
32
+ if (!row) {
33
+ return { exists: false } as DataResultNotFound;
34
+ }
35
+
36
+ // Check expiration
37
+ if (row.expires_at && row.expires_at < now()) {
38
+ // Delete expired row
39
+ await this.delete(name, key);
40
+ return { exists: false } as DataResultNotFound;
41
+ }
42
+
43
+ // Deserialize based on content type
44
+ let data: T;
45
+ if (row.content_type === 'application/json') {
46
+ data = JSON.parse(row.value.toString('utf-8'));
47
+ } else if (row.content_type.startsWith('text/')) {
48
+ data = row.value.toString('utf-8') as T;
49
+ } else {
50
+ data = new Uint8Array(row.value) as T;
51
+ }
52
+
53
+ return {
54
+ data,
55
+ contentType: row.content_type,
56
+ exists: true,
57
+ };
58
+ }
59
+
60
+ async set<T = unknown>(
61
+ name: string,
62
+ key: string,
63
+ value: T,
64
+ params?: KeyValueStorageSetParams
65
+ ): Promise<void> {
66
+ // Validate TTL
67
+ if (params?.ttl && params.ttl < 60) {
68
+ throw new Error(`ttl must be at least 60 seconds, got ${params.ttl}`);
69
+ }
70
+
71
+ // Serialize value
72
+ let buffer: Buffer;
73
+ let contentType = params?.contentType || 'application/octet-stream';
74
+
75
+ if (typeof value === 'string') {
76
+ buffer = Buffer.from(value, 'utf-8');
77
+ if (!params?.contentType) {
78
+ contentType = 'text/plain';
79
+ }
80
+ } else if (value instanceof Uint8Array) {
81
+ buffer = Buffer.from(value);
82
+ } else if (value instanceof ArrayBuffer) {
83
+ buffer = Buffer.from(new Uint8Array(value));
84
+ } else if (typeof value === 'object') {
85
+ buffer = Buffer.from(JSON.stringify(value), 'utf-8');
86
+ contentType = 'application/json';
87
+ } else {
88
+ buffer = Buffer.from(String(value), 'utf-8');
89
+ }
90
+
91
+ // Calculate expiration
92
+ const expiresAt = params?.ttl ? now() + params.ttl * 1000 : null;
93
+ const timestamp = now();
94
+
95
+ // UPSERT
96
+ const stmt = this.#db.prepare(`
97
+ INSERT INTO kv_storage (project_path, name, key, value, content_type, expires_at, created_at, updated_at)
98
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
99
+ ON CONFLICT(project_path, name, key)
100
+ DO UPDATE SET
101
+ value = excluded.value,
102
+ content_type = excluded.content_type,
103
+ expires_at = excluded.expires_at,
104
+ updated_at = excluded.updated_at
105
+ `);
106
+
107
+ stmt.run(this.#projectPath, name, key, buffer, contentType, expiresAt, timestamp, timestamp);
108
+ }
109
+
110
+ async delete(name: string, key: string): Promise<void> {
111
+ const stmt = this.#db.prepare(`
112
+ DELETE FROM kv_storage
113
+ WHERE project_path = ? AND name = ? AND key = ?
114
+ `);
115
+
116
+ stmt.run(this.#projectPath, name, key);
117
+ }
118
+ }
@@ -0,0 +1,152 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type {
3
+ ObjectStorage,
4
+ ObjectResult,
5
+ ObjectResultNotFound,
6
+ ObjectStorePutParams,
7
+ CreatePublicURLParams,
8
+ } from '@agentuity/core';
9
+ import { now } from './_util';
10
+
11
+ export class LocalObjectStorage implements ObjectStorage {
12
+ #db: Database;
13
+ #projectPath: string;
14
+ #serverUrl: string;
15
+
16
+ constructor(db: Database, projectPath: string, serverUrl: string) {
17
+ this.#db = db;
18
+ this.#projectPath = projectPath;
19
+ this.#serverUrl = serverUrl;
20
+ }
21
+
22
+ async get(bucket: string, key: string): Promise<ObjectResult> {
23
+ if (!bucket?.trim() || !key?.trim()) {
24
+ throw new Error('bucket and key are required');
25
+ }
26
+
27
+ const query = this.#db.query(`
28
+ SELECT data, content_type
29
+ FROM object_storage
30
+ WHERE project_path = ? AND bucket = ? AND key = ?
31
+ `);
32
+
33
+ const row = query.get(this.#projectPath, bucket, key) as {
34
+ data: Buffer;
35
+ content_type: string;
36
+ } | null;
37
+
38
+ if (!row) {
39
+ return { exists: false } as ObjectResultNotFound;
40
+ }
41
+
42
+ return {
43
+ exists: true,
44
+ data: new Uint8Array(row.data),
45
+ contentType: row.content_type,
46
+ };
47
+ }
48
+
49
+ async put(
50
+ bucket: string,
51
+ key: string,
52
+ data: Uint8Array | ArrayBuffer | ReadableStream,
53
+ params?: ObjectStorePutParams
54
+ ): Promise<void> {
55
+ if (!bucket?.trim() || !key?.trim()) {
56
+ throw new Error('bucket and key are required');
57
+ }
58
+
59
+ // Convert data to Buffer
60
+ let buffer: Buffer;
61
+ if (data instanceof ReadableStream) {
62
+ // Read entire stream into buffer
63
+ const reader = data.getReader();
64
+ const chunks: Uint8Array[] = [];
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+ chunks.push(value);
69
+ }
70
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
71
+ buffer = Buffer.concat(
72
+ chunks.map((c) => Buffer.from(c)),
73
+ totalLength
74
+ );
75
+ } else if (data instanceof ArrayBuffer) {
76
+ buffer = Buffer.from(data);
77
+ } else {
78
+ buffer = Buffer.from(data);
79
+ }
80
+
81
+ const timestamp = now();
82
+ const metadata = params?.metadata ? JSON.stringify(params.metadata) : null;
83
+
84
+ const stmt = this.#db.prepare(`
85
+ INSERT INTO object_storage (
86
+ project_path, bucket, key, data, content_type,
87
+ content_encoding, cache_control, content_disposition,
88
+ content_language, metadata, created_at, updated_at
89
+ )
90
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
91
+ ON CONFLICT(project_path, bucket, key)
92
+ DO UPDATE SET
93
+ data = excluded.data,
94
+ content_type = excluded.content_type,
95
+ content_encoding = excluded.content_encoding,
96
+ cache_control = excluded.cache_control,
97
+ content_disposition = excluded.content_disposition,
98
+ content_language = excluded.content_language,
99
+ metadata = excluded.metadata,
100
+ updated_at = excluded.updated_at
101
+ `);
102
+
103
+ stmt.run(
104
+ this.#projectPath,
105
+ bucket,
106
+ key,
107
+ buffer,
108
+ params?.contentType || 'application/octet-stream',
109
+ params?.contentEncoding || null,
110
+ params?.cacheControl || null,
111
+ params?.contentDisposition || null,
112
+ params?.contentLanguage || null,
113
+ metadata,
114
+ timestamp,
115
+ timestamp
116
+ );
117
+ }
118
+
119
+ async delete(bucket: string, key: string): Promise<boolean> {
120
+ if (!bucket?.trim() || !key?.trim()) {
121
+ throw new Error('bucket and key are required');
122
+ }
123
+
124
+ const stmt = this.#db.prepare(`
125
+ DELETE FROM object_storage
126
+ WHERE project_path = ? AND bucket = ? AND key = ?
127
+ `);
128
+
129
+ const result = stmt.run(this.#projectPath, bucket, key);
130
+ return result.changes > 0;
131
+ }
132
+
133
+ async createPublicURL(
134
+ bucket: string,
135
+ key: string,
136
+ _params?: CreatePublicURLParams
137
+ ): Promise<string> {
138
+ if (!bucket?.trim() || !key?.trim()) {
139
+ throw new Error('bucket and key are required');
140
+ }
141
+
142
+ // Verify object exists
143
+ const result = await this.get(bucket, key);
144
+ if (!result.exists) {
145
+ throw new Error('Object not found');
146
+ }
147
+
148
+ // Return local HTTP URL
149
+ // Note: params.expiresDuration is ignored for local implementation
150
+ return `${this.#serverUrl}/_agentuity/local/object/${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`;
151
+ }
152
+ }