@fadhilp/stateql 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,17 @@
1
1
  # StateQL
2
2
 
3
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, MySQL, MongoDB, and Redis databases while keeping results
6
- reusable and operations traceable across commands.
4
+ automation. Query, change, and inspect databases while keeping results reusable
5
+ and operations traceable across commands.
7
6
 
8
- StateQL is built around durable handles:
7
+ - **SQL:** SQLite, PostgreSQL, and MySQL.
8
+ - **Native commands:** MongoDB (EJSON) and Redis (JSON).
9
+ - **Durable handles:** reuse, filter, page, count, alias, or export stored results
10
+ without rerunning the original query.
11
+ - **Tracked writes:** inspect operations, review plans, and stage transactions
12
+ where supported by the database.
9
13
 
10
- 1. Run a query and receive a result handle such as `q_k7m2v5x9c3d6f8h4j2n7p5r9tw`.
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.
14
-
15
- Requires Node.js 22.16 or newer for the required `node:sqlite` APIs.
14
+ Requires **Node.js 22.16 or newer** for the required `node:sqlite` APIs.
16
15
 
17
16
  ## Quick start
18
17
 
@@ -22,11 +21,13 @@ Install the CLI:
22
21
  npm install -g @fadhilp/stateql
23
22
  ```
24
23
 
25
- Connect to an existing SQLite database and run a bounded, parameterized query:
24
+ Connect to an existing SQLite database containing a `users` table. These shell
25
+ examples use Bash; see the [usage guide](docs/usage.md) for connection options and
26
+ SQL parameter handling.
26
27
 
27
28
  ```bash
28
29
  export STQL_SESSION=audit
29
- stql profile add local ./app.sqlite
30
+ stql profile add local ./app.sqlite --read-only
30
31
  stql connect local
31
32
 
32
33
  stql query \
@@ -37,759 +38,62 @@ stql query \
37
38
 
38
39
  Parameters keep values separate from SQL. `ORDER BY` makes paging stable, and
39
40
  `LIMIT` bounds work at the database. The default `agent` output is compact,
40
- one-line JSON:
41
-
42
- ```json
43
- {"ok":true,"handle":"q_k7m2v5x9c3d6f8h4j2n7p5r9tw","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}
44
- ```
45
-
46
- `q_k7m2v5x9c3d6f8h4j2n7p5r9tw` is a durable snapshot. Filter it locally without accessing the original
47
- database:
48
-
49
- ```bash
50
- stql filter q_k7m2v5x9c3d6f8h4j2n7p5r9tw "email LIKE ?" --param "%@example.com"
51
- ```
52
-
53
- ```json
54
- {"ok":true,"handle":"q_z4n8c2v6b3m7k5j9h2g4f6d8sa","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}
55
- ```
56
-
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:
59
-
60
- ```bash
61
- stql alias set example-users q_z4n8c2v6b3m7k5j9h2g4f6d8sa
62
- stql rows example-users --offset 0 --limit 1
63
- stql rows example-users --offset 1 --limit 1
64
- stql count example-users
65
- stql export example-users --output example-users.csv --format csv
66
- ```
67
-
68
- Example first page:
41
+ one-line JSON. An example response:
69
42
 
70
43
  ```json
71
- {"ok":true,"handle":"q_z4n8c2v6b3m7k5j9h2g4f6d8sa","rows":[{"id":7,"name":"Ada","email":"ada@example.com"}],"total":2,"truncated":true,"next_offset":1}
72
- ```
73
-
74
- Running the same normalized query with the same parameters reuses `q_k7m2v5x9c3d6f8h4j2n7p5r9tw` while
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`,
80
- `--credential-ref`, or `--profile`. Library and batch callers may additionally
81
- attach `passwordRef`/`password_ref` to a literal password-free remote target;
82
- it is not a fourth source.
83
-
84
- ```bash
85
- stql connect <sqlite-path|postgres-url|mysql-url|mongodb-url> [--name NAME] [--read-write]
86
- stql connect --env ENV [--name NAME] [--read-write]
87
- stql connect --credential-ref REF [--name NAME] [--read-write]
88
- stql connect --profile NAME
89
- stql disconnect
90
- stql status
44
+ {"ok":true,"handle":"q_k7m2v5x9c3d6f8h4j2n7p5r9tw","rows":[{"id":7,"name":"Ada","email":"ada@example.com"}],"truncated":false,"cached":false,"total":1,"next_offset":null}
91
45
  ```
92
46
 
93
- ### Environment-backed credentials
94
-
95
- PostgreSQL, MySQL, and MongoDB credentials should come from environment
96
- variables. The variable must contain the complete connection URL, not only its
97
- password. Environment-backed SQLite paths require an explicit `sqlite:` prefix.
98
-
99
- ```bash
100
- export APP_DATABASE_URL='postgres://user:password@host/app'
101
- stql connect --env APP_DATABASE_URL --name app --read-only
102
-
103
- export MYSQL_DATABASE_URL='mysql://user:password@host/app'
104
- stql connect --env MYSQL_DATABASE_URL --name mysql-app --read-only
105
-
106
- export MONGODB_URL='mongodb://user:password@host/app'
107
- stql connect --env MONGODB_URL --name mongo-app --read-only
108
-
109
- export SQLITE_DATABASE='sqlite:./app.sqlite'
110
- stql connect --env SQLITE_DATABASE --name local --read-only
111
- ```
112
-
113
- StateQL stores no PostgreSQL, MySQL, MongoDB, or Redis password.
114
- Credential-bearing URLs must be supplied through `--env` or an opaque full-URL
115
- credential reference. SQLite paths remain persisted as connection metadata.
116
-
117
- ### Local profiles
118
-
119
- Profiles store exactly one connection target, environment-variable name, or
120
- opaque credential reference together with read-only policy. A remote literal
121
- target may additionally store a `password_ref`; SQLite, environment-backed, and
122
- full-URL `credential_ref` profiles cannot. Credential values are never stored.
123
- Profiles persist under `STQL_HOME` with other StateQL metadata, and list/show
124
- responses include nullable `credential_ref` and `password_ref` fields.
125
-
126
- ```bash
127
- stql profile add local ./app.sqlite --read-write
128
- stql profile add production --env PROD_DATABASE_URL --read-only
129
- stql profile add hosted --credential-ref 'vault://team/app' --read-only
130
- stql profile list
131
- stql profile show production
132
- stql connect local
133
- stql connect --profile production
134
- ```
135
-
136
- Credential references are bounded nonempty opaque strings; StateQL does not
137
- apply environment-variable syntax or normalization to them. They can only be
138
- resolved by a trusted host `CredentialResolver`, so the standalone CLI may
139
- store them in profiles but cannot connect with them.
140
-
141
- Library callers can keep nonsecret endpoint, username, database, TLS, and CA
142
- options in the literal URL while resolving only its password:
143
-
144
- ```ts
145
- await stateql.connect(
146
- "postgres://app@db.example/app?sslmode=verify-full&sslrootcert=/etc/app-ca.pem",
147
- { passwordRef: "vault://database/app/password", readOnly: true },
148
- );
149
- ```
150
-
151
- The same field is accepted by `addProfile`, `updateProfile`, and batch
152
- `connect`/`profile.add`/`profile.update` commands (snake case in batch input).
153
- Targets with an embedded password or query parameters that override endpoint or
154
- credential fields are rejected before credential resolution or driver access.
155
-
156
- A bare connection target matching a profile name resolves to that profile;
157
- otherwise it remains a path or database URL.
158
-
159
- ### Driver notes
160
-
161
- - **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
162
- environment-backed path.
163
- - **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
164
- `sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
165
- the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
166
- `uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
167
- - **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
168
- currently claimed.
169
- - **MongoDB:** supports `mongodb://` and `mongodb+srv://` URLs with an explicit
170
- database path. SQL methods are rejected; use the native MongoDB methods below.
171
-
172
- ## CLI reference
173
-
174
- ```text
175
- stql connect <sqlite-path|postgres-url|mysql-url|mongodb-url> [--name NAME] [--read-write]
176
- stql connect --env ENV [--name NAME] [--read-write]
177
- stql connect --profile NAME
178
- stql disconnect
179
- stql status
180
- stql profile add|list|show|remove
181
- stql session start|list|show|summary|close
182
- stql query <sql> [--params JSON | --param VALUE...] [--cache auto|bypass|require]
183
- stql filter <result-handle> <predicate> [--params JSON | --param VALUE...]
184
- stql exec <sql> [--params JSON | --param VALUE...] [--idempotency-key KEY] [--replay]
185
- [--allow-unbounded] [--allow-destructive]
186
- stql mongo query|exec|plan '<EJSON command>' [--cache MODE] [--idempotency-key KEY]
187
- [--replay] [--allow-unbounded] [--allow-destructive]
188
- stql show|count|columns <result-handle>
189
- stql rows <result-handle> [--offset N] [--limit N]
190
- stql alias set <name> <result-handle>
191
- stql export <result-handle> --output FILE [--format json|jsonl|csv]
192
- stql inspect schema|table|collection|collections|columns|indexes|constraints [name]
193
- stql transaction begin|status|commit|rollback [--isolation LEVEL]
194
- stql plan <sql> [--allow-unbounded] [--allow-destructive]
195
- stql apply <plan-handle>
196
- stql history [--limit N]
197
- stql receipt <operation-handle>
198
- stql doctor
199
- stql purge [expired|results|history|all]
200
- stql capabilities
201
- stql batch [commands.json|commands.jsonl|-] [--continue-on-error]
202
- stql pipe [--continue-on-error]
203
- ```
204
-
205
- ### SQL parameters
206
-
207
- For shell-safe positional parameters, repeat `--param`. JSON scalars become
208
- their native types; other values remain strings.
209
-
210
- ```powershell
211
- stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
212
- --param Ada --param trial
213
- ```
214
-
215
- Use `--params JSON` for a JSON array or named parameters. Use
216
- `--params-file FILE` when JSON is awkward to quote; `--params-file -` reads
217
- JSON from standard input.
218
-
219
- ### PostgreSQL diagnostics and maintenance
220
-
221
- Run PostgreSQL plans through `query`:
222
-
223
- ```bash
224
- stql query "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM jobs WHERE id = 42"
225
- ```
226
-
227
- Plain `EXPLAIN` may plan a structurally validated `SELECT`, `INSERT`, `UPDATE`,
228
- or `DELETE`. Because `EXPLAIN ANALYZE` executes its inner statement, StateQL
229
- accepts only a validated read-only `SELECT`; `SELECT INTO`, writing CTEs, and
230
- mutations are rejected. Diagnostics execute inside PostgreSQL `BEGIN READ ONLY`
231
- and are never reused from cache. `--cache require` therefore returns
232
- `CACHE_MISS` without executing the diagnostic.
233
-
234
- `VACUUM`, `ANALYZE`, `REINDEX`, and `CLUSTER` are PostgreSQL maintenance writes:
235
-
236
- ```bash
237
- stql exec "VACUUM (ANALYZE) public.jobs" --allow-destructive
238
- stql plan "REINDEX TABLE public.jobs" --allow-destructive
239
- ```
240
-
241
- They require a read-write connection and `--allow-destructive`, reject StateQL
242
- parameters, and run as individually tracked autocommit operations. They cannot
243
- be staged in a StateQL transaction. A timeout or cancellation after dispatch is
244
- reported as `OUTCOME_UNKNOWN`; inspect database state before replaying it. Raw
245
- `BEGIN`, `COMMIT`, `ROLLBACK`, savepoint, and other transaction-control SQL
246
- remain unsupported—use `stql transaction` commands instead. See
247
- [`SQL_COMMAND_ROADMAP.md`](SQL_COMMAND_ROADMAP.md) for the exact implemented
248
- boundary and deferred command categories.
249
-
250
- ### Native MongoDB
251
-
252
- MongoDB commands use official Extended JSON (EJSON), so BSON values survive the
253
- CLI boundary:
254
-
255
- ```bash
256
- stql mongo query '{"operation":"find","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}}}'
257
- stql mongo exec '{"operation":"updateOne","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}},"update":{"$set":{"seen_at":{"$date":"2026-01-01T00:00:00Z"}}}}'
258
- stql mongo plan '{"operation":"deleteMany","collection":"users","filter":{"disabled":true}}' --allow-destructive
259
- ```
260
-
261
- The TypeScript equivalents are `mongoQuery(command)`, `mongoExec(command)`, and
262
- `mongoPlan(command)`. Supported reads are `find` and `aggregate`; writes are
263
- `insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `deleteOne`,
264
- and `deleteMany`. Result documents are JSON-safe, order-preserving EJSON: for example,
265
- ObjectIds and dates appear as `{ "$oid": "..." }` and
266
- `{ "$date": { "$numberLong": "..." } }`.
267
-
268
- Empty update, replacement, or delete filters require `--allow-unbounded`;
269
- deletes and replacements also require `--allow-destructive`. Mongo inspection accepts `collections`,
270
- `collection`, `columns`, `indexes`, and `constraints` (`schema` and `table`
271
- remain aliases shared with SQL drivers). MongoDB cache confidence is TTL-based:
272
- external writes are not detected, so use `--cache bypass` for a fresh read.
273
-
274
- ```ts
275
- const result = await stateql.mongoQuery({
276
- operation: "find",
277
- collection: "users",
278
- filter: { active: true },
279
- options: { sort: { _id: 1 }, limit: 50 },
280
- });
281
- ```
282
- ### Output modes
283
-
284
- CLI output defaults to compact, one-line `agent` JSON. Successful responses
285
- flatten useful data and expose the primary durable ID as `handle`. Errors retain
286
- their complete error object. Empty warnings and tracing metadata are omitted.
287
-
288
- ```json
289
- {"ok":false,"error":{"code":"UNBOUNDED_MUTATION","message":"Mutation has no WHERE clause.","retryable":false,"executed":false,"override_flag":"--allow-unbounded"}}
290
- ```
291
-
292
- Other modes are:
293
-
294
- - `--output json`: original pretty, verbose envelope.
295
- - `--output jsonl`: verbose envelope on one line.
296
- - `--output text`: short human-readable status.
297
- - `--output silent`: only a successful handle.
298
-
299
- Set `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
300
- file, so use `STQL_OUTPUT` to choose the command's response mode. Library
301
- responses always keep the full envelope.
302
-
303
- ### Deadlines and cancellation
304
-
305
- Database commands accept `--timeout-ms N`; the default is 30,000 ms. `Ctrl+C`
306
- cancels active work.
307
-
308
- - SQLite runs in a killable child process so long synchronous statements cannot
309
- block StateQL's event loop.
310
- - PostgreSQL combines server-side `statement_timeout` with client deadlines.
311
- - MySQL deadlines destroy the active connection.
312
- - MongoDB uses driver deadlines and closes stopped operations.
313
-
314
- A timed-out or cancelled write may return `OUTCOME_UNKNOWN` when its commit
315
- status cannot be proven. Cancellation stops that command's driver work; it does
316
- not close the `StateQL` actor, and later commands remain usable.
317
-
318
- ## Durable state and result reuse
319
-
320
- State metadata lives under `STQL_HOME`, or the platform data directory when
321
- unset. StateQL keeps connections, sessions, handles, aliases, cache entries,
322
- plans, transactions, history, and receipts available across CLI invocations.
323
-
324
- ### Sessions and actors
325
-
326
- Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select an
327
- attached actor. A session is a shared workspace: attached actors reuse its
328
- connection, handles, aliases, cache, and state version. Plans and staged
329
- transactions remain owned by the actor that created them.
330
-
331
- Callers that omit `actor` keep the legacy behavior where the actor ID is the
332
- session name.
333
-
334
- ### Result lifetime and limits
335
-
336
- SQLite result rows are materialized locally for durable access. Read cache
337
- entries expire after five minutes, and materialized handles expire after 24
338
- hours. Expired results and plans are deleted the next time StateQL opens.
339
-
340
- Queries exceeding 10,000 rows or 16 MiB of serialized row data fail before
341
- persistence. Narrow the `WHERE` clause, add `LIMIT`, or select fewer columns.
342
- These caps bound persisted materialization; the independent deadline bounds
343
- execution time.
344
-
345
- Command history keeps the latest 10,000 entries per session. SQLite cache reuse
346
- also checks the database file signature. PostgreSQL, MySQL, and MongoDB cache
347
- reuse is labeled `ttl_based` and is never authoritative.
348
-
349
- StateQL limits persisted result payloads to 256 MiB by default. When that quota
350
- is reached it removes the oldest unaliased results; aliases remain protected. A
351
- single result that cannot fit fails with `STATE_QUOTA_EXCEEDED`. Configure the
352
- limit with `maxStateBytes` in the library or `--max-state-bytes` in the CLI.
353
- Cache and result retention can be configured with `cacheTtlSeconds` and
354
- `resultTtlSeconds`, or their `--cache-ttl-seconds` and
355
- `--result-ttl-seconds` CLI equivalents.
356
-
357
- `stql doctor` checks SQLite integrity and stored payload shapes without printing
358
- SQL, parameters, or result values. `stql purge` removes expired data by default;
359
- use `results`, `history`, or `all` for explicit session cleanup. On POSIX
360
- systems, StateQL removes group and world access from its state directory,
361
- database, and SQLite sidecar files.
362
-
363
- ### Local filtering
364
-
365
- `filter` evaluates one scalar SQLite predicate against a stored result. It
366
- preserves source order, state metadata, and expiry, and never accesses the
367
- original database.
368
-
369
- Use parameters for values. Subqueries, query-shaping clauses, and
370
- non-allowlisted functions are rejected. Common deterministic functions such as
371
- `lower`, `upper`, `length`, and `coalesce` are supported.
372
-
373
- ## Write safety
374
-
375
- Destructive and unbounded operations require `--allow-destructive` and
376
- `--allow-unbounded`, respectively. The flags are independent.
377
-
378
- `plan` validates and stores a write for later application. A plan persists only
379
- the flags explicitly supplied when it is created; `apply` never adds
380
- authorization.
381
-
382
- Use an idempotency key to protect retryable writes from duplicate execution:
47
+ Use the handle returned by your query in place of the example handle below.
48
+ Give the snapshot a readable alias, then work with the stored data:
383
49
 
384
50
  ```bash
385
- stql exec "UPDATE jobs SET claimed = 1 WHERE id = ?" \
386
- --param 42 \
387
- --idempotency-key claim-job-42
51
+ stql alias set active-users q_k7m2v5x9c3d6f8h4j2n7p5r9tw
52
+ stql rows active-users --offset 0 --limit 10
53
+ stql count active-users
54
+ stql export active-users --output active-users.csv --format csv
55
+ stql filter active-users "email LIKE ?" --param "%@example.com"
388
56
  ```
389
57
 
390
- If a write starts but StateQL cannot safely record its final outcome, it returns
391
- `OUTCOME_UNKNOWN` and blocks automatic replay. Inspect database state before
392
- using `--replay`. Interrupted commits remain fail-closed; stale `committing`
393
- records become `outcome_unknown` after five minutes.
58
+ These commands do not access the original database. `filter` creates a new
59
+ snapshot with its own handle. Repeating a query can reuse a valid cached result;
60
+ use `--cache bypass` when a fresh database read is required.
394
61
 
395
- ### Transactions
62
+ ## Safety and state
396
63
 
397
- Transactions are staged in local state so they survive CLI invocations, then
398
- executed atomically on commit. While a transaction is active, StateQL rejects
399
- database reads, plans, connection changes, and disconnects. Commit or roll back
400
- first.
64
+ - Connections are **read-only by default**. Writes require a read-write connection.
65
+ - Destructive and unbounded SQL/MongoDB operations require independent
66
+ `--allow-destructive` and `--allow-unbounded` approvals, respectively.
67
+ - `plan` stores a write for review; `apply` never adds authorization. Use
68
+ idempotency keys to protect retryable writes from duplicate execution.
69
+ - A write reported as `OUTCOME_UNKNOWN` must be inspected at the database before
70
+ replaying it. Transactions are staged locally, not live interactive SQL
71
+ transactions.
72
+ - Supply credential-bearing URLs through environment variables or a trusted
73
+ library credential resolver, not literal command arguments or profiles.
74
+ - Results and history persist under `STQL_HOME` (or the platform data directory).
75
+ Treat that state as database data; retention and size limits are configurable.
401
76
 
402
- SQLite supports `serializable`. PostgreSQL and MySQL also support
403
- `repeatable read`, `read committed`, and `read uncommitted`. Server reads run
404
- inside database-enforced read-only transactions. MySQL staged transactions
405
- reject DDL because MySQL implicitly commits those statements.
406
- MongoDB transactions use `snapshot` isolation and require a replica set or
407
- sharded deployment; standalone servers do not support them.
77
+ See [write safety](docs/usage.md#write-safety),
78
+ [credentials](docs/usage.md#environment-backed-credentials), and
79
+ [database restrictions](docs/databases.md) before using production data.
408
80
 
409
- ## Batch and pipes
81
+ ## Documentation
410
82
 
411
- `batch` reads a JSON array from a `.json` file or JSONL from a `.jsonl` file.
412
- `pipe` reads JSONL from standard input. Commands run sequentially and stop on
413
- the first error unless `--continue-on-error` is set. Output defaults to one
414
- compact `agent` JSON object per line.
83
+ | Guide | Contents |
84
+ | --- | --- |
85
+ | [Usage](docs/usage.md) | Connections, CLI reference, results, sessions, limits, writes, transactions, batch and pipes |
86
+ | [Database support](docs/databases.md) | SQL dialects, diagnostics, maintenance, native MongoDB and Redis commands |
87
+ | [TypeScript library](docs/library.md) | Connection setup, response handling, cleanup, actors, workspaces, and credential resolution |
88
+ | [SQL command roadmap](SQL_COMMAND_ROADMAP.md) | Development priorities and requirements for extending SQL support |
415
89
 
416
- Pipe commands directly:
417
-
418
- ```bash
419
- printf '%s\n' \
420
- '{"command":"query","sql":"SELECT id, email FROM users ORDER BY id","as":"users"}' \
421
- '{"command":"filter","handle":"users","where":"email LIKE ?","params":["%@example.com"],"as":"example_users"}' \
422
- '{"command":"rows","handle":"example_users","limit":10}' |
423
- stql pipe
424
- ```
90
+ Run `stql --help` for command syntax or `stql capabilities` for capability details.
425
91
 
426
- Or save a JSON array as `commands.json`:
427
-
428
- ```json
429
- [
430
- {
431
- "command": "exec",
432
- "sql": "UPDATE jobs SET claimed = 1 WHERE id = ?",
433
- "params": [42],
434
- "idempotency_key": "claim-job-42"
435
- },
436
- {
437
- "command": "query",
438
- "sql": "SELECT * FROM jobs WHERE id = ?",
439
- "params": [42]
440
- }
441
- ]
442
- ```
92
+ ## TypeScript installation
443
93
 
444
94
  ```bash
445
- stql batch commands.json
446
- ```
447
-
448
- Batch fields use snake case. Supported command names match CLI paths, such as
449
- `filter`, `transaction.begin`, `session.summary`, `alias.set`, `plan`, and
450
- `apply`. Native MongoDB batches use `mongo.query`, `mongo.exec`, or `mongo.plan`
451
- with the command object in `mongo`; the same cache, replay, idempotency, safety,
452
- and timeout fields apply. Database commands may set `timeout_ms`; otherwise they
453
- use the 30-second default.
454
-
455
- ## TypeScript library
456
-
457
- The package exports the same stateful operations for programmatic use. Library
458
- responses retain the full response envelope regardless of the configured CLI
459
- output mode.
460
-
461
- ```ts
462
- import { StateQL } from "@fadhilp/stateql";
463
-
464
- const stateql = StateQL.forActor({
465
- home: "./.stql",
466
- actor: "pi-session-id",
467
- timeoutMs: 30_000,
468
- credentialTimeoutMs: 120_000,
469
- maxResultBytes: 16 * 1024 * 1024,
470
- maxStateBytes: 256 * 1024 * 1024,
471
- });
472
-
473
- const controller = new AbortController();
474
- const response = await stateql.query("SELECT * FROM users", {
475
- signal: controller.signal,
476
- timeoutMs: 5_000,
477
- });
478
-
479
- if (response.ok) {
480
- const handle = (response.data as { result_id: string }).result_id;
481
- await stateql.filter(handle, "email LIKE ?", {
482
- params: ["%@example.com"],
483
- });
484
- }
485
- ```
486
-
487
- Hosts that dispatch batch-shaped commands can attach trusted metadata out of
488
- band. `origin` is audit/source metadata only; it never changes actor membership,
489
- workspace access, or write authorization.
490
-
491
- ```ts
492
- const controller = new AbortController();
493
- await stateql.executeCommand(
494
- { command: "query", sql: "SELECT * FROM users", cache: "bypass" },
495
- { origin: "user", signal: controller.signal },
496
- );
497
-
498
- const userHistory = await stateql.history(50, { origin: "user" });
499
- await stateql.executeCommand(
500
- { command: "history", limit: 50, history_origin: "user" },
501
- { origin: "model" },
502
- );
503
- ```
504
-
505
- Supported origins are `legacy`, `user`, `model`, `system`, and `api`. Existing
506
- direct calls and `executeCommand(command)` calls are recorded as `legacy`.
507
- `history_origin` is only a retrieval filter; putting an `origin` field in a
508
- `BatchCommand` cannot attribute the command. `batch` accepts the same trusted
509
- context as `options.executionContext` for all commands in that batch.
510
-
511
- ### Actor workspaces
512
-
513
- `StateQL.forWorkspace(...)` is a trusted-host primitive that atomically creates
514
- or reopens a durable workspace, attaches the requested actor, and returns a
515
- client bound to that actor:
516
-
517
- ```ts
518
- const stateql = StateQL.forWorkspace({
519
- home: "./.stql",
520
- workspace: "pylon-global",
521
- actor: "pylon-session:abc123",
522
- credentialResolver,
523
- signal,
524
- });
525
- ```
526
-
527
- Repeated opens of the same actor and workspace are idempotent. An actor already
528
- attached elsewhere fails with a `StateQLError` whose code is
529
- `PERMISSION_DENIED`; StateQL never moves or merges it. All actor options,
530
- including limits, credential resolution, cancellation, `home`, and `now`, are
531
- preserved. The workspace name also reserves a same-named actor identity for
532
- legacy compatibility, so workspace and actor identifiers must be globally
533
- collision-free. The returned client is still bound only to `actor`, preserving
534
- plan, transaction, operation, and history ownership.
535
-
536
- `StateQL.forActor(...)` retains its existing behavior: it resolves the actor's
537
- attached session directly from StateQL storage and creates a legacy-compatible
538
- session named after the actor on first use. Use `new StateQL({ session, actor })`
539
- when the session and membership are already known.
540
-
541
- Membership management and `forWorkspace` are library-only host capabilities,
542
- not batch or CLI commands. Existing member-authorized management remains
543
- available through `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
544
- `listActors(session)`, and `resolveActor(actorId)`. Integrations should ask for
545
- user confirmation before changing membership or the shared connection; a host
546
- calling `forWorkspace` is responsible for authorizing that workspace access.
547
-
548
- ### Harness credential resolution
549
-
550
- Library integrations can resolve environment-variable names, opaque full-URL
551
- credential references, or password-only references through a trusted approval
552
- or secret-storage layer instead of mutating `process.env`:
553
-
554
- Integrations pinned to an older published package should gate setup before
555
- sending `password_ref`:
556
-
557
- ```ts
558
- if ((StateQL.passwordReferenceVersion ?? 0) < 1) {
559
- throw new Error("Installed StateQL does not support password references.");
560
- }
561
- ```
562
-
563
- `passwordReferenceVersion = 1` guarantees the password-only resolver request,
564
- validation, persistence, reconnect, and redaction contract documented below.
565
-
566
- ```ts
567
- import {
568
- CredentialResolutionError,
569
- StateQL,
570
- type CredentialRequest,
571
- } from "@fadhilp/stateql";
572
-
573
- async function resolveCredential(
574
- request: CredentialRequest,
575
- ): Promise<string | undefined> {
576
- const approved = await credentialBroker.request({
577
- reference: request.reference,
578
- source: request.source ?? "secret_env",
579
- actor: request.actorId,
580
- session: request.session.id,
581
- operation: request.operation,
582
- access: request.access,
583
- signal: request.signal,
584
- });
585
-
586
- if (approved.denied) throw new CredentialResolutionError("denied");
587
- return approved.value;
588
- }
589
-
590
- const stateql = StateQL.forActor({
591
- actor: "agent-session-id",
592
- credentialResolver: resolveCredential,
593
- });
594
- ```
595
-
596
- Credential resolution has its own two-minute default deadline
597
- (`credentialTimeoutMs`) and remains cancellable through `request.signal`.
598
- The database-operation timeout begins after a credential is resolved.
599
-
600
- When no custom resolver is configured, StateQL reads only `secret_env`
601
- references from `process.env`; `credential_ref` and `password_ref` never fall
602
- back to the environment. A configured resolver is authoritative for all
603
- sources: returning `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls
604
- back to the process environment. Resolver requests retain `reference` and
605
- include `source` (`secret_env`, `credential_ref`, or `password_ref`); source may
606
- be omitted only on legacy secret-environment request objects. A `password_ref`
607
- request additionally includes the exact password-free effective `target`.
608
- Resolvers may throw `CredentialResolutionError` with `denied`, `cancelled`,
609
- `timeout`, or `unavailable` to produce controlled, secret-free failures. Unknown
610
- resolver errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED`
611
- response.
612
-
613
- StateQL calls the resolver only immediately before database access, after SQL
614
- safety and duplicate checks. Requests contain actor and session identity, the
615
- operation's effective read/write access, an abort signal, and sanitized
616
- connection metadata.
617
-
618
- For `secret_env` and `credential_ref`, returned values must be complete
619
- PostgreSQL, MySQL, MongoDB, or Redis URLs, or explicit `sqlite:` sources. For
620
- `password_ref`, the resolver returns only the password; an explicit empty string
621
- is a resolved password, while `undefined` fails closed. StateQL percent-encodes
622
- and injects only that password into the original target for adapter use, leaving
623
- all nonsecret URL/TLS/CA bytes unchanged. It persists only the original target
624
- and reference. Resolved credentials never enter connection metadata, history,
625
- snapshots, cache keys, responses, or stored errors.
626
-
627
- Harnesses remain responsible for approval policy, binding lifetime, revocation,
628
- and keeping values out of their own logs and model-visible data.
629
-
630
- For writes, credential resolution happens after StateQL atomically reserves the
631
- operation for duplicate protection. A resolution failure keeps a non-executed
632
- `failed` audit record, does not consume the idempotency key, and permits a safe
633
- retry.
634
-
635
- ## Pylon database integration API (0.9.0)
636
-
637
- ### Result identities and aliases
638
-
639
- Every materialized SQL, MongoDB, Redis, table, or derived result keeps its
640
- immutable canonical `q_*` `result_id`. New canonical resource IDs use a
641
- cryptographically random 26-character lowercase base32 suffix; existing
642
- incremental IDs such as `q_121` remain valid and are not rewritten. Results also
643
- receive a random 10-character lowercase base32 `display_alias`.
644
- `ResultData.alias` normally equals that alias. When a batch command supplies
645
- `as`, `alias` remains the caller alias for backward compatibility while
646
- `display_alias` remains canonical. Generated aliases are session-scoped,
647
- allocated atomically with the result, stable on cache reuse, and cannot be
648
- reassigned by `setAlias`; explicit aliases and all old handles continue to
649
- resolve.
650
-
651
- Connections likewise retain canonical `conn_*` IDs with random 26-character
652
- suffixes and receive persistent random 10-character lowercase base32 aliases,
653
- exposed as `alias` and `display_alias` by `connect()` and
654
- `snapshot().connection` (optional in snapshot types for older producers).
655
- Connection aliases are unique within the state store, allocated atomically with
656
- the connection, and backfilled for existing records on startup. They survive
657
- reopening; reconnecting creates a new ID and alias. They are display identities
658
- only, separate from result aliases; internal references and lookups continue to
659
- use canonical connection IDs.
660
-
661
- ### Safe profile updates
662
-
663
- ```ts
664
- updateProfile(name, {
665
- target?: string | null,
666
- secretEnv?: string | null,
667
- credentialRef?: string | null,
668
- passwordRef?: string | null,
669
- readOnly?: boolean,
670
- })
671
- ```
672
-
673
- Omitting all source and password-reference fields keeps the existing source and
674
- adjunct reference. Supplying any source field replaces the source atomically:
675
- exactly one non-null source is required and the other source columns are
676
- cleared. An omitted `passwordRef` is preserved when the existing literal target
677
- is unchanged; changing the target clears it unless the update explicitly supplies
678
- a replacement. Replacing the source with `secretEnv` or `credentialRef` clears
679
- it. An explicit non-null password reference combined with either
680
- reference-backed source is rejected. Direct URLs
681
- with embedded passwords or secret-like query parameters are rejected.
682
- `profile.list/show/update` return only
683
- `{profile,target,secret_env,credential_ref,password_ref,read_only}`. Profile
684
- changes affect subsequent `connect` calls and do not silently mutate an
685
- already-open connection.
686
-
687
- The `password_refs_v1` migration adds nullable `password_ref` columns to both
688
- profiles and connections and enforces that they accompany only literal target
689
- configuration. It rejects incompatible profile schemas/rows instead of dropping
690
- references. Downgrading a state home containing password references is
691
- unsupported: older binaries do not resolve this source and may attempt the
692
- password-free target using ambient/trust authentication; constraint-protected
693
- source replacements may also fail. Use the same or newer StateQL binary, or
694
- explicitly clear all password references before downgrade.
695
-
696
- ### Bounded catalog
697
-
698
- ```ts
699
- listObjects(
700
- { kind?, schema?, search?, offset?, limit? },
701
- { timeoutMs?, signal? },
702
- ) -> { objects, next_offset, supported_kinds }
703
-
704
- describeObject(
705
- { kind, schema?, name, identity? },
706
- { timeoutMs?, signal? },
707
- ) -> { object, definition? }
708
- ```
709
-
710
- SQL/MongoDB offsets are non-negative numbers; limits default to 50 and are at
711
- most 200. Redis `offset` and `next_offset` are opaque numeric SCAN cursor strings;
712
- its limit is a SCAN `COUNT` hint with a hard 200-item response bound. Redis pages
713
- are not snapshots and can be empty or contain duplicates while keys change.
714
- Search is a case-insensitive name substring for SQL/MongoDB and escaped glob
715
- substring matching for Redis. No exact counts are forced.
716
-
717
- Supported kinds are returned on every page: SQLite `table,view,trigger`;
718
- PostgreSQL `table,view,function,trigger,enum`; MySQL
719
- `table,view,function,trigger`; MongoDB `collection,view`; Redis `key`.
720
- PostgreSQL function identities include identity arguments, so overloads remain
721
- distinct. `describeObject` is read-only and requires the structured identity;
722
- legacy `inspect` behavior is unchanged (and intentionally unavailable for Redis).
723
-
724
- ### Reviewed multi-row table edits
725
-
726
- ```ts
727
- planTableUpdates(
728
- Array<{ row_token: string; changes: { set?: object; unset?: string[] } }>,
729
- options?,
730
- ) -> PlanData
731
- ```
732
-
733
- Batches contain 1-100 distinct row identities and at most 256 KiB. All tokens,
734
- connection/state versions, expiries, metadata, editable columns, and values are
735
- validated before one plan is stored; expiry is the earliest token expiry.
736
- `apply(plan_id)` executes all conditional row updates in one SQLite/PostgreSQL/
737
- MySQL transaction and requires every row predicate to match, otherwise all are
738
- rolled back. MongoDB uses one snapshot transaction and rejects deployments that
739
- do not support transactions. Redis and active staged StateQL transactions are
740
- rejected. The existing `planTableUpdate` and `apply` APIs remain supported.
741
- Plans are actor-owned, claimed once, and retained as non-replayable when the
742
- remote commit outcome is uncertain.
743
-
744
- ### Redis native commands
745
-
746
- Redis/Rediss URLs support URL database selection, password or ACL username,
747
- and TLS (`rediss`). Credential-bearing URLs must come from `secretEnv` or
748
- `credentialRef`. Native methods accept `{command: string, args?: string[]}`:
749
-
750
- - `redisQuery`: `GET`, `MGET`, `TYPE`, `EXISTS`, `TTL`, `PTTL`, `HGET`, `HMGET`,
751
- bounded `LRANGE`, and bounded `SCAN`/`HSCAN`/`SSCAN`/`ZSCAN`.
752
- - `redisExec` and `redisPlan`: one-key `SET`, `DEL`, `HSET`, `HDEL`, `LPUSH`,
753
- `RPUSH`, `SADD`, `SREM`, `ZADD`, or `ZREM` mutation.
754
- - `describeObject({kind:"key",name})`: bounded string/hash/list/set/zset value
755
- inspection with TTL and continuation metadata where applicable.
756
-
757
- Arguments are UTF-8 strings, at most 100 values/256 KiB; materialized replies are
758
- at most 1 MiB. `KEYS`, scripts, modules, pub/sub, blocking commands, admin/flush,
759
- and arbitrary commands are rejected. Key discovery always uses SCAN. A Redis
760
- plan snapshots one bounded key and `apply` uses an isolated `WATCH` + one-command
761
- `MULTI/EXEC`; a pre-apply content or expiry change returns `ROW_CONFLICT` and is
762
- never retried automatically. Direct `redisExec` has Redis single-command
763
- atomicity only. Redis has no SQL rollback or StateQL staged transaction support;
764
- a lost write/EXEC reply is reported as `OUTCOME_UNKNOWN` and remains blocked.
765
-
766
- ### Lean history
767
-
768
- ```ts
769
- history(limit?, {
770
- origin?,
771
- category?: "statement" | "introspection" | "management",
772
- internal?: boolean,
773
- offset?: number,
774
- })
775
- ```
776
-
777
- `category` and trusted-host `internal` filters are applied in SQLite before
778
- `ORDER BY`, `LIMIT`, and `OFFSET`, so introspection cannot starve statement
779
- history. `CommandExecutionContext.internal` is trusted host metadata and cannot
780
- be supplied inside a batch command. Existing calls and origin filtering remain
781
- compatible; old rows are classified from their command name and migrate as `internal: false`.
782
-
783
- The synchronous, non-mutating snapshot bridge accepts the same classification
784
- filters without entering the command queue or writing a history row:
785
-
786
- ```ts
787
- stateql.snapshot({
788
- historyLimit: 50,
789
- historyCategory: "statement",
790
- historyInternal: false,
791
- });
95
+ npm install @fadhilp/stateql
792
96
  ```
793
97
 
794
- Both snapshot filters are applied by the store before `historyLimit`. Calling
795
- `snapshot()` with no options preserves the legacy 50-entry CLI snapshot.
98
+ The package exports `StateQL` and public TypeScript types. Start with the
99
+ [connected, cleanup-safe example](docs/library.md#getting-started).