@sqlite-sync/ai 0.9.1 → 0.9.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.
- package/README.md +13 -13
- package/dist/index.d.ts +25 -28
- package/dist/index.js +13 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @sqlite-sync/ai
|
|
2
2
|
|
|
3
|
-
AI agent tools for [@sqlite-sync](https://github.com/krolebord-dev/sqlite-sync) databases.
|
|
3
|
+
AI agent tools for [@sqlite-sync](https://github.com/krolebord-dev/sqlite-sync) databases. An AI SDK agent gets read-only access to a synced SQLite database by default, plus an opt-in CRDT mutation tool.
|
|
4
4
|
|
|
5
5
|
## What's included
|
|
6
6
|
|
|
7
|
-
- `createSchemaDoc`
|
|
8
|
-
- `createAiDbAccess`
|
|
9
|
-
- `createDbTools`
|
|
7
|
+
- `createSchemaDoc` builds a markdown schema doc from the declared sync schema. Structure comes from the `t.table()` builders; semantics come from `.describe()` and app-provided context. No database access needed.
|
|
8
|
+
- `createAiDbAccess` is the server-side access object that sits next to storage. Its methods are also the RPC contract for cross-Durable-Object setups.
|
|
9
|
+
- `createDbTools` builds an AI SDK v6 `ToolSet` (`getDbSchema`, `queryDb`, and optional `mutateDb`) from an `AiDbAccess` or a stub that proxies to one.
|
|
10
10
|
|
|
11
|
-
`queryDb`
|
|
11
|
+
`queryDb` accepts a single `SELECT`/`WITH`/`VALUES` statement. The guard checks SQLite `EXPLAIN` bytecode for write opcodes, then runs the statement in a transaction that always rolls back. By default reads are not restricted by table. The agent can query every table in the database file, including sqlite-sync's internal event log, so do not put secret data in the same file unless you hide it (see below). Results are capped (default 200 rows, 2000 chars per cell) and report `truncated` so the agent can narrow its query.
|
|
12
12
|
|
|
13
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
14
|
|
|
@@ -64,20 +64,20 @@ getTools() {
|
|
|
64
64
|
|
|
65
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.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
After your `overview`, the generated doc always adds a short preamble about sqlite-sync: the database syncs between devices, and the listed tables are read-only views with soft-deleted rows already filtered out. Put domain semantics on the schema with `.describe()`. Use `context.overview` for app-level notes.
|
|
68
68
|
|
|
69
69
|
## Limiting what the agent may touch
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Set `ai` on the table options. Tables are `"read-write"` unless narrowed:
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
74
|
const syncDbSchema = defineSyncSchema({
|
|
75
75
|
tables: {
|
|
76
76
|
todos: t.table({ title: t.text() }),
|
|
77
|
-
//
|
|
78
|
-
audit: t.table({ note: t.text() }
|
|
79
|
-
//
|
|
80
|
-
billing: t.table({ card_last4: t.text() }
|
|
77
|
+
// In the doc and queryable; mutateDb rejects writes.
|
|
78
|
+
audit: t.table({ note: t.text() }, { ai: "read-only" }),
|
|
79
|
+
// Omitted from the doc; queries that read it are rejected.
|
|
80
|
+
billing: t.table({ card_last4: t.text() }, { ai: "hidden" }),
|
|
81
81
|
},
|
|
82
82
|
migrations,
|
|
83
83
|
});
|
|
@@ -88,11 +88,11 @@ What each level enforces:
|
|
|
88
88
|
- `"read-only"` keeps the table in the schema doc, labelled read-only, and makes `mutateDb` reject events for it.
|
|
89
89
|
- `"hidden"` removes it from the doc and makes `queryDb` reject any statement that reads it. Enforcement works off the root pages a compiled statement opens, so views, aliases, CTEs, subqueries and quoting tricks all resolve to the same check.
|
|
90
90
|
|
|
91
|
-
Access is table-level. There is no per-column setting
|
|
91
|
+
Access is table-level. There is no per-column setting. SQLite bytecode names tables, not columns, so a column cannot be hidden from reads. Blocking writes to a required column would also stop the agent from creating rows. Put data the agent must not touch in its own table.
|
|
92
92
|
|
|
93
93
|
Hiding a table has two side effects worth knowing:
|
|
94
94
|
|
|
95
95
|
- Reads switch to an allow-list of the remaining base tables, so any other table in the same database file (including non-synced tables you created yourself) becomes unreadable too.
|
|
96
96
|
- `change_history` is dropped from the doc and denied. That view reads the raw event log, which holds every dataset's payloads, so it cannot be filtered per table.
|
|
97
97
|
|
|
98
|
-
Names and columns of hidden tables can still
|
|
98
|
+
Names and columns of hidden tables can still show up through SQLite schema introspection in principle. The guard rejects `pragma_*` table-valued functions and reads of `sqlite_schema` while restricted, but treat hiding as row-level protection, not proof the table does not exist.
|
package/dist/index.d.ts
CHANGED
|
@@ -5,17 +5,16 @@ type SchemaDocContext = {
|
|
|
5
5
|
overview?: string;
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* The internal `tombstone` column is omitted.
|
|
8
|
+
* Builds a markdown schema doc for an AI agent from the declared sync schema.
|
|
9
|
+
* No database access. Tables use the view names the agent queries. Descriptions come from
|
|
10
|
+
* `.describe()` on the table and column builders. The internal `tombstone` column is omitted.
|
|
12
11
|
*
|
|
13
|
-
* Tables
|
|
14
|
-
*
|
|
12
|
+
* Tables with `{ ai: "hidden" }` are left out. Read-only tables are labelled so the agent
|
|
13
|
+
* does not try a mutation that would be rejected.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
* views, soft-deletes already filtered)
|
|
18
|
-
*
|
|
15
|
+
* After the consumer's `overview`, the doc always adds a short preamble about sqlite-sync
|
|
16
|
+
* (read-only views, soft-deletes already filtered). Put domain notes in `overview` or
|
|
17
|
+
* `.describe()`.
|
|
19
18
|
*/
|
|
20
19
|
declare function createSchemaDoc(opts: {
|
|
21
20
|
syncDbSchema: SyncDbSchema;
|
|
@@ -28,9 +27,8 @@ type AiDbExecuteParams = {
|
|
|
28
27
|
parameters: readonly unknown[];
|
|
29
28
|
};
|
|
30
29
|
/**
|
|
31
|
-
* Minimal executor contract for AI database access.
|
|
32
|
-
*
|
|
33
|
-
* executor satisfies it.
|
|
30
|
+
* Minimal executor contract for AI database access. Inject the raw SQL capability that
|
|
31
|
+
* matches where the storage lives. A Cloudflare `ServerSyncDb`'s `unsafe` executor satisfies it.
|
|
34
32
|
*/
|
|
35
33
|
type AiDbExecutor = {
|
|
36
34
|
execute<TResult = unknown>(query: AiDbExecuteParams): {
|
|
@@ -72,18 +70,17 @@ type AiMutationResult = {
|
|
|
72
70
|
errors?: string[];
|
|
73
71
|
};
|
|
74
72
|
/**
|
|
75
|
-
* AI access to a synced database. Lives
|
|
76
|
-
*
|
|
77
|
-
* (promise-wrapped) and satisfies the tool layer's `DbToolsAccess`.
|
|
73
|
+
* AI access to a synced database. Lives next to the storage. Method names are the RPC
|
|
74
|
+
* contract: a DO stub that proxies them (promise-wrapped) satisfies `DbToolsAccess`.
|
|
78
75
|
*
|
|
79
|
-
* `query`
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
76
|
+
* `query` is read-only: one SELECT/WITH/VALUES statement, no write opcodes, run in a
|
|
77
|
+
* forced-rollback transaction. Table-level read limits apply only after the schema sets
|
|
78
|
+
* `{ ai: "hidden" }` on a table. Until then the whole database file is in scope, so do not
|
|
79
|
+
* put secret data in the same file.
|
|
83
80
|
*
|
|
84
|
-
* `mutate`
|
|
85
|
-
* events
|
|
86
|
-
*
|
|
81
|
+
* `mutate` exists only when `createAiDbAccess` gets a CRDT storage. Mutations are CRDT
|
|
82
|
+
* events on the normal own-event path, never direct SQL writes. Tables marked read-only or
|
|
83
|
+
* hidden are rejected.
|
|
87
84
|
*/
|
|
88
85
|
type AiDbAccess = {
|
|
89
86
|
getSchemaDoc(): string;
|
|
@@ -144,15 +141,15 @@ declare class QueryGuardError extends Error {
|
|
|
144
141
|
}
|
|
145
142
|
type QueryGuard = {
|
|
146
143
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* reads are unrestricted: the whole database file is in scope
|
|
150
|
-
*
|
|
144
|
+
* Checks that the query is a read-only single statement. When the guard was created with
|
|
145
|
+
* `readableTables`, also checks that it only reads those tables. Without `readableTables`,
|
|
146
|
+
* reads are unrestricted: the whole database file is in scope, so do not put secret data
|
|
147
|
+
* in the same file.
|
|
151
148
|
*/
|
|
152
149
|
check(input: QueryGuardInput): QueryGuardVerdict;
|
|
153
150
|
/**
|
|
154
|
-
* `check
|
|
155
|
-
*
|
|
151
|
+
* Runs `check`, then executes inside a forced-rollback transaction (backstop for anything
|
|
152
|
+
* the analysis might miss). Throws {@link QueryGuardError} when the check rejects.
|
|
156
153
|
*/
|
|
157
154
|
execute<TResult = unknown>(input: QueryGuardInput): {
|
|
158
155
|
rows: TResult[];
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ var QueryGuardError = class extends Error {
|
|
|
41
41
|
* real-table mutation opens its cursor through it), `Clear`/`Destroy` cover whole-table
|
|
42
42
|
* deletes that skip cursors (truncate optimization), the rest cover DDL, pragma-class
|
|
43
43
|
* statements (which a transaction rollback would NOT undo), virtual-table writes, and trigger
|
|
44
|
-
* subprograms. `Insert`/`Delete`/`IdxInsert` are deliberately absent
|
|
44
|
+
* subprograms. `Insert`/`Delete`/`IdxInsert` are deliberately absent: they also run against
|
|
45
45
|
* ephemeral/sorter cursors in ordinary SELECTs (DISTINCT, ORDER BY) and are only dangerous on
|
|
46
46
|
* a cursor an `OpenWrite` would have created.
|
|
47
47
|
*/
|
|
@@ -71,7 +71,7 @@ const WRITE_OPCODES = /* @__PURE__ */ new Set([
|
|
|
71
71
|
/**
|
|
72
72
|
* Opcodes that open a b-tree cursor for reading. `p2` is the root page of the table or index
|
|
73
73
|
* and `p3` the database it lives in (0 = main), which is how a statement's real table set is
|
|
74
|
-
* recovered
|
|
74
|
+
* recovered. Views are already flattened into their base tables by the time bytecode exists,
|
|
75
75
|
* so aliases, CTEs, subqueries and quoting tricks all resolve here.
|
|
76
76
|
*/
|
|
77
77
|
const READ_CURSOR_OPCODES = /* @__PURE__ */ new Set(["OpenRead", "ReopenIdx"]);
|
|
@@ -183,7 +183,7 @@ function runWithForcedRollback(executor, callback) {
|
|
|
183
183
|
//#endregion
|
|
184
184
|
//#region src/schema-doc.ts
|
|
185
185
|
const SCHEMA_DOC_PREAMBLE = [
|
|
186
|
-
"This is a synced SQLite database
|
|
186
|
+
"This is a synced SQLite database. Data replicates automatically between the user's devices.",
|
|
187
187
|
"All writes go through a sync event log, which is why the tables listed below are exposed as",
|
|
188
188
|
"read-only SQL views; soft-deleted rows are already filtered out, so query them directly",
|
|
189
189
|
"without any tombstone filtering. Every table has a unique `id` text primary key."
|
|
@@ -197,17 +197,16 @@ function renderColumn(name, meta) {
|
|
|
197
197
|
return meta.description ? `${line} — ${meta.description}` : line;
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* The internal `tombstone` column is omitted.
|
|
200
|
+
* Builds a markdown schema doc for an AI agent from the declared sync schema.
|
|
201
|
+
* No database access. Tables use the view names the agent queries. Descriptions come from
|
|
202
|
+
* `.describe()` on the table and column builders. The internal `tombstone` column is omitted.
|
|
204
203
|
*
|
|
205
|
-
* Tables
|
|
206
|
-
*
|
|
204
|
+
* Tables with `{ ai: "hidden" }` are left out. Read-only tables are labelled so the agent
|
|
205
|
+
* does not try a mutation that would be rejected.
|
|
207
206
|
*
|
|
208
|
-
*
|
|
209
|
-
* views, soft-deletes already filtered)
|
|
210
|
-
*
|
|
207
|
+
* After the consumer's `overview`, the doc always adds a short preamble about sqlite-sync
|
|
208
|
+
* (read-only views, soft-deletes already filtered). Put domain notes in `overview` or
|
|
209
|
+
* `.describe()`.
|
|
211
210
|
*/
|
|
212
211
|
function createSchemaDoc(opts) {
|
|
213
212
|
const policy = resolveAiPolicy({ syncDbSchema: opts.syncDbSchema });
|
|
@@ -237,7 +236,7 @@ const CHANGE_HISTORY_DOC = [
|
|
|
237
236
|
"",
|
|
238
237
|
"A read-only, append-only log of every change across all tables, one row per sync event.",
|
|
239
238
|
"Query it like any other view. Unlike the tables above, soft-delete filtering does NOT apply",
|
|
240
|
-
"here
|
|
239
|
+
"here. It includes changes to items that were later deleted, so treat it as an audit log.",
|
|
241
240
|
"",
|
|
242
241
|
"Columns:",
|
|
243
242
|
"- `seq` INTEGER NOT NULL — monotonic sequence number; **order history by this** (ascending = oldest first)",
|
|
@@ -468,7 +467,7 @@ function createDbTools(opts) {
|
|
|
468
467
|
}
|
|
469
468
|
}),
|
|
470
469
|
queryDb: tool({
|
|
471
|
-
description: "Run a read-only SQL query against the synced SQLite database. Only a single SELECT/WITH/VALUES statement is allowed
|
|
470
|
+
description: "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.",
|
|
472
471
|
inputSchema: queryInputSchema,
|
|
473
472
|
execute: async ({ sql, parameters }) => {
|
|
474
473
|
return await (await opts.access()).query({
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/policy.ts","../src/query-guard.ts","../src/schema-doc.ts","../src/db-access.ts","../src/tools.ts"],"sourcesContent":["import type { AiAccess, SyncDbSchema } from \"@sqlite-sync/core\";\n\nexport type ResolvedTablePolicy = {\n crdtTableName: string;\n baseTableName: string;\n access: AiAccess;\n};\n\nexport type ResolvedAiPolicy = {\n /** In schema declaration order, hidden tables included. */\n tables: ResolvedTablePolicy[];\n hasHiddenTables: boolean;\n /** Base table names the agent may read. Only meaningful when `hasHiddenTables` is true. */\n readableBaseTableNames: string[];\n /** Resolves either the crdt or the base table name; anything unknown is \"hidden\". */\n tableAccess(dataset: string): AiAccess;\n};\n\n/**\n * Flattens the AI access declared on the schema's table builders into the lookups the doc\n * generator and the enforcement points share, so they cannot disagree. Access is table-level:\n * see {@link AiAccess}.\n */\nexport function resolveAiPolicy(opts: { syncDbSchema: SyncDbSchema }): ResolvedAiPolicy {\n const tables: ResolvedTablePolicy[] = opts.syncDbSchema.tablesConfig.map(({ crdtTableName, baseTableName }) => ({\n crdtTableName,\n baseTableName,\n access: opts.syncDbSchema.tables[crdtTableName].aiAccess,\n }));\n\n // CRDT events address a table by either name, so both resolve to the same policy.\n const byName = new Map<string, ResolvedTablePolicy>();\n for (const table of tables) {\n byName.set(table.crdtTableName, table);\n byName.set(table.baseTableName, table);\n }\n\n return {\n tables,\n hasHiddenTables: tables.some((table) => table.access === \"hidden\"),\n readableBaseTableNames: tables.filter((table) => table.access !== \"hidden\").map((table) => table.baseTableName),\n tableAccess(dataset) {\n return byName.get(dataset)?.access ?? \"hidden\";\n },\n };\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\" | \"table-denied\";\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, and — when the guard was\n * created with `readableTables` — that it only reads those tables. Without `readableTables`\n * reads are unrestricted: the whole database file is in scope for the agent, so don't\n * colocate data the 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\n/**\n * Opcodes that open a b-tree cursor for reading. `p2` is the root page of the table or index\n * and `p3` the database it lives in (0 = main), which is how a statement's real table set is\n * recovered — views are already flattened into their base tables by the time bytecode exists,\n * so aliases, CTEs, subqueries and quoting tricks all resolve here.\n */\nconst READ_CURSOR_OPCODES = new Set([\"OpenRead\", \"ReopenIdx\"]);\n\ntype ExplainRow = {\n opcode: string;\n p2: number | string;\n p3: number | string;\n};\n\n/**\n * Table-valued pragma functions (`pragma_table_list()`, ...) read the schema through a virtual\n * table, so no root page identifies them. They expose names and columns rather than rows, but a\n * hidden table should not be enumerable either, so they are rejected outright when restricted.\n */\nconst PRAGMA_FUNCTION_PATTERN = /\\bpragma_[a-z_]+\\s*\\(/i;\n\nconst READ_STATEMENT_PREFIXES = [\"select\", \"with\", \"values\"];\n\nexport function createQueryGuard(opts: {\n executor: AiDbExecutor;\n /**\n * Restrict reads to these tables (matched case-insensitively, indexes on them included).\n * Views are resolved through their base tables, so pass base table names. Omit for\n * unrestricted reads.\n */\n readableTables?: readonly string[];\n}): QueryGuard {\n const readableTables = opts.readableTables ? new Set(opts.readableTables.map((name) => name.toLowerCase())) : null;\n const readableTablesLabel = opts.readableTables?.join(\", \") ?? \"\";\n\n /**\n * Root page to table name, for every b-tree in the main database. Re-read per query instead of\n * cached: a migration can drop a table and hand its root page to another one, and a stale map\n * would then resolve that page to the wrong (possibly allowed) name.\n */\n function readRootPages(): Map<number, string> {\n const rows = opts.executor.execute<{ tbl_name: string; rootpage: number }>({\n sql: `SELECT \"tbl_name\", \"rootpage\" FROM \"sqlite_master\" WHERE \"rootpage\" IS NOT NULL AND \"rootpage\" > 0`,\n parameters: [],\n }).rows;\n return new Map(rows.map((row) => [Number(row.rootpage), row.tbl_name]));\n }\n\n function checkReadableTables(sql: string, operations: ExplainRow[]): QueryGuardRejection | null {\n if (!readableTables) {\n return null;\n }\n\n if (PRAGMA_FUNCTION_PATTERN.test(sql)) {\n return reject(\n \"table-denied\",\n `Pragma functions are not available. You can only read these tables: ${readableTablesLabel}.`,\n );\n }\n\n const cursors = operations.filter((operation) => READ_CURSOR_OPCODES.has(operation.opcode));\n if (cursors.length === 0) {\n return null;\n }\n\n let rootPages: Map<number, string>;\n try {\n rootPages = readRootPages();\n } catch {\n // Fail closed: without the schema mapping we cannot tell which tables the statement reads.\n return reject(\"table-denied\", \"The tables this statement reads could not be verified, so it was rejected.\");\n }\n\n for (const cursor of cursors) {\n // Only the main database is mapped; nothing the agent can legitimately read lives elsewhere.\n const name = Number(cursor.p3) === 0 ? rootPages.get(Number(cursor.p2)) : undefined;\n if (!name || !readableTables.has(name.toLowerCase())) {\n const subject = name ? `Table \"${name}\"` : \"A table this statement reads\";\n return reject(\n \"table-denied\",\n `${subject} is not available to you. You can only read these tables: ${readableTablesLabel}.`,\n );\n }\n }\n\n return null;\n }\n\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 const denied = checkReadableTables(body, operations);\n if (denied) {\n return denied;\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\";\nimport { resolveAiPolicy } from \"./policy\";\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\n// Added when the schema hides a table, because then reads are restricted to an allow-list and a\n// query touching anything else fails. Worth spending tokens on to save the agent a wasted turn.\nconst RESTRICTED_READS_NOTE = [\n \"Only the tables documented below are readable. Queries that touch any other table are\",\n \"rejected, including the internal sync event log, so there is no change history available.\",\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 * Tables declared `.ai(\"hidden\")` are left out entirely, and read-only ones are labelled so the\n * agent does not attempt a mutation that would be rejected.\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 policy = resolveAiPolicy({ syncDbSchema: opts.syncDbSchema });\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 if (policy.hasHiddenTables) {\n sections.push(RESTRICTED_READS_NOTE);\n }\n\n for (const [crdtTableName, table] of Object.entries(opts.syncDbSchema.tables)) {\n const access = policy.tableAccess(crdtTableName);\n if (access === \"hidden\") continue;\n\n const lines = [`## ${crdtTableName}`];\n if (table.description) {\n lines.push(\"\", table.description.trim());\n }\n if (access === \"read-only\") {\n lines.push(\"\", \"Read-only: you can query this table but cannot create, update, or delete its rows.\");\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 if (!policy.hasHiddenTables) {\n sections.push(CHANGE_HISTORY_DOC);\n }\n\n return sections.join(\"\\n\\n\");\n}\n\n// Documents the curated `change_history` view created over the sync event log. Unlike the table\n// views above, it is NOT tombstone-filtered — it intentionally surfaces the full change log,\n// including the contents of since-deleted items. Omitted when any table is hidden: the view reads\n// the raw event log, which holds every dataset's payloads, so it cannot be filtered per table.\nconst CHANGE_HISTORY_DOC = [\n \"## change_history\",\n \"\",\n \"A read-only, append-only log of every change across all tables, one row per sync event.\",\n \"Query it like any other view. Unlike the tables above, soft-delete filtering does NOT apply\",\n \"here — it includes changes to items that were later deleted, so treat it as an audit log.\",\n \"\",\n \"Columns:\",\n \"- `seq` INTEGER NOT NULL — monotonic sequence number; **order history by this** (ascending = oldest first)\",\n \"- `dataset` TEXT NOT NULL — which table the change applies to (matches a table name above)\",\n \"- `item_id` TEXT NOT NULL — the `id` of the affected row\",\n '- `change_type` TEXT NOT NULL (one of \"item-created\" | \"item-updated\" | \"item-deleted\")',\n '- `status` TEXT NOT NULL (one of \"applied\" | \"pending\" | \"failed\" | \"deduped\") — \"applied\" took effect; \"failed\" did not; filter to `status = \\'applied\\'` for the effective history',\n '- `origin` TEXT NOT NULL — where the change came from (e.g. \"own\", \"remote\", \"local\")',\n \"- `timestamp` TEXT NOT NULL — opaque hybrid-logical-clock string; do NOT parse it as a date, order by `seq` instead\",\n \"- `changes` TEXT NOT NULL — JSON of what the change set: the changed columns only for item-updated, the full initial row for item-created, `{}` for item-deleted. Use `json_extract(changes, '$.column')` to read a field.\",\n].join(\"\\n\");\n","import {\n type CrdtEventType,\n CrdtEventValidationError,\n type CrdtStorage,\n generateId,\n type OwnCrdtEvent,\n type SyncDbSchema,\n} from \"@sqlite-sync/core\";\nimport { resolveAiPolicy } from \"./policy\";\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 raw SQL\n * capability that matches where the storage lives; a Cloudflare `ServerSyncDb`'s `unsafe`\n * executor 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). Reads are restricted by table only once the schema declares\n * a table `.ai(\"hidden\")`; until then the whole database file is in scope for the agent, so don't\n * 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, and are\n * rejected for tables the schema declares read-only or hidden.\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 policy = resolveAiPolicy({ syncDbSchema: opts.syncDbSchema });\n const schemaDoc = createSchemaDoc({ syncDbSchema: opts.syncDbSchema, context: opts.context });\n // A hidden table flips reads to an allow-list, which also cuts off the event log (it holds every\n // dataset's payloads) and any non-synced table sharing the database file.\n const guard = createQueryGuard({\n executor: opts.executor,\n readableTables: policy.hasHiddenTables ? policy.readableBaseTableNames : undefined,\n });\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 const tableAccess = policy.tableAccess(event.dataset);\n if (tableAccess === \"hidden\") {\n errors.push(`[${index}] unknown or unavailable dataset \"${event.dataset}\"`);\n continue;\n }\n if (tableAccess === \"read-only\") {\n errors.push(`[${index}] dataset \"${event.dataset}\" is read-only and cannot be modified`);\n continue;\n }\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 { 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. Tables the schema documentation marks read-only cannot be written.\",\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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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/policy.ts","../src/query-guard.ts","../src/schema-doc.ts","../src/db-access.ts","../src/tools.ts"],"sourcesContent":["import type { AiAccess, SyncDbSchema } from \"@sqlite-sync/core\";\n\nexport type ResolvedTablePolicy = {\n crdtTableName: string;\n baseTableName: string;\n access: AiAccess;\n};\n\nexport type ResolvedAiPolicy = {\n /** In schema declaration order, hidden tables included. */\n tables: ResolvedTablePolicy[];\n hasHiddenTables: boolean;\n /** Base table names the agent may read. Only meaningful when `hasHiddenTables` is true. */\n readableBaseTableNames: string[];\n /** Resolves either the crdt or the base table name; anything unknown is \"hidden\". */\n tableAccess(dataset: string): AiAccess;\n};\n\n/**\n * Flattens the AI access declared on the schema's table builders into the lookups the doc\n * generator and the enforcement points share, so they cannot disagree. Access is table-level:\n * see {@link AiAccess}.\n */\nexport function resolveAiPolicy(opts: { syncDbSchema: SyncDbSchema }): ResolvedAiPolicy {\n const tables: ResolvedTablePolicy[] = opts.syncDbSchema.tablesConfig.map(({ crdtTableName, baseTableName }) => ({\n crdtTableName,\n baseTableName,\n access: opts.syncDbSchema.tables[crdtTableName].aiAccess,\n }));\n\n // CRDT events address a table by either name, so both resolve to the same policy.\n const byName = new Map<string, ResolvedTablePolicy>();\n for (const table of tables) {\n byName.set(table.crdtTableName, table);\n byName.set(table.baseTableName, table);\n }\n\n return {\n tables,\n hasHiddenTables: tables.some((table) => table.access === \"hidden\"),\n readableBaseTableNames: tables.filter((table) => table.access !== \"hidden\").map((table) => table.baseTableName),\n tableAccess(dataset) {\n return byName.get(dataset)?.access ?? \"hidden\";\n },\n };\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\" | \"table-denied\";\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 * Checks that the query is a read-only single statement. When the guard was created with\n * `readableTables`, also checks that it only reads those tables. Without `readableTables`,\n * reads are unrestricted: the whole database file is in scope, so do not put secret data\n * in the same file.\n */\n check(input: QueryGuardInput): QueryGuardVerdict;\n /**\n * Runs `check`, then executes inside a forced-rollback transaction (backstop for anything\n * 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\n/**\n * Opcodes that open a b-tree cursor for reading. `p2` is the root page of the table or index\n * and `p3` the database it lives in (0 = main), which is how a statement's real table set is\n * recovered. Views are already flattened into their base tables by the time bytecode exists,\n * so aliases, CTEs, subqueries and quoting tricks all resolve here.\n */\nconst READ_CURSOR_OPCODES = new Set([\"OpenRead\", \"ReopenIdx\"]);\n\ntype ExplainRow = {\n opcode: string;\n p2: number | string;\n p3: number | string;\n};\n\n/**\n * Table-valued pragma functions (`pragma_table_list()`, ...) read the schema through a virtual\n * table, so no root page identifies them. They expose names and columns rather than rows, but a\n * hidden table should not be enumerable either, so they are rejected outright when restricted.\n */\nconst PRAGMA_FUNCTION_PATTERN = /\\bpragma_[a-z_]+\\s*\\(/i;\n\nconst READ_STATEMENT_PREFIXES = [\"select\", \"with\", \"values\"];\n\nexport function createQueryGuard(opts: {\n executor: AiDbExecutor;\n /**\n * Restrict reads to these tables (matched case-insensitively, indexes on them included).\n * Views are resolved through their base tables, so pass base table names. Omit for\n * unrestricted reads.\n */\n readableTables?: readonly string[];\n}): QueryGuard {\n const readableTables = opts.readableTables ? new Set(opts.readableTables.map((name) => name.toLowerCase())) : null;\n const readableTablesLabel = opts.readableTables?.join(\", \") ?? \"\";\n\n /**\n * Root page to table name, for every b-tree in the main database. Re-read per query instead of\n * cached: a migration can drop a table and hand its root page to another one, and a stale map\n * would then resolve that page to the wrong (possibly allowed) name.\n */\n function readRootPages(): Map<number, string> {\n const rows = opts.executor.execute<{ tbl_name: string; rootpage: number }>({\n sql: `SELECT \"tbl_name\", \"rootpage\" FROM \"sqlite_master\" WHERE \"rootpage\" IS NOT NULL AND \"rootpage\" > 0`,\n parameters: [],\n }).rows;\n return new Map(rows.map((row) => [Number(row.rootpage), row.tbl_name]));\n }\n\n function checkReadableTables(sql: string, operations: ExplainRow[]): QueryGuardRejection | null {\n if (!readableTables) {\n return null;\n }\n\n if (PRAGMA_FUNCTION_PATTERN.test(sql)) {\n return reject(\n \"table-denied\",\n `Pragma functions are not available. You can only read these tables: ${readableTablesLabel}.`,\n );\n }\n\n const cursors = operations.filter((operation) => READ_CURSOR_OPCODES.has(operation.opcode));\n if (cursors.length === 0) {\n return null;\n }\n\n let rootPages: Map<number, string>;\n try {\n rootPages = readRootPages();\n } catch {\n // Fail closed: without the schema mapping we cannot tell which tables the statement reads.\n return reject(\"table-denied\", \"The tables this statement reads could not be verified, so it was rejected.\");\n }\n\n for (const cursor of cursors) {\n // Only the main database is mapped; nothing the agent can legitimately read lives elsewhere.\n const name = Number(cursor.p3) === 0 ? rootPages.get(Number(cursor.p2)) : undefined;\n if (!name || !readableTables.has(name.toLowerCase())) {\n const subject = name ? `Table \"${name}\"` : \"A table this statement reads\";\n return reject(\n \"table-denied\",\n `${subject} is not available to you. You can only read these tables: ${readableTablesLabel}.`,\n );\n }\n }\n\n return null;\n }\n\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 const denied = checkReadableTables(body, operations);\n if (denied) {\n return denied;\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\";\nimport { resolveAiPolicy } from \"./policy\";\n\nexport type SchemaDocContext = {\n overview?: string;\n};\n\n// Mechanics every generated doc should explain, so `context` only needs domain notes.\n// Skips 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\n// Added when any table is hidden: reads become an allow-list, and anything else fails.\nconst RESTRICTED_READS_NOTE = [\n \"Only the tables documented below are readable. Queries that touch any other table are\",\n \"rejected, including the internal sync event log, so there is no change history available.\",\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 * Builds a markdown schema doc for an AI agent from the declared sync schema.\n * No database access. Tables use the view names the agent queries. Descriptions come from\n * `.describe()` on the table and column builders. The internal `tombstone` column is omitted.\n *\n * Tables with `{ ai: \"hidden\" }` are left out. Read-only tables are labelled so the agent\n * does not try a mutation that would be rejected.\n *\n * After the consumer's `overview`, the doc always adds a short preamble about sqlite-sync\n * (read-only views, soft-deletes already filtered). Put domain notes in `overview` or\n * `.describe()`.\n */\nexport function createSchemaDoc(opts: { syncDbSchema: SyncDbSchema; context?: SchemaDocContext }): string {\n const policy = resolveAiPolicy({ syncDbSchema: opts.syncDbSchema });\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 if (policy.hasHiddenTables) {\n sections.push(RESTRICTED_READS_NOTE);\n }\n\n for (const [crdtTableName, table] of Object.entries(opts.syncDbSchema.tables)) {\n const access = policy.tableAccess(crdtTableName);\n if (access === \"hidden\") continue;\n\n const lines = [`## ${crdtTableName}`];\n if (table.description) {\n lines.push(\"\", table.description.trim());\n }\n if (access === \"read-only\") {\n lines.push(\"\", \"Read-only: you can query this table but cannot create, update, or delete its rows.\");\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 if (!policy.hasHiddenTables) {\n sections.push(CHANGE_HISTORY_DOC);\n }\n\n return sections.join(\"\\n\\n\");\n}\n\n// Documents the curated `change_history` view over the sync event log. Unlike the table\n// views above, it is NOT tombstone-filtered: it shows the full change log, including\n// since-deleted items. Omitted when any table is hidden, because the view reads the raw\n// event log (every dataset's payloads) and cannot be filtered per table.\nconst CHANGE_HISTORY_DOC = [\n \"## change_history\",\n \"\",\n \"A read-only, append-only log of every change across all tables, one row per sync event.\",\n \"Query it like any other view. Unlike the tables above, soft-delete filtering does NOT apply\",\n \"here. It includes changes to items that were later deleted, so treat it as an audit log.\",\n \"\",\n \"Columns:\",\n \"- `seq` INTEGER NOT NULL — monotonic sequence number; **order history by this** (ascending = oldest first)\",\n \"- `dataset` TEXT NOT NULL — which table the change applies to (matches a table name above)\",\n \"- `item_id` TEXT NOT NULL — the `id` of the affected row\",\n '- `change_type` TEXT NOT NULL (one of \"item-created\" | \"item-updated\" | \"item-deleted\")',\n '- `status` TEXT NOT NULL (one of \"applied\" | \"pending\" | \"failed\" | \"deduped\") — \"applied\" took effect; \"failed\" did not; filter to `status = \\'applied\\'` for the effective history',\n '- `origin` TEXT NOT NULL — where the change came from (e.g. \"own\", \"remote\", \"local\")',\n \"- `timestamp` TEXT NOT NULL — opaque hybrid-logical-clock string; do NOT parse it as a date, order by `seq` instead\",\n \"- `changes` TEXT NOT NULL — JSON of what the change set: the changed columns only for item-updated, the full initial row for item-created, `{}` for item-deleted. Use `json_extract(changes, '$.column')` to read a field.\",\n].join(\"\\n\");\n","import {\n type CrdtEventType,\n CrdtEventValidationError,\n type CrdtStorage,\n generateId,\n type OwnCrdtEvent,\n type SyncDbSchema,\n} from \"@sqlite-sync/core\";\nimport { resolveAiPolicy } from \"./policy\";\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. Inject the raw SQL capability that\n * matches where the storage lives. A Cloudflare `ServerSyncDb`'s `unsafe` executor 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 next to the storage. Method names are the RPC\n * contract: a DO stub that proxies them (promise-wrapped) satisfies `DbToolsAccess`.\n *\n * `query` is read-only: one SELECT/WITH/VALUES statement, no write opcodes, run in a\n * forced-rollback transaction. Table-level read limits apply only after the schema sets\n * `{ ai: \"hidden\" }` on a table. Until then the whole database file is in scope, so do not\n * put secret data in the same file.\n *\n * `mutate` exists only when `createAiDbAccess` gets a CRDT storage. Mutations are CRDT\n * events on the normal own-event path, never direct SQL writes. Tables marked read-only or\n * hidden are rejected.\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 policy = resolveAiPolicy({ syncDbSchema: opts.syncDbSchema });\n const schemaDoc = createSchemaDoc({ syncDbSchema: opts.syncDbSchema, context: opts.context });\n // A hidden table flips reads to an allow-list, which also cuts off the event log (it holds\n // every dataset's payloads) and any non-synced table sharing the database file.\n const guard = createQueryGuard({\n executor: opts.executor,\n readableTables: policy.hasHiddenTables ? policy.readableBaseTableNames : undefined,\n });\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 const tableAccess = policy.tableAccess(event.dataset);\n if (tableAccess === \"hidden\") {\n errors.push(`[${index}] unknown or unavailable dataset \"${event.dataset}\"`);\n continue;\n }\n if (tableAccess === \"read-only\") {\n errors.push(`[${index}] dataset \"${event.dataset}\" is read-only and cannot be modified`);\n continue;\n }\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 { 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. Tables the schema documentation marks read-only cannot be written.\",\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqlite-sync/ai",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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.9.
|
|
34
|
+
"@sqlite-sync/core": "^0.9.3",
|
|
35
35
|
"ai": "^6.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"tsdown": "0.22.14",
|
|
40
40
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
41
41
|
"vitest": "^4.1.8",
|
|
42
|
-
"@sqlite-sync/core": "0.9.
|
|
42
|
+
"@sqlite-sync/core": "0.9.3"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsdown",
|