@fadhilp/stateql 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,28 @@
1
1
  # StateQL
2
2
 
3
- StateQL is a stateful database CLI for AI agents and automation. It provides a
4
- safe interface for querying, changing, and inspecting databases while keeping
5
- results reusable and operations traceable across commands.
3
+ StateQL is a stateful database CLI and TypeScript library for AI agents and
4
+ automation. It provides a safe interface for querying, changing, and inspecting
5
+ SQLite, PostgreSQL, and MySQL databases while keeping results reusable and
6
+ operations traceable across commands.
7
+
8
+ StateQL is built around durable handles:
9
+
10
+ 1. Run a query and receive a result handle such as `q_1`.
11
+ 2. Reuse, filter, page, count, alias, or export that stored result without
12
+ rerunning the original SQL.
13
+ 3. Use operation, plan, and transaction handles to inspect and control writes.
6
14
 
7
15
  Requires Node.js 22.5 or newer.
8
16
 
9
17
  ## Quick start
10
18
 
19
+ Install the CLI:
20
+
11
21
  ```bash
12
22
  npm install -g @fadhilp/stateql
13
23
  ```
14
24
 
15
- Connect to an existing SQLite database, then run a filtered, parameterized
16
- query. Parameters keep values separate from SQL; `ORDER BY` makes paging
17
- stable, while `LIMIT` bounds work at the database.
25
+ Connect to an existing SQLite database and run a bounded, parameterized query:
18
26
 
19
27
  ```bash
20
28
  export STQL_SESSION=audit
@@ -27,14 +35,16 @@ stql query \
27
35
  --param 2026-01-01
28
36
  ```
29
37
 
30
- Example output:
38
+ Parameters keep values separate from SQL. `ORDER BY` makes paging stable, and
39
+ `LIMIT` bounds work at the database. The default `agent` output is compact,
40
+ one-line JSON:
31
41
 
32
42
  ```json
33
43
  {"ok":true,"handle":"q_1","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"},{"id":18,"name":"Linus","email":"linus@kernel.org"}],"truncated":false,"cached":false,"total":3,"next_offset":null}
34
44
  ```
35
45
 
36
- `q_1` is a durable result handle. Filter its stored snapshot without querying
37
- the original database; the result becomes another durable handle:
46
+ `q_1` is a durable snapshot. Filter it locally without accessing the original
47
+ database:
38
48
 
39
49
  ```bash
40
50
  stql filter q_1 "email LIKE ?" --param "%@example.com"
@@ -44,8 +54,8 @@ stql filter q_1 "email LIKE ?" --param "%@example.com"
44
54
  {"ok":true,"handle":"q_2","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"}],"truncated":false,"cached":false,"total":2,"next_offset":null}
45
55
  ```
46
56
 
47
- Give the derived result a readable alias, page through it, inspect its count,
48
- or export it—all without rerunning SQL:
57
+ The filtered snapshot receives its own handle. Give it a readable alias, page
58
+ through it, inspect its count, or export it without rerunning SQL:
49
59
 
50
60
  ```bash
51
61
  stql alias set example-users q_2
@@ -62,7 +72,22 @@ Example first page:
62
72
  ```
63
73
 
64
74
  Running the same normalized query with the same parameters reuses `q_1` while
65
- its cache is valid. Use `--cache bypass` when a fresh read is required.
75
+ its cache entry is valid. Use `--cache bypass` when a fresh read is required.
76
+
77
+ ## Connections and profiles
78
+
79
+ A connection accepts exactly one source: a direct target, `--env`, or
80
+ `--profile`.
81
+
82
+ ```bash
83
+ stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
84
+ stql connect --env ENV [--name NAME] [--read-write]
85
+ stql connect --profile NAME
86
+ stql disconnect
87
+ stql status
88
+ ```
89
+
90
+ ### Environment-backed credentials
66
91
 
67
92
  PostgreSQL and MySQL credentials should come from environment variables. The
68
93
  variable must contain the complete connection URL, not only its password.
@@ -79,21 +104,45 @@ export SQLITE_DATABASE='sqlite:./app.sqlite'
79
104
  stql connect --env SQLITE_DATABASE --name local --read-only
80
105
  ```
81
106
 
82
- A connection accepts exactly one direct target, `--env`, or `--profile` source.
83
- For PostgreSQL, StateQL preserves strict TLS verification by normalizing
84
- `sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening the
85
- adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
86
- `uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
107
+ StateQL stores no PostgreSQL or MySQL password. Credential-bearing URLs must be
108
+ supplied through `--env`. SQLite paths remain persisted as connection metadata.
109
+
110
+ ### Local profiles
111
+
112
+ Profiles store connection targets, read-only policy, and environment-variable
113
+ names. Credential values are never stored. Profiles persist under `STQL_HOME`
114
+ with other StateQL metadata.
115
+
116
+ ```bash
117
+ stql profile add local ./app.sqlite --read-write
118
+ stql profile add production --env PROD_DATABASE_URL --read-only
119
+ stql profile list
120
+ stql profile show production
121
+ stql connect local
122
+ stql connect --profile production
123
+ ```
124
+
125
+ A bare connection target matching a profile name resolves to that profile;
126
+ otherwise it remains a path or database URL.
127
+
128
+ ### Driver notes
87
129
 
88
- MySQL uses positional `?` parameters. MariaDB compatibility is not currently
89
- claimed.
130
+ - **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
131
+ environment-backed path.
132
+ - **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
133
+ `sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
134
+ the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
135
+ `uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
136
+ - **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
137
+ currently claimed.
90
138
 
91
- ## Commands
139
+ ## CLI reference
92
140
 
93
141
  ```text
94
142
  stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
95
143
  stql connect --env ENV [--name NAME] [--read-write]
96
144
  stql connect --profile NAME
145
+ stql disconnect
97
146
  stql status
98
147
  stql profile add|list|show|remove
99
148
  stql session start|list|show|summary|close
@@ -103,6 +152,7 @@ stql exec <sql> [--params JSON | --param VALUE...] [--idempotency-key KEY] [--re
103
152
  [--allow-unbounded] [--allow-destructive]
104
153
  stql show|count|columns <result-handle>
105
154
  stql rows <result-handle> [--offset N] [--limit N]
155
+ stql alias set <name> <result-handle>
106
156
  stql export <result-handle> --output FILE [--format json|jsonl|csv]
107
157
  stql inspect schema|table|columns|indexes|constraints [table]
108
158
  stql transaction begin|status|commit|rollback [--isolation LEVEL]
@@ -110,61 +160,149 @@ stql plan <sql> [--allow-unbounded] [--allow-destructive]
110
160
  stql apply <plan-handle>
111
161
  stql history [--limit N]
112
162
  stql receipt <operation-handle>
163
+ stql doctor
164
+ stql purge [expired|results|history|all]
113
165
  stql capabilities
114
166
  stql batch [commands.json|commands.jsonl|-] [--continue-on-error]
115
167
  stql pipe [--continue-on-error]
116
168
  ```
117
169
 
118
- Database commands accept `--timeout-ms N`; default is 30,000 ms. `Ctrl+C`
119
- cancels active work. SQLite runs in a killable child process so long synchronous
120
- statements cannot block StateQL's event loop. PostgreSQL uses server-side
121
- `statement_timeout` plus client deadlines. MySQL deadlines destroy the active
122
- connection. A timed-out write may return `OUTCOME_UNKNOWN` when commit status
123
- cannot be proven.
170
+ ### SQL parameters
124
171
 
125
- ## Output modes
172
+ For shell-safe positional parameters, repeat `--param`. JSON scalars become
173
+ their native types; other values remain strings.
174
+
175
+ ```powershell
176
+ stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
177
+ --param Ada --param trial
178
+ ```
126
179
 
127
- CLI output defaults to compact, one-line `agent` JSON. Successes flatten useful
128
- data and expose the primary durable ID as `handle`; errors retain their complete
129
- error object. Empty warnings and tracing metadata are omitted.
180
+ Use `--params JSON` for a JSON array or named parameters. Use
181
+ `--params-file FILE` when JSON is awkward to quote; `--params-file -` reads
182
+ JSON from standard input.
183
+
184
+ ### Output modes
185
+
186
+ CLI output defaults to compact, one-line `agent` JSON. Successful responses
187
+ flatten useful data and expose the primary durable ID as `handle`. Errors retain
188
+ their complete error object. Empty warnings and tracing metadata are omitted.
130
189
 
131
190
  ```json
132
191
  {"ok":false,"error":{"code":"UNBOUNDED_MUTATION","message":"Mutation has no WHERE clause.","retryable":false,"executed":false,"override_flag":"--allow-unbounded"}}
133
192
  ```
134
193
 
135
- Use `--output json` for the original pretty, verbose envelope, or
136
- `--output jsonl` for that envelope on one line. `--output text` prints a short
137
- human status; `--output silent` prints only a successful handle. Set
138
- `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
139
- file, so use `STQL_OUTPUT` to choose its response mode. Library responses keep
140
- the full envelope regardless of CLI mode.
194
+ Other modes are:
141
195
 
142
- For shell-safe positional parameters, repeat `--param`. JSON scalars become
143
- their native types; other values remain strings.
196
+ - `--output json`: original pretty, verbose envelope.
197
+ - `--output jsonl`: verbose envelope on one line.
198
+ - `--output text`: short human-readable status.
199
+ - `--output silent`: only a successful handle.
144
200
 
145
- ```powershell
146
- stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
147
- --param Ada --param trial
148
- ```
201
+ Set `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
202
+ file, so use `STQL_OUTPUT` to choose the command's response mode. Library
203
+ responses always keep the full envelope.
204
+
205
+ ### Deadlines and cancellation
206
+
207
+ Database commands accept `--timeout-ms N`; the default is 30,000 ms. `Ctrl+C`
208
+ cancels active work.
209
+
210
+ - SQLite runs in a killable child process so long synchronous statements cannot
211
+ block StateQL's event loop.
212
+ - PostgreSQL combines server-side `statement_timeout` with client deadlines.
213
+ - MySQL deadlines destroy the active connection.
214
+
215
+ A timed-out write may return `OUTCOME_UNKNOWN` when its commit status cannot be
216
+ proven.
217
+
218
+ ## Durable state and result reuse
219
+
220
+ State metadata lives under `STQL_HOME`, or the platform data directory when
221
+ unset. StateQL keeps connections, sessions, handles, aliases, cache entries,
222
+ plans, transactions, history, and receipts available across CLI invocations.
223
+
224
+ ### Sessions and actors
225
+
226
+ Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select an
227
+ attached actor. A session is a shared workspace: attached actors reuse its
228
+ connection, handles, aliases, cache, and state version. Plans and staged
229
+ transactions remain owned by the actor that created them.
230
+
231
+ Callers that omit `actor` keep the legacy behavior where the actor ID is the
232
+ session name.
233
+
234
+ ### Result lifetime and limits
149
235
 
150
- Use `--params-file FILE` for arrays or named parameters that are awkward to
151
- quote. `--params-file -` reads JSON from standard input.
236
+ SQLite result rows are materialized locally for durable access. Read cache
237
+ entries expire after five minutes, and materialized handles expire after 24
238
+ hours. Expired results and plans are deleted the next time StateQL opens.
152
239
 
153
- ## Local profiles
240
+ Queries exceeding 10,000 rows or 16 MiB of serialized row data fail before
241
+ persistence. Narrow the `WHERE` clause, add `LIMIT`, or select fewer columns.
242
+ These caps bound persisted materialization; the independent deadline bounds
243
+ execution time.
154
244
 
155
- Profiles persist under `STQL_HOME` with other StateQL metadata.
245
+ Command history keeps the latest 10,000 entries per session. SQLite cache reuse
246
+ also checks the database file signature. PostgreSQL and MySQL cache reuse is
247
+ labeled `ttl_based` and is never authoritative.
248
+
249
+ StateQL limits persisted result payloads to 256 MiB by default. When that quota
250
+ is reached it removes the oldest unaliased results; aliases remain protected. A
251
+ single result that cannot fit fails with `STATE_QUOTA_EXCEEDED`. Configure the
252
+ limit with `maxStateBytes` in the library or `--max-state-bytes` in the CLI.
253
+ Cache and result retention can be configured with `cacheTtlSeconds` and
254
+ `resultTtlSeconds`, or their `--cache-ttl-seconds` and
255
+ `--result-ttl-seconds` CLI equivalents.
256
+
257
+ `stql doctor` checks SQLite integrity and stored payload shapes without printing
258
+ SQL, parameters, or result values. `stql purge` removes expired data by default;
259
+ use `results`, `history`, or `all` for explicit session cleanup. On POSIX
260
+ systems, StateQL removes group and world access from its state directory,
261
+ database, and SQLite sidecar files.
262
+
263
+ ### Local filtering
264
+
265
+ `filter` evaluates one scalar SQLite predicate against a stored result. It
266
+ preserves source order, state metadata, and expiry, and never accesses the
267
+ original database.
268
+
269
+ Use parameters for values. Subqueries, query-shaping clauses, and
270
+ non-allowlisted functions are rejected. Common deterministic functions such as
271
+ `lower`, `upper`, `length`, and `coalesce` are supported.
272
+
273
+ ## Write safety
274
+
275
+ Destructive and unbounded operations require `--allow-destructive` and
276
+ `--allow-unbounded`, respectively. The flags are independent.
277
+
278
+ `plan` validates and stores a write for later application. A plan persists only
279
+ the flags explicitly supplied when it is created; `apply` never adds
280
+ authorization.
281
+
282
+ Use an idempotency key to protect retryable writes from duplicate execution:
156
283
 
157
284
  ```bash
158
- stql profile add local ./app.sqlite --read-write
159
- stql profile add production --env PROD_DATABASE_URL --read-only
160
- stql profile list
161
- stql connect local
162
- stql connect --profile production
285
+ stql exec "UPDATE jobs SET claimed = 1 WHERE id = ?" \
286
+ --param 42 \
287
+ --idempotency-key claim-job-42
163
288
  ```
164
289
 
165
- A bare connection target matching a profile name resolves to that profile;
166
- otherwise it remains a path or database URL. Profiles store targets, read-only
167
- policy, and environment-variable names. Credential values are never stored.
290
+ If a write starts but StateQL cannot safely record its final outcome, it returns
291
+ `OUTCOME_UNKNOWN` and blocks automatic replay. Inspect database state before
292
+ using `--replay`. Interrupted commits remain fail-closed; stale `committing`
293
+ records become `outcome_unknown` after five minutes.
294
+
295
+ ### Transactions
296
+
297
+ Transactions are staged in local state so they survive CLI invocations, then
298
+ executed atomically on commit. While a transaction is active, StateQL rejects
299
+ database reads, plans, connection changes, and disconnects. Commit or roll back
300
+ first.
301
+
302
+ SQLite supports `serializable`. PostgreSQL and MySQL also support
303
+ `repeatable read`, `read committed`, and `read uncommitted`. Server reads run
304
+ inside database-enforced read-only transactions. MySQL staged transactions
305
+ reject DDL because MySQL implicitly commits those statements.
168
306
 
169
307
  ## Batch and pipes
170
308
 
@@ -173,6 +311,8 @@ policy, and environment-variable names. Credential values are never stored.
173
311
  the first error unless `--continue-on-error` is set. Output defaults to one
174
312
  compact `agent` JSON object per line.
175
313
 
314
+ Pipe commands directly:
315
+
176
316
  ```bash
177
317
  printf '%s\n' \
178
318
  '{"command":"query","sql":"SELECT id, email FROM users ORDER BY id","as":"users"}' \
@@ -181,6 +321,8 @@ printf '%s\n' \
181
321
  stql pipe
182
322
  ```
183
323
 
324
+ Or save a JSON array as `commands.json`:
325
+
184
326
  ```json
185
327
  [
186
328
  {
@@ -197,55 +339,21 @@ printf '%s\n' \
197
339
  ]
198
340
  ```
199
341
 
200
- Run the array with `stql batch commands.json`. Batch fields use snake case;
201
- supported command names match CLI paths, such as `filter`,
202
- `transaction.begin`, `session.summary`, `alias.set`, `plan`, and `apply`.
203
- Batch filters use `where` for the predicate and may assign the derived result
204
- with `as`. Database commands may set `timeout_ms`; otherwise they use the
205
- 30-second default.
206
-
207
- State metadata lives under `STQL_HOME`, or the platform data directory when
208
- unset. Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select
209
- an attached actor for CLI invocations. A session is a shared workspace:
210
- attached actors reuse its connection, handles, aliases, cache, and
211
- state version, while plans and staged transactions remain owned by their
212
- creating actor. Callers that omit `actor` keep the legacy behavior where the
213
- actor ID is the session name.
214
-
215
- Read cache entries expire after five minutes; materialized handles expire after
216
- 24 hours. Expired results and plans are deleted when StateQL next opens. Queries
217
- exceeding 10,000 rows or 16 MiB of serialized row data fail before persistence;
218
- add a narrower `WHERE` clause, `LIMIT`, or smaller column selection. These caps
219
- bound persisted materialization, while the independent deadline bounds execution
220
- time. Command history keeps the latest 10,000 entries per session. SQLite cache reuse also checks
221
- the database file signature; PostgreSQL and MySQL reuse is labeled `ttl_based`,
222
- never authoritative. Transactions are staged in local state so they survive CLI
223
- invocations, then executed atomically on commit. Database reads, plans,
224
- connection changes, and disconnects are rejected while a transaction is active;
225
- commit or roll back first. SQLite supports `serializable`;
226
- PostgreSQL and MySQL also support `repeatable read`, `read committed`, and
227
- `read uncommitted`. Server reads run inside database-enforced read-only
228
- transactions. MySQL staged transactions reject DDL because MySQL implicitly
229
- commits those statements.
342
+ ```bash
343
+ stql batch commands.json
344
+ ```
230
345
 
231
- StateQL stores no PostgreSQL or MySQL password. Credential-bearing URLs must be
232
- supplied through `--env`. SQLite result rows are materialized locally for
233
- durable access.
234
- `filter` evaluates one scalar SQLite predicate against those stored rows, keeps
235
- source order, state metadata, and expiry, and never accesses the original
236
- database. Use parameters for values. Subqueries, query-shaping clauses, and
237
- non-allowlisted functions are rejected; common deterministic functions such as
238
- `lower`, `upper`, `length`, and `coalesce` are supported.
346
+ Batch fields use snake case. Supported command names match CLI paths, such as
347
+ `filter`, `transaction.begin`, `session.summary`, `alias.set`, `plan`, and
348
+ `apply`. Batch filters use `where` for the predicate and may assign the derived
349
+ result with `as`. Database commands may set `timeout_ms`; otherwise they use the
350
+ 30-second default.
239
351
 
240
- Destructive and unbounded operations require their respective flags
241
- independently. Plans persist only flags explicitly supplied when the plan is
242
- created; `apply` never adds authorization. If a database write starts but its
243
- final outcome cannot be recorded safely, StateQL returns `OUTCOME_UNKNOWN` and
244
- blocks automatic replay. Inspect database state before using `--replay`.
245
- Interrupted commits remain fail-closed; stale `committing` records become
246
- `outcome_unknown` after five minutes.
352
+ ## TypeScript library
247
353
 
248
- ## Library
354
+ The package exports the same stateful operations for programmatic use. Library
355
+ responses retain the full response envelope regardless of the configured CLI
356
+ output mode.
249
357
 
250
358
  ```ts
251
359
  import { StateQL } from "@fadhilp/stateql";
@@ -255,12 +363,15 @@ const stateql = StateQL.forActor({
255
363
  actor: "pi-session-id",
256
364
  timeoutMs: 30_000,
257
365
  maxResultBytes: 16 * 1024 * 1024,
366
+ maxStateBytes: 256 * 1024 * 1024,
258
367
  });
368
+
259
369
  const controller = new AbortController();
260
370
  const response = await stateql.query("SELECT * FROM users", {
261
371
  signal: controller.signal,
262
372
  timeoutMs: 5_000,
263
373
  });
374
+
264
375
  if (response.ok) {
265
376
  const handle = (response.data as { result_id: string }).result_id;
266
377
  await stateql.filter(handle, "email LIKE ?", {
@@ -269,6 +380,19 @@ if (response.ok) {
269
380
  }
270
381
  ```
271
382
 
383
+ ### Actor workspaces
384
+
385
+ `StateQL.forActor(...)` resolves the actor's attached session directly from
386
+ StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
387
+ On first use, it creates a legacy-compatible session named after the actor. Use
388
+ `new StateQL({ session, actor })` when the session is already known.
389
+
390
+ Membership is managed only through the library API, not batch commands:
391
+ `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
392
+ `listActors(session)`, and `resolveActor(actorId)`. An existing member must link
393
+ an actor before that actor opens an existing workspace. Integrations should ask
394
+ for user confirmation before changing membership or the shared connection.
395
+
272
396
  ### Harness credential resolution
273
397
 
274
398
  Library integrations can resolve a profile's credential reference through a
@@ -292,6 +416,7 @@ async function resolveCredential(
292
416
  access: request.access,
293
417
  signal: request.signal,
294
418
  });
419
+
295
420
  if (approved.denied) throw new CredentialResolutionError("denied");
296
421
  return approved.value;
297
422
  }
@@ -302,39 +427,31 @@ const stateql = StateQL.forActor({
302
427
  });
303
428
  ```
304
429
 
305
- When no custom resolver is configured, StateQL continues to read references
306
- from `process.env`. A configured resolver is authoritative: returning
307
- `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls back to the
308
- process environment. Resolvers may throw `CredentialResolutionError` with
309
- `denied`, `cancelled`, `timeout`, or `unavailable` to produce controlled,
310
- secret-free failures. Unknown resolver errors are replaced with a generic
430
+ When no custom resolver is configured, StateQL reads references from
431
+ `process.env`. A configured resolver is authoritative: returning `undefined`
432
+ produces `CREDENTIAL_UNAVAILABLE` and never falls back to the process
433
+ environment. Resolvers may throw `CredentialResolutionError` with `denied`,
434
+ `cancelled`, `timeout`, or `unavailable` to produce controlled, secret-free
435
+ failures. Unknown resolver errors are replaced with a generic
311
436
  `CREDENTIAL_RESOLUTION_FAILED` response.
312
437
 
313
438
  StateQL calls the resolver only immediately before database access, after SQL
314
- safety and duplicate checks. Requests contain actor/session identity, the
439
+ safety and duplicate checks. Requests contain actor and session identity, the
315
440
  operation's effective read/write access, an abort signal, and sanitized
316
- connection metadata. Returned values must be complete PostgreSQL/MySQL URLs or
317
- explicit `sqlite:` sources. StateQL validates the source and its stored driver
318
- before adapter construction, and normalizes SQLite paths. Credential-bearing
319
- PostgreSQL and MySQL URLs are redacted before connection
320
- metadata is persisted and never enter history, snapshots, cache keys, or
321
- responses. SQLite paths remain persisted connection metadata, as they are for
322
- direct SQLite connections. Harnesses remain responsible for approval policy,
323
- binding lifetime, revocation, and keeping values out of their own logs and
324
- model-visible data.
441
+ connection metadata.
442
+
443
+ Returned values must be complete PostgreSQL or MySQL URLs, or explicit
444
+ `sqlite:` sources. StateQL validates the source and its stored driver before
445
+ adapter construction and normalizes SQLite paths. Credential-bearing
446
+ PostgreSQL and MySQL URLs are redacted before connection metadata is persisted
447
+ and never enter history, snapshots, cache keys, or responses. SQLite paths
448
+ remain persisted connection metadata, as they are for direct SQLite
449
+ connections.
450
+
451
+ Harnesses remain responsible for approval policy, binding lifetime, revocation,
452
+ and keeping values out of their own logs and model-visible data.
325
453
 
326
454
  For writes, credential resolution happens after StateQL atomically reserves the
327
455
  operation for duplicate protection. A resolution failure keeps a non-executed
328
456
  `failed` audit record, does not consume the idempotency key, and permits a safe
329
457
  retry.
330
-
331
- `StateQL.forActor(...)` resolves the actor's attached session directly from
332
- StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
333
- On first use, it creates a legacy-compatible session named after the actor.
334
- Use `new StateQL({ session, actor })` when the session is already known.
335
-
336
- Membership is managed only through the library API, not batch commands:
337
- `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
338
- `listActors(session)`, and `resolveActor(actorId)`. An existing member must link
339
- an actor before that actor opens an existing workspace. Integrations should ask
340
- for user confirmation before changing membership or the shared connection.
@@ -1,7 +1,7 @@
1
1
  import { fork } from "node:child_process";
2
2
  import { createConnection as createMySqlConnection, } from "mysql2";
3
3
  import { Client, types as pgTypes } from "pg";
4
- import { parseJson, toJsonSafe } from "./util.js";
4
+ import { isSqlParameters, parseJson, toJsonSafe } from "./util.js";
5
5
  export class AdapterExecutionError extends Error {
6
6
  reason;
7
7
  outcomeUnknown;
@@ -288,7 +288,7 @@ class PostgresAdapter {
288
288
  try {
289
289
  for (const operation of operations) {
290
290
  await this.setLocalDeadline();
291
- const result = await this.query(operation.sql, postgresParams(parseJson(operation.parameters, [])), true);
291
+ const result = await this.query(operation.sql, postgresParams(parseJson(operation.parameters, `operation "${operation.id}" parameters`, isSqlParameters)), true);
292
292
  results.push({ affectedRows: result.rowCount ?? 0 });
293
293
  }
294
294
  }
@@ -506,7 +506,7 @@ class MySqlAdapter {
506
506
  const results = [];
507
507
  try {
508
508
  for (const operation of operations) {
509
- const [result] = await this.query(operation.sql, mysqlParams(parseJson(operation.parameters, [])), true, true);
509
+ const [result] = await this.query(operation.sql, mysqlParams(parseJson(operation.parameters, `operation "${operation.id}" parameters`, isSqlParameters)), true, true);
510
510
  results.push({ affectedRows: mysqlAffectedRows(result) });
511
511
  }
512
512
  }
package/dist/src/cli.js CHANGED
@@ -25,6 +25,9 @@ const parsed = parseArgs({
25
25
  offset: { type: "string" },
26
26
  limit: { type: "string" },
27
27
  "timeout-ms": { type: "string" },
28
+ "max-state-bytes": { type: "string" },
29
+ "cache-ttl-seconds": { type: "string" },
30
+ "result-ttl-seconds": { type: "string" },
28
31
  format: { type: "string" },
29
32
  output: { type: "string" },
30
33
  isolation: { type: "string" },
@@ -50,6 +53,15 @@ const stateql = new StateQL({
50
53
  ? {}
51
54
  : { timeoutMs: Number(values["timeout-ms"]) }),
52
55
  ...(process.env.STQL_ACTOR ? { actor: process.env.STQL_ACTOR } : {}),
56
+ ...(values["max-state-bytes"] === undefined
57
+ ? {}
58
+ : { maxStateBytes: Number(values["max-state-bytes"]) }),
59
+ ...(values["cache-ttl-seconds"] === undefined
60
+ ? {}
61
+ : { cacheTtlSeconds: Number(values["cache-ttl-seconds"]) }),
62
+ ...(values["result-ttl-seconds"] === undefined
63
+ ? {}
64
+ : { resultTtlSeconds: Number(values["result-ttl-seconds"]) }),
53
65
  signal: abortController.signal,
54
66
  });
55
67
  try {
@@ -195,6 +207,10 @@ async function dispatch() {
195
207
  return stateql.history(numberOption(values.limit, 20));
196
208
  case "receipt":
197
209
  return stateql.receipt(requireValue(subcommand, "operation handle"));
210
+ case "doctor":
211
+ return stateql.doctor();
212
+ case "purge":
213
+ return stateql.purge(purgeScope(subcommand));
198
214
  case "capabilities":
199
215
  return stateql.capabilities();
200
216
  default:
@@ -325,6 +341,13 @@ function parseBatchJson(value, location) {
325
341
  function numberOption(value, fallback) {
326
342
  return value === undefined ? fallback : Number(value);
327
343
  }
344
+ function purgeScope(value) {
345
+ if (!value || value === "expired")
346
+ return "expired";
347
+ if (value === "results" || value === "history" || value === "all")
348
+ return value;
349
+ throw new Error("purge scope must be expired, results, history, or all.");
350
+ }
328
351
  function cacheMode(value) {
329
352
  if (!value || value === "auto")
330
353
  return "auto";
@@ -515,12 +538,13 @@ Commands:
515
538
  alias set
516
539
  inspect schema|table|columns|indexes|constraints
517
540
  transaction begin|status|commit|rollback
518
- plan, apply, history, receipt, capabilities
541
+ plan, apply, history, receipt, doctor, purge, capabilities
519
542
  batch [file.json|file.jsonl|-]
520
543
  pipe
521
544
 
522
545
  SQL parameters: --params JSON, repeated --param VALUE, or --params-file FILE.
523
546
  Deadline: --timeout-ms N (default: 30000). Ctrl+C cancels database work.
547
+ State: --max-state-bytes N, --cache-ttl-seconds N, --result-ttl-seconds N.
524
548
  Output: --output agent|json|jsonl|text|silent (default: agent).
525
549
  Batch/pipe accept JSON array files or JSONL streams. Stop on first error.`;
526
550
  }
@@ -1,4 +1,4 @@
1
1
  export { StateQL } from "./stateql.js";
2
2
  export { CredentialResolutionError, StateQLError, exitCodeFor, } from "./errors.js";
3
3
  export type { CredentialResolutionFailure } from "./errors.js";
4
- export type { BatchCommand, BatchCommandName, BatchOptions, ConnectOptions, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, ExecOptions, ExecutionOptions, Failure, FilterOptions, HistoryEntry, PlanOptions, ProfileOptions, QueryOptions, Response, RowsOptions, SqlParameters, StateQLActorOptions, StateQLOptions, StateQLSnapshot, Success, } from "./types.js";
4
+ export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CapabilitiesData, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfilesData, PurgeData, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ export declare function runMigrations(db: DatabaseSync, now: () => Date): void;