@alexkroman1/aai 0.10.1 → 0.10.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.
Files changed (46) hide show
  1. package/dist/_embeddings.d.ts +31 -0
  2. package/dist/_internal-types-IfPcaJd5.js +61 -0
  3. package/dist/_internal-types.js +1 -60
  4. package/dist/_ssrf-DCp_27V4.js +123 -0
  5. package/dist/_ssrf.js +1 -122
  6. package/dist/_utils-DgzpOMSV.js +61 -0
  7. package/dist/_utils.js +1 -60
  8. package/dist/{direct-executor-Ca0wt5H0.js → direct-executor-B-5mq3cu.js} +15 -17
  9. package/dist/index.js +1 -1
  10. package/dist/kv-iXtikQmR.js +32 -0
  11. package/dist/kv.js +1 -31
  12. package/dist/matchers.js +1 -1
  13. package/dist/middleware-core-BwyBIPed.js +107 -0
  14. package/dist/middleware-core.js +1 -106
  15. package/dist/protocol-B-H2Q4ox.js +162 -0
  16. package/dist/protocol.js +1 -161
  17. package/dist/runtime-CxcwaK68.js +58 -0
  18. package/dist/runtime.js +1 -52
  19. package/dist/s2s-M7JqtgFw.js +272 -0
  20. package/dist/s2s.js +1 -271
  21. package/dist/server.d.ts +6 -6
  22. package/dist/server.js +47 -43
  23. package/dist/{session-BkN9u0ni.js → session-BYlwcrya.js} +6 -6
  24. package/dist/session.js +1 -1
  25. package/dist/telemetry-CJlaDFNc.js +95 -0
  26. package/dist/telemetry.js +1 -94
  27. package/dist/{testing-MRl3SXsI.js → testing-BbitshLb.js} +7 -9
  28. package/dist/testing.js +1 -1
  29. package/dist/types-D8ZBxTL_.js +192 -0
  30. package/dist/types.js +1 -191
  31. package/dist/unstorage-kv-CDgP-frt.js +64 -0
  32. package/dist/unstorage-kv.d.ts +33 -0
  33. package/dist/unstorage-kv.js +2 -0
  34. package/dist/unstorage-vector-Cj5llNhg.js +172 -0
  35. package/dist/unstorage-vector.d.ts +47 -0
  36. package/dist/unstorage-vector.js +2 -0
  37. package/dist/vector.d.ts +3 -2
  38. package/dist/worker-entry-2jaiqIj0.js +70 -0
  39. package/dist/worker-entry.js +1 -69
  40. package/dist/ws-handler-C0Q6eSay.js +207 -0
  41. package/dist/ws-handler.js +1 -206
  42. package/package.json +14 -9
  43. package/dist/sqlite-kv.d.ts +0 -34
  44. package/dist/sqlite-kv.js +0 -133
  45. package/dist/sqlite-vector.d.ts +0 -58
  46. package/dist/sqlite-vector.js +0 -149
@@ -0,0 +1,207 @@
1
+ import { n as errorDetail, r as errorMessage } from "./_utils-DgzpOMSV.js";
2
+ import { r as ClientMessageSchema } from "./protocol-B-H2Q4ox.js";
3
+ import { n as consoleLogger } from "./runtime-CxcwaK68.js";
4
+ import { h as tracer, y as wsSendDroppedCounter } from "./telemetry-CJlaDFNc.js";
5
+ //#region ws-handler.ts
6
+ /**
7
+ * WebSocket session lifecycle handler.
8
+ *
9
+ * Audio validation is handled at the host transport layer (see server.ts).
10
+ */
11
+ /** Default timeout for session.start() in milliseconds. */
12
+ const DEFAULT_SESSION_START_TIMEOUT_MS = 1e4;
13
+ /**
14
+ * Creates a {@link ClientSink} backed by a plain WebSocket.
15
+ *
16
+ * Text events are sent as JSON text frames; audio chunks are sent as
17
+ * binary frames (zero-copy).
18
+ */
19
+ function createClientSink(ws, log) {
20
+ /** Send data over ws, silently dropping if the socket is not open. */
21
+ function safeSend(data) {
22
+ try {
23
+ if (ws.readyState !== 1) return;
24
+ ws.send(data);
25
+ } catch (err) {
26
+ log.debug?.("safeSend: socket closed between readyState check and send", { error: err instanceof Error ? err.message : String(err) });
27
+ wsSendDroppedCounter.add(1);
28
+ }
29
+ }
30
+ return {
31
+ get open() {
32
+ return ws.readyState === 1;
33
+ },
34
+ event(e) {
35
+ safeSend(JSON.stringify(e));
36
+ },
37
+ playAudioChunk(chunk) {
38
+ safeSend(chunk);
39
+ },
40
+ playAudioDone() {
41
+ safeSend(JSON.stringify({ type: "audio_done" }));
42
+ }
43
+ };
44
+ }
45
+ function handleBinaryAudio(data, session) {
46
+ if (data instanceof Uint8Array) {
47
+ session.onAudio(data);
48
+ return true;
49
+ }
50
+ if (data instanceof ArrayBuffer) {
51
+ session.onAudio(new Uint8Array(data));
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ function handleTextMessage(data, session, log, ctx, sid) {
57
+ if (typeof data !== "string") return;
58
+ let json;
59
+ try {
60
+ json = JSON.parse(data);
61
+ } catch {
62
+ log.warn("Invalid JSON from client", {
63
+ ...ctx,
64
+ sid
65
+ });
66
+ return;
67
+ }
68
+ const parsed = ClientMessageSchema.safeParse(json);
69
+ if (!parsed.success) {
70
+ log.warn("Invalid client message", {
71
+ ...ctx,
72
+ sid,
73
+ error: parsed.error.message
74
+ });
75
+ return;
76
+ }
77
+ const msg = parsed.data;
78
+ switch (msg.type) {
79
+ case "audio_ready":
80
+ session.onAudioReady();
81
+ break;
82
+ case "cancel":
83
+ session.onCancel();
84
+ break;
85
+ case "reset":
86
+ session.onReset();
87
+ break;
88
+ case "history":
89
+ session.onHistory(msg.messages);
90
+ break;
91
+ default: break;
92
+ }
93
+ }
94
+ /**
95
+ * Attaches session lifecycle handlers to a native WebSocket using
96
+ * plain JSON text frames and binary audio frames.
97
+ *
98
+ * Connection flow:
99
+ * 1. WebSocket opens → server sends `{ type: "config", ...ReadyConfig }`
100
+ * 2. Client sets up audio → sends `{ type: "audio_ready" }`
101
+ * 3. If reconnecting → client sends `{ type: "history", messages: [...] }`
102
+ */
103
+ function wireSessionSocket(ws, opts) {
104
+ const { sessions, logger: log = consoleLogger } = opts;
105
+ const sessionId = opts.resumeFrom ?? crypto.randomUUID();
106
+ const sid = sessionId.slice(0, 8);
107
+ const ctx = opts.logContext ?? {};
108
+ let session = null;
109
+ /** Set to true once session.start() resolves. Messages arriving before
110
+ * this flag is set are buffered and replayed once the session is ready,
111
+ * preventing audio/text from being dispatched to a half-initialized session. */
112
+ let sessionReady = false;
113
+ let messageBuffer = [];
114
+ const sessionSpan = tracer.startSpan("ws.session", { attributes: { "aai.session.id": sessionId } });
115
+ function drainBuffer() {
116
+ if (!(session && messageBuffer)) return;
117
+ const buf = messageBuffer;
118
+ messageBuffer = null;
119
+ for (const event of buf) {
120
+ const { data } = event;
121
+ if (handleBinaryAudio(data, session)) continue;
122
+ handleTextMessage(data, session, log, ctx, sid);
123
+ }
124
+ }
125
+ function onOpen() {
126
+ opts.onOpen?.();
127
+ log.info("Session connected", {
128
+ ...ctx,
129
+ sid
130
+ });
131
+ sessionSpan.addEvent("ws.open");
132
+ const client = createClientSink(ws, log);
133
+ session = opts.createSession(sessionId, client);
134
+ sessions.set(sessionId, session);
135
+ ws.send(JSON.stringify({
136
+ type: "config",
137
+ ...opts.readyConfig,
138
+ sessionId
139
+ }));
140
+ const timeoutMs = opts.sessionStartTimeoutMs ?? DEFAULT_SESSION_START_TIMEOUT_MS;
141
+ Promise.race([session.start(), new Promise((_resolve, reject) => {
142
+ setTimeout(() => reject(/* @__PURE__ */ new Error(`session.start() timed out after ${timeoutMs}ms`)), timeoutMs);
143
+ })]).then(() => {
144
+ log.info("Session ready", {
145
+ ...ctx,
146
+ sid
147
+ });
148
+ sessionSpan.addEvent("session.ready");
149
+ sessionReady = true;
150
+ drainBuffer();
151
+ }).catch((err) => {
152
+ log.error("Session start failed", {
153
+ ...ctx,
154
+ sid,
155
+ error: errorDetail(err)
156
+ });
157
+ sessionSpan.setStatus({
158
+ code: 2,
159
+ message: errorMessage(err)
160
+ });
161
+ sessions.delete(sessionId);
162
+ session = null;
163
+ messageBuffer = null;
164
+ });
165
+ }
166
+ if (ws.readyState === 1) onOpen();
167
+ else ws.addEventListener("open", onOpen);
168
+ ws.addEventListener("message", (event) => {
169
+ if (!session) return;
170
+ if (!sessionReady) {
171
+ messageBuffer?.push(event);
172
+ return;
173
+ }
174
+ const { data } = event;
175
+ if (handleBinaryAudio(data, session)) return;
176
+ handleTextMessage(data, session, log, ctx, sid);
177
+ });
178
+ ws.addEventListener("close", () => {
179
+ log.info("Session disconnected", {
180
+ ...ctx,
181
+ sid
182
+ });
183
+ sessionSpan.addEvent("ws.close");
184
+ sessionSpan.end();
185
+ if (session) session.stop().catch((err) => {
186
+ log.error("Session stop failed", {
187
+ ...ctx,
188
+ sid,
189
+ error: errorDetail(err)
190
+ });
191
+ }).finally(() => {
192
+ sessions.delete(sessionId);
193
+ });
194
+ opts.onClose?.();
195
+ });
196
+ ws.addEventListener("error", (ev) => {
197
+ const msg = typeof ev.message === "string" ? ev.message : "WebSocket error";
198
+ log.error("WebSocket error", {
199
+ ...ctx,
200
+ sid,
201
+ error: msg
202
+ });
203
+ sessionSpan.recordException(new Error(msg));
204
+ });
205
+ }
206
+ //#endregion
207
+ export { wireSessionSocket as t };
@@ -1,207 +1,2 @@
1
- import { errorDetail, errorMessage } from "./_utils.js";
2
- import { ClientMessageSchema } from "./protocol.js";
3
- import { consoleLogger } from "./runtime.js";
4
- import { tracer, wsSendDroppedCounter } from "./telemetry.js";
5
- //#region ws-handler.ts
6
- /**
7
- * WebSocket session lifecycle handler.
8
- *
9
- * Audio validation is handled at the host transport layer (see server.ts).
10
- */
11
- /** Default timeout for session.start() in milliseconds. */
12
- const DEFAULT_SESSION_START_TIMEOUT_MS = 1e4;
13
- /**
14
- * Creates a {@link ClientSink} backed by a plain WebSocket.
15
- *
16
- * Text events are sent as JSON text frames; audio chunks are sent as
17
- * binary frames (zero-copy).
18
- */
19
- function createClientSink(ws, log) {
20
- /** Send data over ws, silently dropping if the socket is not open. */
21
- function safeSend(data) {
22
- try {
23
- if (ws.readyState !== 1) return;
24
- ws.send(data);
25
- } catch (err) {
26
- log.debug?.("safeSend: socket closed between readyState check and send", { error: err instanceof Error ? err.message : String(err) });
27
- wsSendDroppedCounter.add(1);
28
- }
29
- }
30
- return {
31
- get open() {
32
- return ws.readyState === 1;
33
- },
34
- event(e) {
35
- safeSend(JSON.stringify(e));
36
- },
37
- playAudioChunk(chunk) {
38
- safeSend(chunk);
39
- },
40
- playAudioDone() {
41
- safeSend(JSON.stringify({ type: "audio_done" }));
42
- }
43
- };
44
- }
45
- function handleBinaryAudio(data, session) {
46
- if (data instanceof Uint8Array) {
47
- session.onAudio(data);
48
- return true;
49
- }
50
- if (data instanceof ArrayBuffer) {
51
- session.onAudio(new Uint8Array(data));
52
- return true;
53
- }
54
- return false;
55
- }
56
- function handleTextMessage(data, session, log, ctx, sid) {
57
- if (typeof data !== "string") return;
58
- let json;
59
- try {
60
- json = JSON.parse(data);
61
- } catch {
62
- log.warn("Invalid JSON from client", {
63
- ...ctx,
64
- sid
65
- });
66
- return;
67
- }
68
- const parsed = ClientMessageSchema.safeParse(json);
69
- if (!parsed.success) {
70
- log.warn("Invalid client message", {
71
- ...ctx,
72
- sid,
73
- error: parsed.error.message
74
- });
75
- return;
76
- }
77
- const msg = parsed.data;
78
- switch (msg.type) {
79
- case "audio_ready":
80
- session.onAudioReady();
81
- break;
82
- case "cancel":
83
- session.onCancel();
84
- break;
85
- case "reset":
86
- session.onReset();
87
- break;
88
- case "history":
89
- session.onHistory(msg.messages);
90
- break;
91
- default: break;
92
- }
93
- }
94
- /**
95
- * Attaches session lifecycle handlers to a native WebSocket using
96
- * plain JSON text frames and binary audio frames.
97
- *
98
- * Connection flow:
99
- * 1. WebSocket opens → server sends `{ type: "config", ...ReadyConfig }`
100
- * 2. Client sets up audio → sends `{ type: "audio_ready" }`
101
- * 3. If reconnecting → client sends `{ type: "history", messages: [...] }`
102
- */
103
- function wireSessionSocket(ws, opts) {
104
- const { sessions, logger: log = consoleLogger } = opts;
105
- const sessionId = opts.resumeFrom ?? crypto.randomUUID();
106
- const sid = sessionId.slice(0, 8);
107
- const ctx = opts.logContext ?? {};
108
- let session = null;
109
- /** Set to true once session.start() resolves. Messages arriving before
110
- * this flag is set are buffered and replayed once the session is ready,
111
- * preventing audio/text from being dispatched to a half-initialized session. */
112
- let sessionReady = false;
113
- let messageBuffer = [];
114
- const sessionSpan = tracer.startSpan("ws.session", { attributes: { "aai.session.id": sessionId } });
115
- function drainBuffer() {
116
- if (!(session && messageBuffer)) return;
117
- const buf = messageBuffer;
118
- messageBuffer = null;
119
- for (const event of buf) {
120
- const { data } = event;
121
- if (handleBinaryAudio(data, session)) continue;
122
- handleTextMessage(data, session, log, ctx, sid);
123
- }
124
- }
125
- function onOpen() {
126
- opts.onOpen?.();
127
- log.info("Session connected", {
128
- ...ctx,
129
- sid
130
- });
131
- sessionSpan.addEvent("ws.open");
132
- const client = createClientSink(ws, log);
133
- session = opts.createSession(sessionId, client);
134
- sessions.set(sessionId, session);
135
- ws.send(JSON.stringify({
136
- type: "config",
137
- ...opts.readyConfig,
138
- sessionId
139
- }));
140
- const timeoutMs = opts.sessionStartTimeoutMs ?? DEFAULT_SESSION_START_TIMEOUT_MS;
141
- Promise.race([session.start(), new Promise((_resolve, reject) => {
142
- setTimeout(() => reject(/* @__PURE__ */ new Error(`session.start() timed out after ${timeoutMs}ms`)), timeoutMs);
143
- })]).then(() => {
144
- log.info("Session ready", {
145
- ...ctx,
146
- sid
147
- });
148
- sessionSpan.addEvent("session.ready");
149
- sessionReady = true;
150
- drainBuffer();
151
- }).catch((err) => {
152
- log.error("Session start failed", {
153
- ...ctx,
154
- sid,
155
- error: errorDetail(err)
156
- });
157
- sessionSpan.setStatus({
158
- code: 2,
159
- message: errorMessage(err)
160
- });
161
- sessions.delete(sessionId);
162
- session = null;
163
- messageBuffer = null;
164
- });
165
- }
166
- if (ws.readyState === 1) onOpen();
167
- else ws.addEventListener("open", onOpen);
168
- ws.addEventListener("message", (event) => {
169
- if (!session) return;
170
- if (!sessionReady) {
171
- messageBuffer?.push(event);
172
- return;
173
- }
174
- const { data } = event;
175
- if (handleBinaryAudio(data, session)) return;
176
- handleTextMessage(data, session, log, ctx, sid);
177
- });
178
- ws.addEventListener("close", () => {
179
- log.info("Session disconnected", {
180
- ...ctx,
181
- sid
182
- });
183
- sessionSpan.addEvent("ws.close");
184
- sessionSpan.end();
185
- if (session) session.stop().catch((err) => {
186
- log.error("Session stop failed", {
187
- ...ctx,
188
- sid,
189
- error: errorDetail(err)
190
- });
191
- }).finally(() => {
192
- sessions.delete(sessionId);
193
- });
194
- opts.onClose?.();
195
- });
196
- ws.addEventListener("error", (ev) => {
197
- const msg = typeof ev.message === "string" ? ev.message : "WebSocket error";
198
- log.error("WebSocket error", {
199
- ...ctx,
200
- sid,
201
- error: msg
202
- });
203
- sessionSpan.recordException(new Error(msg));
204
- });
205
- }
206
- //#endregion
1
+ import { t as wireSessionSocket } from "./ws-handler-C0Q6eSay.js";
207
2
  export { wireSessionSocket };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"
@@ -96,15 +96,15 @@
96
96
  "types": "./dist/middleware-core.d.ts",
97
97
  "import": "./dist/middleware-core.js"
98
98
  },
99
- "./sqlite-kv": {
100
- "source": "./sqlite-kv.ts",
101
- "types": "./dist/sqlite-kv.d.ts",
102
- "import": "./dist/sqlite-kv.js"
99
+ "./unstorage-kv": {
100
+ "source": "./unstorage-kv.ts",
101
+ "types": "./dist/unstorage-kv.d.ts",
102
+ "import": "./dist/unstorage-kv.js"
103
103
  },
104
- "./sqlite-vector": {
105
- "source": "./sqlite-vector.ts",
106
- "types": "./dist/sqlite-vector.d.ts",
107
- "import": "./dist/sqlite-vector.js"
104
+ "./unstorage-vector": {
105
+ "source": "./unstorage-vector.ts",
106
+ "types": "./dist/unstorage-vector.d.ts",
107
+ "import": "./dist/unstorage-vector.js"
108
108
  }
109
109
  },
110
110
  "dependencies": {
@@ -112,11 +112,13 @@
112
112
  "html-to-text": "^9.0.5",
113
113
  "nanoevents": "^9.1.0",
114
114
  "secure-exec": "^0.1.0",
115
+ "unstorage": "^1.17.5",
115
116
  "ws": "^8.20.0",
116
117
  "zod": "^4.3.6"
117
118
  },
118
119
  "peerDependencies": {
119
120
  "@hono/node-server": "^1.19.11",
121
+ "@hono/node-ws": "^1.3.0",
120
122
  "@opentelemetry/api": "^1.9.1",
121
123
  "hono": "^4.12.9",
122
124
  "vitest": "^4.1.1"
@@ -125,6 +127,9 @@
125
127
  "@hono/node-server": {
126
128
  "optional": true
127
129
  },
130
+ "@hono/node-ws": {
131
+ "optional": true
132
+ },
128
133
  "@opentelemetry/api": {
129
134
  "optional": true
130
135
  },
@@ -1,34 +0,0 @@
1
- /**
2
- * SQLite-backed key-value storage for local development.
3
- *
4
- * Persists data across restarts using a local SQLite database file.
5
- * Uses `node:sqlite` (built into Node 22+) — no native dependencies.
6
- * Drop-in replacement for the in-memory KV store.
7
- */
8
- import type { Kv } from "./kv.ts";
9
- /**
10
- * Options for creating a SQLite-backed KV store.
11
- */
12
- export type SqliteKvOptions = {
13
- /** Path to the SQLite database file. Defaults to `.aai/local.db`. */
14
- path?: string;
15
- };
16
- /**
17
- * Create a SQLite-backed KV store for local development.
18
- *
19
- * Data persists to a local SQLite file (default: `.aai/local.db`).
20
- * TTL expiration is enforced on reads and periodically cleaned up.
21
- *
22
- * @param options - Optional configuration. See {@link SqliteKvOptions}.
23
- * @returns A {@link Kv} instance backed by SQLite.
24
- *
25
- * @example
26
- * ```ts
27
- * import { createSqliteKv } from "@alexkroman1/aai/sqlite-kv";
28
- *
29
- * const kv = createSqliteKv();
30
- * await kv.set("greeting", "hello");
31
- * const value = await kv.get<string>("greeting"); // "hello"
32
- * ```
33
- */
34
- export declare function createSqliteKv(options?: SqliteKvOptions): Kv;
package/dist/sqlite-kv.js DELETED
@@ -1,133 +0,0 @@
1
- import { MAX_VALUE_SIZE, matchGlob, sortAndPaginate } from "./kv.js";
2
- import { DatabaseSync } from "node:sqlite";
3
- //#region sqlite-kv.ts
4
- /**
5
- * SQLite-backed key-value storage for local development.
6
- *
7
- * Persists data across restarts using a local SQLite database file.
8
- * Uses `node:sqlite` (built into Node 22+) — no native dependencies.
9
- * Drop-in replacement for the in-memory KV store.
10
- */
11
- /**
12
- * Create a SQLite-backed KV store for local development.
13
- *
14
- * Data persists to a local SQLite file (default: `.aai/local.db`).
15
- * TTL expiration is enforced on reads and periodically cleaned up.
16
- *
17
- * @param options - Optional configuration. See {@link SqliteKvOptions}.
18
- * @returns A {@link Kv} instance backed by SQLite.
19
- *
20
- * @example
21
- * ```ts
22
- * import { createSqliteKv } from "@alexkroman1/aai/sqlite-kv";
23
- *
24
- * const kv = createSqliteKv();
25
- * await kv.set("greeting", "hello");
26
- * const value = await kv.get<string>("greeting"); // "hello"
27
- * ```
28
- */
29
- function createSqliteKv(options) {
30
- const db = new DatabaseSync(options?.path ?? ".aai/local.db");
31
- db.exec("PRAGMA journal_mode = WAL");
32
- db.exec(`
33
- CREATE TABLE IF NOT EXISTS kv (
34
- key TEXT PRIMARY KEY,
35
- value TEXT NOT NULL,
36
- expires_at INTEGER
37
- )
38
- `);
39
- db.exec(`
40
- CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv(expires_at)
41
- WHERE expires_at IS NOT NULL
42
- `);
43
- const stmtGet = db.prepare("SELECT value, expires_at FROM kv WHERE key = ?");
44
- const stmtUpsert = db.prepare("INSERT INTO kv (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at");
45
- const stmtDelete = db.prepare("DELETE FROM kv WHERE key = ?");
46
- const stmtDeleteExpired = db.prepare("DELETE FROM kv WHERE expires_at IS NOT NULL AND expires_at <= ?");
47
- const stmtListPrefix = db.prepare("SELECT key, value FROM kv WHERE key >= ? AND key < ? AND (expires_at IS NULL OR expires_at > ?)");
48
- const stmtListAll = db.prepare("SELECT key, value FROM kv WHERE expires_at IS NULL OR expires_at > ?");
49
- const stmtKeysAll = db.prepare("SELECT key FROM kv WHERE expires_at IS NULL OR expires_at > ?");
50
- const stmtKeysPrefix = db.prepare("SELECT key FROM kv WHERE key >= ? AND key < ? AND (expires_at IS NULL OR expires_at > ?)");
51
- /** Compute the exclusive upper bound for a prefix scan. */
52
- function prefixUpperBound(prefix) {
53
- if (prefix === "") return "￿";
54
- const last = prefix.charCodeAt(prefix.length - 1);
55
- return prefix.slice(0, -1) + String.fromCharCode(last + 1);
56
- }
57
- const cleanupInterval = setInterval(() => {
58
- stmtDeleteExpired.run(Date.now());
59
- }, 6e4);
60
- if (cleanupInterval.unref) cleanupInterval.unref();
61
- return {
62
- close() {
63
- clearInterval(cleanupInterval);
64
- db.close();
65
- },
66
- get(key) {
67
- const now = Date.now();
68
- const row = stmtGet.get(key);
69
- if (!row) return Promise.resolve(null);
70
- if (row.expires_at !== null && row.expires_at <= now) {
71
- stmtDelete.run(key);
72
- return Promise.resolve(null);
73
- }
74
- return Promise.resolve(JSON.parse(row.value));
75
- },
76
- set(key, value, setOptions) {
77
- try {
78
- const raw = JSON.stringify(value);
79
- if (raw.length > 65536) return Promise.reject(/* @__PURE__ */ new Error(`Value exceeds max size of ${MAX_VALUE_SIZE} bytes`));
80
- const expireIn = setOptions?.expireIn;
81
- const expiresAt = expireIn && expireIn > 0 ? Date.now() + expireIn : null;
82
- stmtUpsert.run(key, raw, expiresAt);
83
- return Promise.resolve();
84
- } catch (err) {
85
- return Promise.reject(err);
86
- }
87
- },
88
- delete(keys) {
89
- const keyArray = Array.isArray(keys) ? keys : [keys];
90
- for (const k of keyArray) stmtDelete.run(k);
91
- return Promise.resolve();
92
- },
93
- list(prefix, listOptions) {
94
- const now = Date.now();
95
- let rows;
96
- if (prefix === "") rows = stmtListAll.all(now);
97
- else {
98
- const upper = prefixUpperBound(prefix);
99
- rows = stmtListPrefix.all(prefix, upper, now);
100
- }
101
- const entries = rows.map((row) => ({
102
- key: row.key,
103
- value: JSON.parse(row.value)
104
- }));
105
- return Promise.resolve(sortAndPaginate(entries, listOptions));
106
- },
107
- keys(pattern) {
108
- const now = Date.now();
109
- const isGlob = pattern?.includes("*");
110
- if (!pattern) {
111
- const keys = stmtKeysAll.all(now).map((r) => r.key);
112
- return Promise.resolve(keys.sort((a, b) => a.localeCompare(b)));
113
- }
114
- if (isGlob) {
115
- const starIdx = pattern.indexOf("*");
116
- const prefix = pattern.slice(0, starIdx);
117
- let rows;
118
- if (prefix === "") rows = stmtKeysAll.all(now);
119
- else {
120
- const upper = prefixUpperBound(prefix);
121
- rows = stmtKeysPrefix.all(prefix, upper, now);
122
- }
123
- const keys = rows.filter((r) => matchGlob(r.key, pattern)).map((r) => r.key);
124
- return Promise.resolve(keys.sort((a, b) => a.localeCompare(b)));
125
- }
126
- const upper = prefixUpperBound(pattern);
127
- const keys = stmtKeysPrefix.all(pattern, upper, now).map((r) => r.key);
128
- return Promise.resolve(keys.sort((a, b) => a.localeCompare(b)));
129
- }
130
- };
131
- }
132
- //#endregion
133
- export { createSqliteKv };
@@ -1,58 +0,0 @@
1
- /**
2
- * SQLite-backed vector store with local embeddings.
3
- *
4
- * Persists data across restarts using a local SQLite database file.
5
- * Uses brute-force cosine similarity over `node:sqlite` — no native
6
- * extensions required. Fast enough for local dev (sub-ms for <10k vectors).
7
- * Embeddings are computed locally via `all-MiniLM-L6-v2` (384 dims) —
8
- * no external API key required. The model is downloaded on first use
9
- * (~86 MB) and cached in `.aai/models/`.
10
- */
11
- import type { VectorStore } from "./vector.ts";
12
- /** Function that converts text into an embedding vector. */
13
- export type EmbedFn = (text: string) => Promise<number[]>;
14
- /**
15
- * Options for creating a SQLite-vec backed vector store.
16
- */
17
- export type SqliteVecVectorStoreOptions = {
18
- /** Path to the SQLite database file. Defaults to `.aai/vectors.db`. */
19
- path?: string;
20
- /** Custom embedding function. Defaults to local `all-MiniLM-L6-v2` model. */
21
- embedFn?: EmbedFn;
22
- /** Embedding dimensions. Must match the embedFn output. Defaults to 384. */
23
- dimensions?: number;
24
- /** Directory for caching downloaded models. Defaults to `.aai/models`. */
25
- modelCacheDir?: string;
26
- };
27
- /**
28
- * Create a deterministic hash-based embedding function for testing.
29
- *
30
- * Produces repeatable vectors where similar text yields similar embeddings.
31
- * Not suitable for production — use the default local model instead.
32
- *
33
- * @param dimensions - Vector dimensions (default: 384).
34
- */
35
- export declare function createTestEmbedFn(dimensions?: number): EmbedFn;
36
- /**
37
- * Create a SQLite-backed vector store with local embeddings.
38
- *
39
- * Data persists to a local SQLite file (default: `.aai/vectors.db`).
40
- * Embeddings are computed locally using `all-MiniLM-L6-v2` by default —
41
- * no API key required. The model auto-downloads on first use (~86 MB).
42
- *
43
- * Vector search uses brute-force cosine similarity over all stored
44
- * embeddings. This is fast for local dev workloads (<10k vectors).
45
- *
46
- * @param options - See {@link SqliteVecVectorStoreOptions}.
47
- * @returns A {@link VectorStore} instance.
48
- *
49
- * @example
50
- * ```ts
51
- * import { createSqliteVectorStore } from "@alexkroman1/aai/sqlite-vector";
52
- *
53
- * const vector = createSqliteVectorStore();
54
- * await vector.upsert("doc-1", "The capital of France is Paris.");
55
- * const results = await vector.query("France capital");
56
- * ```
57
- */
58
- export declare function createSqliteVectorStore(options?: SqliteVecVectorStoreOptions): VectorStore;