@sqlite-sync/ai 0.6.0 → 0.7.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/README.md +14 -12
- package/dist/index.d.ts +41 -19
- package/dist/index.js +155 -22
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# @sqlite-sync/ai
|
|
2
2
|
|
|
3
|
-
AI agent tools for [@sqlite-sync](https://github.com/krolebord-dev/sqlite-sync) databases. Gives an AI SDK agent safe, read-only access to a synced SQLite database.
|
|
3
|
+
AI agent tools for [@sqlite-sync](https://github.com/krolebord-dev/sqlite-sync) databases. Gives an AI SDK agent safe, read-only access to a synced SQLite database by default, with an explicit opt-in CRDT mutation tool.
|
|
4
4
|
|
|
5
5
|
## What's included
|
|
6
6
|
|
|
7
|
-
- `createSchemaDoc` — generates a markdown schema doc
|
|
7
|
+
- `createSchemaDoc` — generates a markdown schema doc from the declared sync schema (structure from the `t.table()` builders, semantics from `.describe()` and app-provided context). No database access needed.
|
|
8
8
|
- `createAiDbAccess` — server-side access object living next to the storage; its methods double as an RPC contract for cross-Durable-Object setups.
|
|
9
|
-
- `createDbTools` — AI SDK v6 `ToolSet` (`getDbSchema` and `queryDb` tools) backed by an `AiDbAccess` or a stub proxying to one.
|
|
9
|
+
- `createDbTools` — AI SDK v6 `ToolSet` (`getDbSchema` and `queryDb` tools, plus optional `mutateDb`) backed by an `AiDbAccess` or a stub proxying to one.
|
|
10
10
|
|
|
11
11
|
`queryDb` is strictly read-only: a single `SELECT`/`WITH`/`VALUES` statement, verified against SQLite's `EXPLAIN` bytecode for write opcodes, and executed inside a transaction that is always rolled back. Reads are **not** restricted by table — the agent can query every table in the database file (including sqlite-sync's internal event log), so don't colocate data the agent must not see. Results are capped (default 200 rows, 2000 chars per cell) and report `truncated` so the agent can narrow its query.
|
|
12
12
|
|
|
13
|
+
`mutateDb` is opt-in. It applies `item-created`, `item-updated`, and `item-deleted` CRDT events through sqlite-sync's own-event path, so writes are validated, persisted to the event log, applied locally, and synced normally. It does not run arbitrary write SQL. For `item-created` events, omit `item_id` and `payload.id`; the tool generates ids, injects them into the CRDT events, and returns them as `createdIds`.
|
|
14
|
+
|
|
13
15
|
## Usage (Cloudflare Durable Object)
|
|
14
16
|
|
|
15
17
|
```ts
|
|
@@ -18,18 +20,13 @@ import { createKyselyExecutor, durableObjectAdapter } from "@sqlite-sync/cloudfl
|
|
|
18
20
|
|
|
19
21
|
// In the DO that owns the synced database:
|
|
20
22
|
async onStart() {
|
|
21
|
-
await durableObjectAdapter.createCrdtStorage({ syncDbSchema, storage: this.ctx.storage, /* ... */ });
|
|
23
|
+
const { syncDb } = await durableObjectAdapter.createCrdtStorage({ syncDbSchema, storage: this.ctx.storage, /* ... */ });
|
|
22
24
|
this.aiDbAccess = createAiDbAccess({
|
|
23
25
|
executor: createKyselyExecutor(this.ctx.storage),
|
|
26
|
+
storage: syncDb, // optional; enables mutate() on the access object
|
|
24
27
|
syncDbSchema,
|
|
25
28
|
context: {
|
|
26
29
|
overview: "# My app's database\n\nA todo app for a single user.",
|
|
27
|
-
tables: {
|
|
28
|
-
todos: {
|
|
29
|
-
description: "The user's todos.",
|
|
30
|
-
columns: { completed: "1 when done." },
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
30
|
},
|
|
34
31
|
});
|
|
35
32
|
}
|
|
@@ -41,6 +38,9 @@ getDbSchemaDoc() {
|
|
|
41
38
|
queryDb(input: AiQueryInput) {
|
|
42
39
|
return this.aiDbAccess.query(input);
|
|
43
40
|
}
|
|
41
|
+
mutateDb(input: AiMutationInput) {
|
|
42
|
+
return this.aiDbAccess.mutate?.(input) ?? { error: "Database mutations are not enabled." };
|
|
43
|
+
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
```ts
|
|
@@ -54,12 +54,14 @@ getTools() {
|
|
|
54
54
|
return {
|
|
55
55
|
getSchemaDoc: () => stub.getDbSchemaDoc(),
|
|
56
56
|
query: (input) => stub.queryDb(input),
|
|
57
|
+
mutate: (input) => stub.mutateDb(input),
|
|
57
58
|
};
|
|
58
59
|
},
|
|
60
|
+
mutations: true,
|
|
59
61
|
});
|
|
60
62
|
}
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
The doc skips the internal `tombstone` column and renders enum columns with their allowed values and boolean columns with a `0/1` hint. Table and column descriptions come from `.describe()` on the builders.
|
|
64
66
|
|
|
65
|
-
The generated doc always includes a built-in preamble (after your `overview`) explaining sqlite-sync mechanics — that the database syncs between devices and that the listed tables are read-only views with soft-deleted rows already filtered out.
|
|
67
|
+
The generated doc always includes a built-in preamble (after your `overview`) explaining sqlite-sync mechanics — that the database syncs between devices and that the listed tables are read-only views with soft-deleted rows already filtered out. Prefer keeping domain semantics in the schema with `.describe()` and using `context.overview` for app-level notes.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import { SyncDbSchema } from '@sqlite-sync/core';
|
|
1
|
+
import { SyncDbSchema, CrdtEventType, CrdtStorage } from '@sqlite-sync/core';
|
|
2
2
|
import { ToolSet } from 'ai';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* App-provided semantics merged into the generated schema doc.
|
|
6
|
-
* Keys of `tables` are CRDT view names (`crdtTableName`), not base table names.
|
|
7
|
-
*/
|
|
8
4
|
type SchemaDocContext = {
|
|
9
5
|
overview?: string;
|
|
10
|
-
tables?: Record<string, {
|
|
11
|
-
description?: string;
|
|
12
|
-
columns?: Record<string, string>;
|
|
13
|
-
}>;
|
|
14
6
|
};
|
|
15
7
|
/**
|
|
16
|
-
* Generates a markdown schema doc for an AI agent
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
8
|
+
* Generates a markdown schema doc for an AI agent from the declared sync schema's table
|
|
9
|
+
* builders — no database access needed. Tables are presented under the view names the agent
|
|
10
|
+
* queries; descriptions come from `.describe()` on the table and column builders.
|
|
11
|
+
* The internal `tombstone` column is omitted.
|
|
20
12
|
*
|
|
21
13
|
* The doc always includes a built-in preamble explaining sqlite-sync mechanics (read-only
|
|
22
14
|
* views, soft-deletes already filtered) after the consumer's `overview` — consumers only
|
|
23
15
|
* need to describe their own domain.
|
|
24
16
|
*/
|
|
25
17
|
declare function createSchemaDoc(opts: {
|
|
26
|
-
execute: (sql: string) => Record<string, unknown>[];
|
|
27
18
|
syncDbSchema: SyncDbSchema;
|
|
28
19
|
context?: SchemaDocContext;
|
|
29
20
|
}): string;
|
|
@@ -53,21 +44,48 @@ type AiQueryResult = {
|
|
|
53
44
|
} | {
|
|
54
45
|
error: string;
|
|
55
46
|
};
|
|
47
|
+
type AiMutationEvent = {
|
|
48
|
+
type: "item-created";
|
|
49
|
+
dataset: string;
|
|
50
|
+
item_id?: never;
|
|
51
|
+
payload?: Record<string, unknown>;
|
|
52
|
+
} | {
|
|
53
|
+
type: Exclude<CrdtEventType, "item-created">;
|
|
54
|
+
dataset: string;
|
|
55
|
+
item_id: string;
|
|
56
|
+
payload?: Record<string, unknown>;
|
|
57
|
+
};
|
|
58
|
+
type AiMutationInput = {
|
|
59
|
+
events: AiMutationEvent[];
|
|
60
|
+
};
|
|
61
|
+
type AiMutationResult = {
|
|
62
|
+
applied: true;
|
|
63
|
+
eventCount: number;
|
|
64
|
+
createdIds: string[];
|
|
65
|
+
} | {
|
|
66
|
+
error: string;
|
|
67
|
+
errors?: string[];
|
|
68
|
+
};
|
|
56
69
|
/**
|
|
57
|
-
*
|
|
70
|
+
* AI access to a synced database. Lives where the storage lives; its method names
|
|
58
71
|
* are the RPC contract, so a DO stub proxying to these methods exposes the same surface
|
|
59
72
|
* (promise-wrapped) and satisfies the tool layer's `DbToolsAccess`.
|
|
60
73
|
*
|
|
61
74
|
* `query` enforces read-only (single SELECT/WITH/VALUES statement, no write opcodes, executed
|
|
62
75
|
* in a forced-rollback transaction) but reads are not restricted by table — the whole database
|
|
63
76
|
* file is in scope for the agent, so don't colocate data the agent must not see.
|
|
77
|
+
*
|
|
78
|
+
* `mutate` is only present when `createAiDbAccess` receives a CRDT storage. Mutations are CRDT
|
|
79
|
+
* events applied through sqlite-sync's normal own-event path, never direct SQL writes.
|
|
64
80
|
*/
|
|
65
81
|
type AiDbAccess = {
|
|
66
82
|
getSchemaDoc(): string;
|
|
67
83
|
query(input: AiQueryInput): AiQueryResult;
|
|
84
|
+
mutate?(input: AiMutationInput): AiMutationResult;
|
|
68
85
|
};
|
|
69
86
|
declare function createAiDbAccess(opts: {
|
|
70
87
|
executor: AiDbExecutor;
|
|
88
|
+
storage?: Pick<CrdtStorage, "applyOwnEvents">;
|
|
71
89
|
syncDbSchema: SyncDbSchema;
|
|
72
90
|
context?: SchemaDocContext;
|
|
73
91
|
limits?: {
|
|
@@ -125,13 +143,17 @@ type MaybePromise<T> = T | Promise<T>;
|
|
|
125
143
|
type DbToolsAccess = {
|
|
126
144
|
getSchemaDoc(): MaybePromise<string>;
|
|
127
145
|
query(input: AiQueryInput): MaybePromise<AiQueryResult>;
|
|
146
|
+
mutate?(input: AiMutationInput): MaybePromise<AiMutationResult>;
|
|
147
|
+
};
|
|
148
|
+
type CreateDbToolsOptions = {
|
|
149
|
+
access: () => MaybePromise<DbToolsAccess>;
|
|
150
|
+
/** Expose the write-capable `mutateDb` tool. The access object must also implement `mutate`. */
|
|
151
|
+
mutations?: boolean;
|
|
128
152
|
};
|
|
129
153
|
/**
|
|
130
154
|
* AI SDK tools for a synced database. `access` is a factory because acquiring the database
|
|
131
155
|
* may itself be async per call (e.g. resolving a Durable Object stub from another DO).
|
|
132
156
|
*/
|
|
133
|
-
declare function createDbTools(opts:
|
|
134
|
-
access: () => MaybePromise<DbToolsAccess>;
|
|
135
|
-
}): ToolSet;
|
|
157
|
+
declare function createDbTools(opts: CreateDbToolsOptions): ToolSet;
|
|
136
158
|
|
|
137
|
-
export { type AiDbAccess, type AiDbExecuteParams, type AiDbExecutor, type AiQueryInput, type AiQueryResult, type DbToolsAccess, type QueryGuard, QueryGuardError, type QueryGuardInput, type QueryGuardRejection, type QueryGuardVerdict, type SchemaDocContext, createAiDbAccess, createDbTools, createQueryGuard, createSchemaDoc, runWithForcedRollback };
|
|
159
|
+
export { type AiDbAccess, type AiDbExecuteParams, type AiDbExecutor, type AiMutationEvent, type AiMutationInput, type AiMutationResult, type AiQueryInput, type AiQueryResult, type CreateDbToolsOptions, type DbToolsAccess, type QueryGuard, QueryGuardError, type QueryGuardInput, type QueryGuardRejection, type QueryGuardVerdict, type SchemaDocContext, createAiDbAccess, createDbTools, createQueryGuard, createSchemaDoc, runWithForcedRollback };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// src/db-access.ts
|
|
2
|
+
import {
|
|
3
|
+
CrdtEventValidationError,
|
|
4
|
+
generateId
|
|
5
|
+
} from "@sqlite-sync/core";
|
|
6
|
+
|
|
1
7
|
// src/query-guard.ts
|
|
2
8
|
var QueryGuardError = class extends Error {
|
|
3
9
|
rejection;
|
|
@@ -113,8 +119,17 @@ var SCHEMA_DOC_PREAMBLE = [
|
|
|
113
119
|
"read-only SQL views; soft-deleted rows are already filtered out, so query them directly",
|
|
114
120
|
"without any tombstone filtering. Every table has a unique `id` text primary key."
|
|
115
121
|
].join("\n");
|
|
116
|
-
function
|
|
117
|
-
|
|
122
|
+
function renderColumn(name, meta) {
|
|
123
|
+
let line = `- \`${name}\` ${meta.sqlType.toUpperCase()}`;
|
|
124
|
+
if (!meta.nullable) {
|
|
125
|
+
line += " NOT NULL";
|
|
126
|
+
}
|
|
127
|
+
if (meta.kind === "boolean") {
|
|
128
|
+
line += " (boolean 0/1)";
|
|
129
|
+
} else if (meta.kind === "enum") {
|
|
130
|
+
line += ` (one of ${(meta.enumValues ?? []).map((value) => `"${value}"`).join(" | ")})`;
|
|
131
|
+
}
|
|
132
|
+
return meta.description ? `${line} \u2014 ${meta.description}` : line;
|
|
118
133
|
}
|
|
119
134
|
function createSchemaDoc(opts) {
|
|
120
135
|
const sections = [];
|
|
@@ -123,20 +138,15 @@ function createSchemaDoc(opts) {
|
|
|
123
138
|
sections.push(overview);
|
|
124
139
|
}
|
|
125
140
|
sections.push(SCHEMA_DOC_PREAMBLE);
|
|
126
|
-
for (const table of opts.syncDbSchema.
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (tableContext?.description) {
|
|
131
|
-
lines.push("", tableContext.description.trim());
|
|
141
|
+
for (const [crdtTableName, table] of Object.entries(opts.syncDbSchema.tables)) {
|
|
142
|
+
const lines = [`## ${crdtTableName}`];
|
|
143
|
+
if (table.description) {
|
|
144
|
+
lines.push("", table.description.trim());
|
|
132
145
|
}
|
|
133
146
|
lines.push("", "Columns:");
|
|
134
|
-
for (const
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
lines.push(
|
|
138
|
-
`- \`${column.name}\` ${column.type}${column.notnull ? " NOT NULL" : ""}${description ? ` \u2014 ${description}` : ""}`
|
|
139
|
-
);
|
|
147
|
+
for (const [name, meta] of Object.entries(table.columns)) {
|
|
148
|
+
if (name === "tombstone") continue;
|
|
149
|
+
lines.push(renderColumn(name, meta));
|
|
140
150
|
}
|
|
141
151
|
sections.push(lines.join("\n"));
|
|
142
152
|
}
|
|
@@ -152,17 +162,12 @@ function toBase64(bytes) {
|
|
|
152
162
|
return btoa(binary);
|
|
153
163
|
}
|
|
154
164
|
function createAiDbAccess(opts) {
|
|
155
|
-
|
|
165
|
+
const schemaDoc = createSchemaDoc({ syncDbSchema: opts.syncDbSchema, context: opts.context });
|
|
156
166
|
const guard = createQueryGuard({ executor: opts.executor });
|
|
157
167
|
const maxRows = opts.limits?.maxRows ?? 200;
|
|
158
168
|
const maxCellChars = opts.limits?.maxCellChars ?? 2e3;
|
|
159
|
-
|
|
169
|
+
const access = {
|
|
160
170
|
getSchemaDoc() {
|
|
161
|
-
schemaDoc ??= createSchemaDoc({
|
|
162
|
-
execute: (sql) => opts.executor.execute({ sql, parameters: [] }).rows,
|
|
163
|
-
syncDbSchema: opts.syncDbSchema,
|
|
164
|
-
context: opts.context
|
|
165
|
-
});
|
|
166
171
|
return schemaDoc;
|
|
167
172
|
},
|
|
168
173
|
query(input) {
|
|
@@ -194,6 +199,57 @@ function createAiDbAccess(opts) {
|
|
|
194
199
|
return { rows, rowCount: resultRows.length, truncated };
|
|
195
200
|
}
|
|
196
201
|
};
|
|
202
|
+
const storage = opts.storage;
|
|
203
|
+
if (storage) {
|
|
204
|
+
access.mutate = (input) => {
|
|
205
|
+
const errors = [];
|
|
206
|
+
const createdIds = [];
|
|
207
|
+
const events = [];
|
|
208
|
+
for (const [index, event] of input.events.entries()) {
|
|
209
|
+
if (event.type === "item-created") {
|
|
210
|
+
const looseEvent = event;
|
|
211
|
+
const payload = event.payload ?? {};
|
|
212
|
+
if (looseEvent.item_id !== void 0) {
|
|
213
|
+
errors.push(`[${index}] item-created events must omit item_id; an id is generated automatically`);
|
|
214
|
+
}
|
|
215
|
+
if ("id" in payload) {
|
|
216
|
+
errors.push(`[${index}] item-created payload must omit id; an id is generated automatically`);
|
|
217
|
+
}
|
|
218
|
+
if (looseEvent.item_id !== void 0 || "id" in payload) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const id = generateId();
|
|
222
|
+
createdIds.push(id);
|
|
223
|
+
events.push({
|
|
224
|
+
type: "item-created",
|
|
225
|
+
dataset: event.dataset,
|
|
226
|
+
item_id: id,
|
|
227
|
+
payload: JSON.stringify({ ...payload, id })
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
events.push({
|
|
232
|
+
type: event.type,
|
|
233
|
+
dataset: event.dataset,
|
|
234
|
+
item_id: event.item_id,
|
|
235
|
+
payload: JSON.stringify(event.payload ?? {})
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (errors.length > 0) {
|
|
239
|
+
return { error: `Invalid mutation events: ${errors.join("; ")}`, errors };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
storage.applyOwnEvents(events);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof CrdtEventValidationError) {
|
|
245
|
+
return { error: error.message, errors: error.errors };
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
return { applied: true, eventCount: events.length, createdIds };
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return access;
|
|
197
253
|
}
|
|
198
254
|
|
|
199
255
|
// src/tools.ts
|
|
@@ -219,8 +275,71 @@ var queryInputSchema = jsonSchema({
|
|
|
219
275
|
required: ["sql"],
|
|
220
276
|
additionalProperties: false
|
|
221
277
|
});
|
|
278
|
+
var mutationInputSchema = jsonSchema({
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
events: {
|
|
282
|
+
type: "array",
|
|
283
|
+
minItems: 1,
|
|
284
|
+
items: {
|
|
285
|
+
anyOf: [
|
|
286
|
+
{
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
type: {
|
|
290
|
+
type: "string",
|
|
291
|
+
enum: ["item-created"],
|
|
292
|
+
description: "Create a synced row. Omit item_id and payload.id; the tool generates the id."
|
|
293
|
+
},
|
|
294
|
+
dataset: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: "The synced dataset/table name from the schema documentation."
|
|
297
|
+
},
|
|
298
|
+
payload: {
|
|
299
|
+
type: "object",
|
|
300
|
+
additionalProperties: true,
|
|
301
|
+
not: { required: ["id"] },
|
|
302
|
+
description: "Column values for the new row, excluding id. Include all required non-id columns from the schema."
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
required: ["type", "dataset", "payload"],
|
|
306
|
+
additionalProperties: false
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
type: {
|
|
312
|
+
type: "string",
|
|
313
|
+
enum: ["item-updated", "item-deleted"],
|
|
314
|
+
description: "Update or delete an existing synced row."
|
|
315
|
+
},
|
|
316
|
+
dataset: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description: "The synced dataset/table name from the schema documentation."
|
|
319
|
+
},
|
|
320
|
+
item_id: {
|
|
321
|
+
type: "string",
|
|
322
|
+
description: "The stable id of the row being updated or deleted."
|
|
323
|
+
},
|
|
324
|
+
payload: {
|
|
325
|
+
type: "object",
|
|
326
|
+
additionalProperties: true,
|
|
327
|
+
description: "Changed column values for item-updated. Omit or pass {} for item-deleted."
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
required: ["type", "dataset", "item_id"],
|
|
331
|
+
additionalProperties: false
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
description: "One or more CRDT mutation events to apply atomically."
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
required: ["events"],
|
|
339
|
+
additionalProperties: false
|
|
340
|
+
});
|
|
222
341
|
function createDbTools(opts) {
|
|
223
|
-
|
|
342
|
+
const tools = {
|
|
224
343
|
getDbSchema: tool({
|
|
225
344
|
description: "Get the schema documentation for the synced SQLite database: tables, columns, types, and data conventions. Call this before reasoning about the data.",
|
|
226
345
|
inputSchema: emptyInputSchema,
|
|
@@ -238,6 +357,20 @@ function createDbTools(opts) {
|
|
|
238
357
|
}
|
|
239
358
|
})
|
|
240
359
|
};
|
|
360
|
+
if (opts.mutations) {
|
|
361
|
+
tools.mutateDb = tool({
|
|
362
|
+
description: "Apply one or more CRDT mutation events to the synced database. Use this for writes instead of SQL. Query the current data first when updating or deleting existing rows. Create events must omit ids: do not provide item_id or payload.id, because the tool generates ids and returns them. Create payloads must include all required non-id columns, update events should include only changed columns, and delete events should use an empty payload.",
|
|
363
|
+
inputSchema: mutationInputSchema,
|
|
364
|
+
execute: async (input) => {
|
|
365
|
+
const access = await opts.access();
|
|
366
|
+
if (!access.mutate) {
|
|
367
|
+
return { error: "Database mutations are not enabled for this access object." };
|
|
368
|
+
}
|
|
369
|
+
return await access.mutate(input);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return tools;
|
|
241
374
|
}
|
|
242
375
|
export {
|
|
243
376
|
QueryGuardError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/query-guard.ts","../src/schema-doc.ts","../src/db-access.ts","../src/tools.ts"],"sourcesContent":["import type { AiDbExecutor } from \"./db-access\";\n\nexport type QueryGuardInput = {\n sql: string;\n parameters?: readonly unknown[];\n};\n\nexport type QueryGuardRejection = {\n allowed: false;\n code: \"invalid-statement\" | \"multi-statement\" | \"invalid-sql\" | \"write-detected\";\n message: string;\n};\n\nexport type QueryGuardVerdict = { allowed: true } | QueryGuardRejection;\n\nexport class QueryGuardError extends Error {\n readonly rejection: QueryGuardRejection;\n\n constructor(rejection: QueryGuardRejection) {\n super(rejection.message);\n this.name = \"QueryGuardError\";\n this.rejection = rejection;\n }\n}\n\nexport type QueryGuard = {\n /**\n * Statically verifies the query is a read-only single statement. Reads are not restricted\n * by table — the whole database file is in scope for the agent, so don't colocate data the\n * agent must not see.\n */\n check(input: QueryGuardInput): QueryGuardVerdict;\n /**\n * `check` + execute inside a forced-rollback transaction (the unconditional backstop for\n * anything the analysis might miss). Throws {@link QueryGuardError} when the check rejects.\n */\n execute<TResult = unknown>(input: QueryGuardInput): { rows: TResult[] };\n};\n\n/**\n * Opcodes that prove a statement is not read-only. `OpenWrite` covers row-level writes (every\n * real-table mutation opens its cursor through it), `Clear`/`Destroy` cover whole-table\n * deletes that skip cursors (truncate optimization), the rest cover DDL, pragma-class\n * statements (which a transaction rollback would NOT undo), virtual-table writes, and trigger\n * subprograms. `Insert`/`Delete`/`IdxInsert` are deliberately absent — they also run against\n * ephemeral/sorter cursors in ordinary SELECTs (DISTINCT, ORDER BY) and are only dangerous on\n * a cursor an `OpenWrite` would have created.\n */\nconst WRITE_OPCODES = new Set([\n \"OpenWrite\",\n \"Clear\",\n \"Destroy\",\n \"CreateBtree\",\n \"SqlExec\",\n \"ParseSchema\",\n \"DropTable\",\n \"DropIndex\",\n \"DropTrigger\",\n \"SetCookie\",\n \"JournalMode\",\n \"Vacuum\",\n \"IncrVacuum\",\n \"Checkpoint\",\n \"MaxPgcnt\",\n \"Expire\",\n \"AutoCommit\",\n \"VUpdate\",\n \"VCreate\",\n \"VDestroy\",\n \"Program\",\n]);\n\ntype ExplainRow = {\n opcode: string;\n};\n\nconst READ_STATEMENT_PREFIXES = [\"select\", \"with\", \"values\"];\n\nexport function createQueryGuard(opts: { executor: AiDbExecutor }): QueryGuard {\n function reject(code: QueryGuardRejection[\"code\"], message: string): QueryGuardRejection {\n return { allowed: false, code, message };\n }\n\n function check(input: QueryGuardInput): QueryGuardVerdict {\n let body = input.sql.trim();\n while (body.endsWith(\";\")) {\n body = body.slice(0, -1).trimEnd();\n }\n\n if (body.includes(\";\")) {\n return reject(\n \"multi-statement\",\n \"Only a single SQL statement is allowed per query, and semicolons are only allowed at the very end. If the semicolon is inside a string literal, pass the value as a bound parameter instead.\",\n );\n }\n\n const lowered = body.toLowerCase();\n if (!READ_STATEMENT_PREFIXES.some((keyword) => lowered.startsWith(keyword))) {\n return reject(\n \"invalid-statement\",\n \"Only read-only queries are allowed: the statement must start directly with SELECT, WITH, or VALUES (no leading comments).\",\n );\n }\n\n let operations: ExplainRow[];\n try {\n operations = opts.executor.execute<ExplainRow>({\n sql: `EXPLAIN ${input.sql}`,\n parameters: input.parameters ?? [],\n }).rows;\n } catch (error) {\n return reject(\"invalid-sql\", `SQL error: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n for (const operation of operations) {\n if (WRITE_OPCODES.has(operation.opcode)) {\n return reject(\n \"write-detected\",\n `The statement was rejected because it would modify the database (${operation.opcode}). This tool is strictly read-only; rewrite the query as a pure SELECT.`,\n );\n }\n }\n\n return { allowed: true };\n }\n\n return {\n check,\n execute(input) {\n const verdict = check(input);\n if (!verdict.allowed) {\n throw new QueryGuardError(verdict);\n }\n return runWithForcedRollback(opts.executor, (tx) => {\n return tx.execute({ sql: input.sql, parameters: input.parameters ?? [] });\n });\n },\n };\n}\n\n/**\n * Runs `callback` in a transaction that is always rolled back (via a sentinel throw, relying\n * on the {@link AiDbExecutor} contract that a throwing callback rolls back). This is the\n * unconditional write guard backing any gap in the static analysis.\n */\nexport function runWithForcedRollback<T>(\n executor: Pick<AiDbExecutor, \"transaction\">,\n callback: (tx: Pick<AiDbExecutor, \"execute\">) => T,\n): T {\n let result: T | undefined;\n let completed = false;\n const rollbackSentinel = new Error(\"forced read-only rollback\");\n\n try {\n executor.transaction((tx) => {\n result = callback(tx);\n completed = true;\n throw rollbackSentinel;\n });\n } catch (error) {\n if (error !== rollbackSentinel) {\n throw error;\n }\n }\n\n if (!completed) {\n throw new Error(\"Transaction completed without running the callback\");\n }\n return result as T;\n}\n","import type { SyncDbSchema } from \"@sqlite-sync/core\";\n\n/**\n * App-provided semantics merged into the generated schema doc.\n * Keys of `tables` are CRDT view names (`crdtTableName`), not base table names.\n */\nexport type SchemaDocContext = {\n overview?: string;\n tables?: Record<string, { description?: string; columns?: Record<string, string> }>;\n};\n\n// Library mechanics every generated doc should explain, so consumers only have to describe\n// their own domain in `context`. Kept free of tombstone-column details the agent never sees.\nconst SCHEMA_DOC_PREAMBLE = [\n \"This is a synced SQLite database — data replicates automatically between the user's devices.\",\n \"All writes go through a sync event log, which is why the tables listed below are exposed as\",\n \"read-only SQL views; soft-deleted rows are already filtered out, so query them directly\",\n \"without any tombstone filtering. Every table has a unique `id` text primary key.\",\n].join(\"\\n\");\n\ntype TableInfoRow = {\n cid: number;\n name: string;\n type: string;\n notnull: number;\n};\n\nfunction quoteId(name: string) {\n return `\"${name.replaceAll('\"', '\"\"')}\"`;\n}\n\n/**\n * Generates a markdown schema doc for an AI agent by introspecting the synced database.\n * Structure comes from `PRAGMA table_info` on the base tables (introspecting the views would\n * lose NOT NULL fidelity), presented under the view names the agent queries; semantics come\n * from the consumer-provided context. The internal `tombstone` column is omitted.\n *\n * The doc always includes a built-in preamble explaining sqlite-sync mechanics (read-only\n * views, soft-deletes already filtered) after the consumer's `overview` — consumers only\n * need to describe their own domain.\n */\nexport function createSchemaDoc(opts: {\n execute: (sql: string) => Record<string, unknown>[];\n syncDbSchema: SyncDbSchema;\n context?: SchemaDocContext;\n}): string {\n const sections: string[] = [];\n\n const overview = opts.context?.overview?.trim();\n if (overview) {\n sections.push(overview);\n }\n sections.push(SCHEMA_DOC_PREAMBLE);\n\n for (const table of opts.syncDbSchema.tablesConfig) {\n const tableContext = opts.context?.tables?.[table.crdtTableName];\n const columns = opts.execute(`PRAGMA table_info(${quoteId(table.baseTableName)})`) as TableInfoRow[];\n\n const lines = [`## ${table.crdtTableName}`];\n if (tableContext?.description) {\n lines.push(\"\", tableContext.description.trim());\n }\n lines.push(\"\", \"Columns:\");\n for (const column of columns) {\n if (column.name === \"tombstone\") continue;\n const description = tableContext?.columns?.[column.name];\n lines.push(\n `- \\`${column.name}\\` ${column.type}${column.notnull ? \" NOT NULL\" : \"\"}${description ? ` — ${description}` : \"\"}`,\n );\n }\n sections.push(lines.join(\"\\n\"));\n }\n\n return sections.join(\"\\n\\n\");\n}\n","import type { SyncDbSchema } from \"@sqlite-sync/core\";\nimport { createQueryGuard, QueryGuardError } from \"./query-guard\";\nimport { createSchemaDoc, type SchemaDocContext } from \"./schema-doc\";\n\nexport type AiDbExecuteParams = {\n sql: string;\n parameters: readonly unknown[];\n};\n\n/**\n * Minimal executor contract for AI database access. Runtime-specific — inject the one that\n * matches where the storage lives; @sqlite-sync/cloudflare's `createKyselyExecutor` satisfies it.\n */\nexport type AiDbExecutor = {\n execute<TResult = unknown>(query: AiDbExecuteParams): { rows: TResult[] };\n transaction(callback: (tx: Pick<AiDbExecutor, \"execute\">) => void): void;\n};\n\nexport type AiQueryInput = {\n sql: string;\n parameters?: readonly unknown[];\n};\n\nexport type AiQueryResult =\n | {\n rows: Record<string, unknown>[];\n rowCount: number;\n truncated: boolean;\n }\n | {\n error: string;\n };\n\n/**\n * Read-only AI access to a synced database. Lives where the storage lives; its method names\n * are the RPC contract, so a DO stub proxying to these methods exposes the same surface\n * (promise-wrapped) and satisfies the tool layer's `DbToolsAccess`.\n *\n * `query` enforces read-only (single SELECT/WITH/VALUES statement, no write opcodes, executed\n * in a forced-rollback transaction) but reads are not restricted by table — the whole database\n * file is in scope for the agent, so don't colocate data the agent must not see.\n */\nexport type AiDbAccess = {\n getSchemaDoc(): string;\n query(input: AiQueryInput): AiQueryResult;\n};\n\nfunction toBase64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\nexport function createAiDbAccess(opts: {\n executor: AiDbExecutor;\n syncDbSchema: SyncDbSchema;\n context?: SchemaDocContext;\n limits?: { maxRows?: number; maxCellChars?: number };\n}): AiDbAccess {\n let schemaDoc: string | undefined;\n const guard = createQueryGuard({ executor: opts.executor });\n const maxRows = opts.limits?.maxRows ?? 200;\n const maxCellChars = opts.limits?.maxCellChars ?? 2000;\n\n return {\n getSchemaDoc() {\n schemaDoc ??= createSchemaDoc({\n execute: (sql) => opts.executor.execute<Record<string, unknown>>({ sql, parameters: [] }).rows,\n syncDbSchema: opts.syncDbSchema,\n context: opts.context,\n });\n return schemaDoc;\n },\n query(input) {\n let resultRows: Record<string, unknown>[];\n try {\n resultRows = guard.execute<Record<string, unknown>>(input).rows;\n } catch (error) {\n if (error instanceof QueryGuardError) {\n return { error: error.message };\n }\n throw error;\n }\n\n let truncated = resultRows.length > maxRows;\n\n function shapeCell(value: unknown): unknown {\n if (value instanceof Uint8Array) {\n // Base64 inflates by 4/3, so budget the encoded length against the cell cap.\n if (Math.ceil(value.byteLength / 3) * 4 <= maxCellChars) {\n return `<blob base64 ${toBase64(value)}>`;\n }\n truncated = true;\n return `<blob ${value.byteLength} bytes>`;\n }\n if (typeof value === \"string\" && value.length > maxCellChars) {\n truncated = true;\n return `${value.slice(0, maxCellChars)}…`;\n }\n return value;\n }\n\n const rows = resultRows\n .slice(0, maxRows)\n .map((row) => Object.fromEntries(Object.entries(row).map(([column, value]) => [column, shapeCell(value)])));\n\n return { rows, rowCount: resultRows.length, truncated };\n },\n };\n}\n","import { jsonSchema, type ToolSet, tool } from \"ai\";\nimport type { AiQueryInput, AiQueryResult } from \"./db-access\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\n/**\n * What the tools need from the database side. Satisfied directly by `AiDbAccess`, or by a\n * Durable Object stub whose RPC methods delegate to one (RPC wraps returns in promises).\n */\nexport type DbToolsAccess = {\n getSchemaDoc(): MaybePromise<string>;\n query(input: AiQueryInput): MaybePromise<AiQueryResult>;\n};\n\nconst emptyInputSchema = jsonSchema<Record<string, never>>({\n type: \"object\",\n properties: {},\n additionalProperties: false,\n});\n\nconst queryInputSchema = jsonSchema<{ sql: string; parameters?: unknown[] }>({\n type: \"object\",\n properties: {\n sql: {\n type: \"string\",\n description:\n \"A single read-only SQLite statement starting with SELECT, WITH, or VALUES. Use ? placeholders for values.\",\n },\n parameters: {\n type: \"array\",\n items: { type: [\"string\", \"number\", \"boolean\", \"null\"] },\n description: \"Values bound to the ? placeholders, in order.\",\n },\n },\n required: [\"sql\"],\n additionalProperties: false,\n});\n\n/**\n * AI SDK tools for a synced database. `access` is a factory because acquiring the database\n * may itself be async per call (e.g. resolving a Durable Object stub from another DO).\n */\nexport function createDbTools(opts: { access: () => MaybePromise<DbToolsAccess> }): ToolSet {\n return {\n getDbSchema: tool({\n description:\n \"Get the schema documentation for the synced SQLite database: tables, columns, types, and data conventions. Call this before reasoning about the data.\",\n inputSchema: emptyInputSchema,\n execute: async () => {\n const access = await opts.access();\n return await access.getSchemaDoc();\n },\n }),\n queryDb: tool({\n description:\n \"Run a read-only SQL query against the synced SQLite database. Only a single SELECT/WITH/VALUES statement is allowed — anything that writes is rejected. Pass values as ? placeholders via `parameters` instead of inlining them as literals. Results are capped; ask for fewer columns or add LIMIT/WHERE if `truncated` is true.\",\n inputSchema: queryInputSchema,\n execute: async ({ sql, parameters }) => {\n const access = await opts.access();\n return await access.query({ sql, parameters });\n },\n }),\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/db-access.ts","../src/query-guard.ts","../src/schema-doc.ts","../src/tools.ts"],"sourcesContent":["import {\n type CrdtEventType,\n CrdtEventValidationError,\n type CrdtStorage,\n generateId,\n type OwnCrdtEvent,\n type SyncDbSchema,\n} from \"@sqlite-sync/core\";\nimport { createQueryGuard, QueryGuardError } from \"./query-guard\";\nimport { createSchemaDoc, type SchemaDocContext } from \"./schema-doc\";\n\nexport type AiDbExecuteParams = {\n sql: string;\n parameters: readonly unknown[];\n};\n\n/**\n * Minimal executor contract for AI database access. Runtime-specific — inject the one that\n * matches where the storage lives; @sqlite-sync/cloudflare's `createKyselyExecutor` satisfies it.\n */\nexport type AiDbExecutor = {\n execute<TResult = unknown>(query: AiDbExecuteParams): { rows: TResult[] };\n transaction(callback: (tx: Pick<AiDbExecutor, \"execute\">) => void): void;\n};\n\nexport type AiQueryInput = {\n sql: string;\n parameters?: readonly unknown[];\n};\n\nexport type AiQueryResult =\n | {\n rows: Record<string, unknown>[];\n rowCount: number;\n truncated: boolean;\n }\n | {\n error: string;\n };\n\nexport type AiMutationEvent =\n | {\n type: \"item-created\";\n dataset: string;\n item_id?: never;\n payload?: Record<string, unknown>;\n }\n | {\n type: Exclude<CrdtEventType, \"item-created\">;\n dataset: string;\n item_id: string;\n payload?: Record<string, unknown>;\n };\n\nexport type AiMutationInput = {\n events: AiMutationEvent[];\n};\n\nexport type AiMutationResult =\n | {\n applied: true;\n eventCount: number;\n createdIds: string[];\n }\n | {\n error: string;\n errors?: string[];\n };\n\n/**\n * AI access to a synced database. Lives where the storage lives; its method names\n * are the RPC contract, so a DO stub proxying to these methods exposes the same surface\n * (promise-wrapped) and satisfies the tool layer's `DbToolsAccess`.\n *\n * `query` enforces read-only (single SELECT/WITH/VALUES statement, no write opcodes, executed\n * in a forced-rollback transaction) but reads are not restricted by table — the whole database\n * file is in scope for the agent, so don't colocate data the agent must not see.\n *\n * `mutate` is only present when `createAiDbAccess` receives a CRDT storage. Mutations are CRDT\n * events applied through sqlite-sync's normal own-event path, never direct SQL writes.\n */\nexport type AiDbAccess = {\n getSchemaDoc(): string;\n query(input: AiQueryInput): AiQueryResult;\n mutate?(input: AiMutationInput): AiMutationResult;\n};\n\nfunction toBase64(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary);\n}\n\nexport function createAiDbAccess(opts: {\n executor: AiDbExecutor;\n storage?: Pick<CrdtStorage, \"applyOwnEvents\">;\n syncDbSchema: SyncDbSchema;\n context?: SchemaDocContext;\n limits?: { maxRows?: number; maxCellChars?: number };\n}): AiDbAccess {\n const schemaDoc = createSchemaDoc({ syncDbSchema: opts.syncDbSchema, context: opts.context });\n const guard = createQueryGuard({ executor: opts.executor });\n const maxRows = opts.limits?.maxRows ?? 200;\n const maxCellChars = opts.limits?.maxCellChars ?? 2000;\n\n const access: AiDbAccess = {\n getSchemaDoc() {\n return schemaDoc;\n },\n query(input) {\n let resultRows: Record<string, unknown>[];\n try {\n resultRows = guard.execute<Record<string, unknown>>(input).rows;\n } catch (error) {\n if (error instanceof QueryGuardError) {\n return { error: error.message };\n }\n throw error;\n }\n\n let truncated = resultRows.length > maxRows;\n\n function shapeCell(value: unknown): unknown {\n if (value instanceof Uint8Array) {\n // Base64 inflates by 4/3, so budget the encoded length against the cell cap.\n if (Math.ceil(value.byteLength / 3) * 4 <= maxCellChars) {\n return `<blob base64 ${toBase64(value)}>`;\n }\n truncated = true;\n return `<blob ${value.byteLength} bytes>`;\n }\n if (typeof value === \"string\" && value.length > maxCellChars) {\n truncated = true;\n return `${value.slice(0, maxCellChars)}…`;\n }\n return value;\n }\n\n const rows = resultRows\n .slice(0, maxRows)\n .map((row) => Object.fromEntries(Object.entries(row).map(([column, value]) => [column, shapeCell(value)])));\n\n return { rows, rowCount: resultRows.length, truncated };\n },\n };\n\n const storage = opts.storage;\n if (storage) {\n access.mutate = (input) => {\n const errors: string[] = [];\n const createdIds: string[] = [];\n const events: OwnCrdtEvent[] = [];\n\n for (const [index, event] of input.events.entries()) {\n if (event.type === \"item-created\") {\n const looseEvent = event as { item_id?: unknown };\n const payload = event.payload ?? {};\n if (looseEvent.item_id !== undefined) {\n errors.push(`[${index}] item-created events must omit item_id; an id is generated automatically`);\n }\n if (\"id\" in payload) {\n errors.push(`[${index}] item-created payload must omit id; an id is generated automatically`);\n }\n if (looseEvent.item_id !== undefined || \"id\" in payload) {\n continue;\n }\n\n const id = generateId();\n createdIds.push(id);\n events.push({\n type: \"item-created\",\n dataset: event.dataset,\n item_id: id,\n payload: JSON.stringify({ ...payload, id }),\n });\n continue;\n }\n\n events.push({\n type: event.type,\n dataset: event.dataset,\n item_id: event.item_id,\n payload: JSON.stringify(event.payload ?? {}),\n });\n }\n\n if (errors.length > 0) {\n return { error: `Invalid mutation events: ${errors.join(\"; \")}`, errors };\n }\n\n try {\n storage.applyOwnEvents(events);\n } catch (error) {\n if (error instanceof CrdtEventValidationError) {\n return { error: error.message, errors: error.errors };\n }\n throw error;\n }\n\n return { applied: true, eventCount: events.length, createdIds };\n };\n }\n\n return access;\n}\n","import type { AiDbExecutor } from \"./db-access\";\n\nexport type QueryGuardInput = {\n sql: string;\n parameters?: readonly unknown[];\n};\n\nexport type QueryGuardRejection = {\n allowed: false;\n code: \"invalid-statement\" | \"multi-statement\" | \"invalid-sql\" | \"write-detected\";\n message: string;\n};\n\nexport type QueryGuardVerdict = { allowed: true } | QueryGuardRejection;\n\nexport class QueryGuardError extends Error {\n readonly rejection: QueryGuardRejection;\n\n constructor(rejection: QueryGuardRejection) {\n super(rejection.message);\n this.name = \"QueryGuardError\";\n this.rejection = rejection;\n }\n}\n\nexport type QueryGuard = {\n /**\n * Statically verifies the query is a read-only single statement. Reads are not restricted\n * by table — the whole database file is in scope for the agent, so don't colocate data the\n * agent must not see.\n */\n check(input: QueryGuardInput): QueryGuardVerdict;\n /**\n * `check` + execute inside a forced-rollback transaction (the unconditional backstop for\n * anything the analysis might miss). Throws {@link QueryGuardError} when the check rejects.\n */\n execute<TResult = unknown>(input: QueryGuardInput): { rows: TResult[] };\n};\n\n/**\n * Opcodes that prove a statement is not read-only. `OpenWrite` covers row-level writes (every\n * real-table mutation opens its cursor through it), `Clear`/`Destroy` cover whole-table\n * deletes that skip cursors (truncate optimization), the rest cover DDL, pragma-class\n * statements (which a transaction rollback would NOT undo), virtual-table writes, and trigger\n * subprograms. `Insert`/`Delete`/`IdxInsert` are deliberately absent — they also run against\n * ephemeral/sorter cursors in ordinary SELECTs (DISTINCT, ORDER BY) and are only dangerous on\n * a cursor an `OpenWrite` would have created.\n */\nconst WRITE_OPCODES = new Set([\n \"OpenWrite\",\n \"Clear\",\n \"Destroy\",\n \"CreateBtree\",\n \"SqlExec\",\n \"ParseSchema\",\n \"DropTable\",\n \"DropIndex\",\n \"DropTrigger\",\n \"SetCookie\",\n \"JournalMode\",\n \"Vacuum\",\n \"IncrVacuum\",\n \"Checkpoint\",\n \"MaxPgcnt\",\n \"Expire\",\n \"AutoCommit\",\n \"VUpdate\",\n \"VCreate\",\n \"VDestroy\",\n \"Program\",\n]);\n\ntype ExplainRow = {\n opcode: string;\n};\n\nconst READ_STATEMENT_PREFIXES = [\"select\", \"with\", \"values\"];\n\nexport function createQueryGuard(opts: { executor: AiDbExecutor }): QueryGuard {\n function reject(code: QueryGuardRejection[\"code\"], message: string): QueryGuardRejection {\n return { allowed: false, code, message };\n }\n\n function check(input: QueryGuardInput): QueryGuardVerdict {\n let body = input.sql.trim();\n while (body.endsWith(\";\")) {\n body = body.slice(0, -1).trimEnd();\n }\n\n if (body.includes(\";\")) {\n return reject(\n \"multi-statement\",\n \"Only a single SQL statement is allowed per query, and semicolons are only allowed at the very end. If the semicolon is inside a string literal, pass the value as a bound parameter instead.\",\n );\n }\n\n const lowered = body.toLowerCase();\n if (!READ_STATEMENT_PREFIXES.some((keyword) => lowered.startsWith(keyword))) {\n return reject(\n \"invalid-statement\",\n \"Only read-only queries are allowed: the statement must start directly with SELECT, WITH, or VALUES (no leading comments).\",\n );\n }\n\n let operations: ExplainRow[];\n try {\n operations = opts.executor.execute<ExplainRow>({\n sql: `EXPLAIN ${input.sql}`,\n parameters: input.parameters ?? [],\n }).rows;\n } catch (error) {\n return reject(\"invalid-sql\", `SQL error: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n for (const operation of operations) {\n if (WRITE_OPCODES.has(operation.opcode)) {\n return reject(\n \"write-detected\",\n `The statement was rejected because it would modify the database (${operation.opcode}). This tool is strictly read-only; rewrite the query as a pure SELECT.`,\n );\n }\n }\n\n return { allowed: true };\n }\n\n return {\n check,\n execute(input) {\n const verdict = check(input);\n if (!verdict.allowed) {\n throw new QueryGuardError(verdict);\n }\n return runWithForcedRollback(opts.executor, (tx) => {\n return tx.execute({ sql: input.sql, parameters: input.parameters ?? [] });\n });\n },\n };\n}\n\n/**\n * Runs `callback` in a transaction that is always rolled back (via a sentinel throw, relying\n * on the {@link AiDbExecutor} contract that a throwing callback rolls back). This is the\n * unconditional write guard backing any gap in the static analysis.\n */\nexport function runWithForcedRollback<T>(\n executor: Pick<AiDbExecutor, \"transaction\">,\n callback: (tx: Pick<AiDbExecutor, \"execute\">) => T,\n): T {\n let result: T | undefined;\n let completed = false;\n const rollbackSentinel = new Error(\"forced read-only rollback\");\n\n try {\n executor.transaction((tx) => {\n result = callback(tx);\n completed = true;\n throw rollbackSentinel;\n });\n } catch (error) {\n if (error !== rollbackSentinel) {\n throw error;\n }\n }\n\n if (!completed) {\n throw new Error(\"Transaction completed without running the callback\");\n }\n return result as T;\n}\n","import type { ColumnMeta, SyncDbSchema } from \"@sqlite-sync/core\";\n\nexport type SchemaDocContext = {\n overview?: string;\n};\n\n// Library mechanics every generated doc should explain, so consumers only have to describe\n// their own domain in `context`. Kept free of tombstone-column details the agent never sees.\nconst SCHEMA_DOC_PREAMBLE = [\n \"This is a synced SQLite database — data replicates automatically between the user's devices.\",\n \"All writes go through a sync event log, which is why the tables listed below are exposed as\",\n \"read-only SQL views; soft-deleted rows are already filtered out, so query them directly\",\n \"without any tombstone filtering. Every table has a unique `id` text primary key.\",\n].join(\"\\n\");\n\nfunction renderColumn(name: string, meta: ColumnMeta): string {\n let line = `- \\`${name}\\` ${meta.sqlType.toUpperCase()}`;\n if (!meta.nullable) {\n line += \" NOT NULL\";\n }\n if (meta.kind === \"boolean\") {\n line += \" (boolean 0/1)\";\n } else if (meta.kind === \"enum\") {\n line += ` (one of ${(meta.enumValues ?? []).map((value) => `\"${value}\"`).join(\" | \")})`;\n }\n return meta.description ? `${line} — ${meta.description}` : line;\n}\n\n/**\n * Generates a markdown schema doc for an AI agent from the declared sync schema's table\n * builders — no database access needed. Tables are presented under the view names the agent\n * queries; descriptions come from `.describe()` on the table and column builders.\n * The internal `tombstone` column is omitted.\n *\n * The doc always includes a built-in preamble explaining sqlite-sync mechanics (read-only\n * views, soft-deletes already filtered) after the consumer's `overview` — consumers only\n * need to describe their own domain.\n */\nexport function createSchemaDoc(opts: { syncDbSchema: SyncDbSchema; context?: SchemaDocContext }): string {\n const sections: string[] = [];\n\n const overview = opts.context?.overview?.trim();\n if (overview) {\n sections.push(overview);\n }\n sections.push(SCHEMA_DOC_PREAMBLE);\n\n for (const [crdtTableName, table] of Object.entries(opts.syncDbSchema.tables)) {\n const lines = [`## ${crdtTableName}`];\n if (table.description) {\n lines.push(\"\", table.description.trim());\n }\n lines.push(\"\", \"Columns:\");\n for (const [name, meta] of Object.entries(table.columns)) {\n if (name === \"tombstone\") continue;\n lines.push(renderColumn(name, meta));\n }\n sections.push(lines.join(\"\\n\"));\n }\n\n return sections.join(\"\\n\\n\");\n}\n","import { jsonSchema, type ToolSet, tool } from \"ai\";\nimport type { AiMutationInput, AiMutationResult, AiQueryInput, AiQueryResult } from \"./db-access\";\n\ntype MaybePromise<T> = T | Promise<T>;\n\n/**\n * What the tools need from the database side. Satisfied directly by `AiDbAccess`, or by a\n * Durable Object stub whose RPC methods delegate to one (RPC wraps returns in promises).\n */\nexport type DbToolsAccess = {\n getSchemaDoc(): MaybePromise<string>;\n query(input: AiQueryInput): MaybePromise<AiQueryResult>;\n mutate?(input: AiMutationInput): MaybePromise<AiMutationResult>;\n};\n\nexport type CreateDbToolsOptions = {\n access: () => MaybePromise<DbToolsAccess>;\n /** Expose the write-capable `mutateDb` tool. The access object must also implement `mutate`. */\n mutations?: boolean;\n};\n\nconst emptyInputSchema = jsonSchema<Record<string, never>>({\n type: \"object\",\n properties: {},\n additionalProperties: false,\n});\n\nconst queryInputSchema = jsonSchema<{ sql: string; parameters?: unknown[] }>({\n type: \"object\",\n properties: {\n sql: {\n type: \"string\",\n description:\n \"A single read-only SQLite statement starting with SELECT, WITH, or VALUES. Use ? placeholders for values.\",\n },\n parameters: {\n type: \"array\",\n items: { type: [\"string\", \"number\", \"boolean\", \"null\"] },\n description: \"Values bound to the ? placeholders, in order.\",\n },\n },\n required: [\"sql\"],\n additionalProperties: false,\n});\n\nconst mutationInputSchema = jsonSchema<AiMutationInput>({\n type: \"object\",\n properties: {\n events: {\n type: \"array\",\n minItems: 1,\n items: {\n anyOf: [\n {\n type: \"object\",\n properties: {\n type: {\n type: \"string\",\n enum: [\"item-created\"],\n description: \"Create a synced row. Omit item_id and payload.id; the tool generates the id.\",\n },\n dataset: {\n type: \"string\",\n description: \"The synced dataset/table name from the schema documentation.\",\n },\n payload: {\n type: \"object\",\n additionalProperties: true,\n not: { required: [\"id\"] },\n description:\n \"Column values for the new row, excluding id. Include all required non-id columns from the schema.\",\n },\n },\n required: [\"type\", \"dataset\", \"payload\"],\n additionalProperties: false,\n },\n {\n type: \"object\",\n properties: {\n type: {\n type: \"string\",\n enum: [\"item-updated\", \"item-deleted\"],\n description: \"Update or delete an existing synced row.\",\n },\n dataset: {\n type: \"string\",\n description: \"The synced dataset/table name from the schema documentation.\",\n },\n item_id: {\n type: \"string\",\n description: \"The stable id of the row being updated or deleted.\",\n },\n payload: {\n type: \"object\",\n additionalProperties: true,\n description: \"Changed column values for item-updated. Omit or pass {} for item-deleted.\",\n },\n },\n required: [\"type\", \"dataset\", \"item_id\"],\n additionalProperties: false,\n },\n ],\n },\n description: \"One or more CRDT mutation events to apply atomically.\",\n },\n },\n required: [\"events\"],\n additionalProperties: false,\n});\n\n/**\n * AI SDK tools for a synced database. `access` is a factory because acquiring the database\n * may itself be async per call (e.g. resolving a Durable Object stub from another DO).\n */\nexport function createDbTools(opts: CreateDbToolsOptions): ToolSet {\n const tools: ToolSet = {\n getDbSchema: tool({\n description:\n \"Get the schema documentation for the synced SQLite database: tables, columns, types, and data conventions. Call this before reasoning about the data.\",\n inputSchema: emptyInputSchema,\n execute: async () => {\n const access = await opts.access();\n return await access.getSchemaDoc();\n },\n }),\n queryDb: tool({\n description:\n \"Run a read-only SQL query against the synced SQLite database. Only a single SELECT/WITH/VALUES statement is allowed — anything that writes is rejected. Pass values as ? placeholders via `parameters` instead of inlining them as literals. Results are capped; ask for fewer columns or add LIMIT/WHERE if `truncated` is true.\",\n inputSchema: queryInputSchema,\n execute: async ({ sql, parameters }) => {\n const access = await opts.access();\n return await access.query({ sql, parameters });\n },\n }),\n };\n\n if (opts.mutations) {\n tools.mutateDb = tool({\n description:\n \"Apply one or more CRDT mutation events to the synced database. Use this for writes instead of SQL. Query the current data first when updating or deleting existing rows. Create events must omit ids: do not provide item_id or payload.id, because the tool generates ids and returns them. Create payloads must include all required non-id columns, update events should include only changed columns, and delete events should use an empty payload.\",\n inputSchema: mutationInputSchema,\n execute: async (input) => {\n const access = await opts.access();\n if (!access.mutate) {\n return { error: \"Database mutations are not enabled for this access object.\" } satisfies AiMutationResult;\n }\n return await access.mutate(input);\n },\n });\n }\n\n return tools;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqlite-sync/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "AI agent tools for @sqlite-sync databases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@sqlite-sync/core": "^0.
|
|
34
|
+
"@sqlite-sync/core": "^0.7.1",
|
|
35
35
|
"ai": "^6.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"tsup": "^8.3.5",
|
|
40
40
|
"typescript": "~6.0.3",
|
|
41
41
|
"vitest": "^4.1.8",
|
|
42
|
-
"@sqlite-sync/core": "0.
|
|
42
|
+
"@sqlite-sync/core": "0.7.1"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup",
|