@abide/abide 0.35.0 → 0.36.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/AGENTS.md CHANGED
@@ -1,249 +1,230 @@
1
1
  # AGENTS.md — abide complete surface map
2
2
 
3
- > Generated from the source as the single index of abide's public featureset, so an
4
- > agent can grasp the whole API in one read and know which file to open for depth.
5
- > The README is the curated human intro (3 primitives); this is the exhaustive map.
6
- > CONTEXT.md is the domain glossary; docs/adr/ holds design rationale.
3
+ > The exhaustive index of abide's public surface: every `exports` key appears once,
4
+ > grouped by namespace, with its import specifier and a one-line spec so an agent
5
+ > grasps the whole API in one read and knows which file to open for depth. The README
6
+ > is the curated 3-primitive intro; this is the complete map.
7
7
  >
8
- > **Ground rule:** every public name has its own module path no barrels. The
9
- > namespace marks the side: `abide/server/*` server-only, `abide/ui/*` client-only,
10
- > `abide/shared/*` isomorphic (same callable, same behaviour on both sides; the
11
- > bundler swaps the runtime). Package: `@abide/abide`. Runtime: Bun 1.3, web
12
- > standards only, zero runtime deps.
8
+ > No barrels: every public name has its own module path, and the namespace marks the
9
+ > side it runs on `abide/server/*` is server-only, `abide/ui/*` is client-only,
10
+ > `abide/shared/*` is isomorphic (same callable, same behaviour on both sides; the
11
+ > bundler swaps the runtime). Importing one name never drags in a side-effecting sibling.
13
12
  >
14
- > **Two kinds of path below.** Import specifiers (`abide/server/GET`) are the stable
15
- > identity that's what you import. File paths like `src/lib/...` are *inside the
16
- > abide package* (relative to this file: `packages/abide/` in this repo,
17
- > `node_modules/@abide/abide/` when abide is a dependency); the published package
18
- > ships `src`, so those files are readable in both. Paths like `src/server/rpc/...`
19
- > and `src/.abide/...` in the conventions table refer to **your own app**, not the
20
- > package.
21
-
22
- ---
13
+ > Package `@abide/abide`, runtime Bun `>= 1.3.0`, one dependency (`typescript`); the
14
+ > import specifier (`abide/server/GET`) is the `@abide/abide/...` `exports` key, not the
15
+ > on-disk file path.
23
16
 
24
17
  ## The premise
25
18
 
26
- One declared verb fans out to every surface, with no extra work:
27
-
28
19
  ```text
29
- export const getMessages = GET(fn, { inputSchema })
30
-
31
- ┌───────────────┬────────────┼──────────────┬────────────────┐
32
- SSR call browser fetch MCP tool CLI subcommand OpenAPI op
33
- cache(fn)() fetch /rpc/... getMessages app get-messages /openapi.json
34
- (in-process) (typed proxy) (read-only+schema) (schema→flags) (described)
20
+ getMessages (one declaration)
21
+
22
+ ┌──────────┬─────────┼─────────┬──────────────┐
23
+ ▼ ▼ ▼ ▼ ▼
24
+ SSR call browser MCP tool CLI sub- OpenAPI
25
+ cache(fn)() fetch (read-only) command operation
35
26
  ```
36
27
 
37
- A Standard Schema (zod / valibot / arktype, unadapted) is the contract: it
38
- validates args and projects the CLI flags, the MCP tool, and the OpenAPI operation.
39
- The schema gates the machine surfaces — it unlocks CLI and (for read-only/GET verbs)
40
- MCP; a mutating verb never auto-exposes to MCP, it needs explicit `clients: { mcp: true }`.
41
-
42
- ## File-based conventions (the bundler reads these paths)
43
-
44
- | Path | Meaning |
45
- |---|---|
46
- | `src/server/rpc/<name>.ts` | one RPC verb per file; file path = URL (`/rpc/<name>`), one export named after the file |
47
- | `src/server/sockets/<name>.ts` | one broadcast socket per file; multiplexes onto `/__abide/sockets` |
48
- | `src/mcp/prompts/<name>.md` | MCP prompt template (`{{arg}}` placeholders) → `definePrompt` |
49
- | `src/server/config.ts` | `export const config = env(schema)` — boot-validated env, also drives the bundle setup form |
50
- | `src/app.ts` | optional app module: `handleError`, `health()` hook, lifecycle (see `AppModule`) |
51
- | `src/bundle/window.ts` | optional desktop bundle window config (`BundleWindow`, default export) |
52
- | `**/page.abide` | a page; directory path → route in bracket form (`/post/[id]`, `/docs/[...rest]`) |
53
- | `**/layout.abide` | nearest-only layout wrapping a page via `<slot/>` (layouts never stack) |
54
- | `src/.abide/*.d.ts` | generated: route/param/rpc/health types (do not hand-edit) |
55
- | `public/<file>` | static asset, served at `/<file>` |
56
- | `dist/_app/` | `abide build` output; `dist/` is what `abide start` serves |
57
-
58
- ## CLI (`bunx abide <cmd>` / `abide <cmd>`)
59
-
60
- | Command | Does |
61
- |---|---|
62
- | `scaffold <name>` | scaffold a project, install, start dev (`--no-install` / `--no-dev`) |
63
- | `dev` | build + run with hot reload |
64
- | `build` | build the client into `dist/_app/` |
65
- | `check` | type-check `.abide` templates + props |
66
- | `start` | run the production server against `dist/` |
67
- | `run <file> [args]` | run a script under the abide preload (same runtime as the server) |
68
- | `compile [--target] [--out]` | build a standalone server executable |
69
- | `cli [--target] [--out] [--platforms]` | build the cli client binary (ships the server beside it; cross-compiles) |
70
- | `bundle` | build a movable self-contained desktop app bundle (unsigned) |
71
- | `lsp` | language server for `.abide` files |
72
-
73
- For `bun test`, add `preload = ["@abide/abide/preload"]` under `[test]` in `bunfig.toml`.
28
+ A verb's `inputSchema` (any Standard Schema) unlocks the CLI and projects the OpenAPI op; a read-only `GET`/`HEAD` also auto-exposes an MCP tool. A mutating verb never auto-exposes to an agent — it opts in with `clients: { mcp: true }`. The same gating applies to sockets via their schema.
74
29
 
75
- ---
30
+ ## File-based conventions
76
31
 
77
- ## Server surface `abide/server/*` (and authored helpers)
78
-
79
- ### RPC verbs — `@documentation rpc`
80
- Each is `Verb(fn, opts?)` with three overloads (see `src/lib/server/rpc/types/VerbHelper.ts`):
81
- `Verb(fn, { inputSchema, outputSchema?, filesSchema?, clients?, timeout?, maxBodySize?, crossOrigin? })`,
82
- `Verb(fn, { clients })`, and bare `Verb(fn)`. Return type usually infers from the
83
- `TypedResponse<T>` brand on `json`/`error`/`redirect`/`jsonl`/`sse`.
84
-
85
- - `abide/server/GET` · `abide/server/POST` · `abide/server/PUT` · `abide/server/PATCH` · `abide/server/DELETE` · `abide/server/HEAD`
86
- - **Query args travel as strings** — use `z.coerce.*` in the schema for numbers/booleans.
87
- - `timeout` (ms) bounds the handler on *every* surface → 504; on the network path it also aborts `request().signal`.
88
- - `crossOrigin: true` exempts a mutating verb from the same-origin CSRF gate (non-GET/HEAD cross-origin browser requests are 403 by default).
89
- - `filesSchema` enables multipart upload: handler gets text fields ∩ validated `File` parts; call with a `FormData`.
90
- - `abide/shared/withJsonSchema(schema, toJsonSchema)` — attach `toJSONSchema()` to a schema whose library lacks one (feeds OpenAPI / MCP / CLI help).
91
-
92
- The reference each verb returns is a `RemoteFunction<Args, Return>` (see `src/lib/shared/types/RemoteFunction.ts`):
93
- plain call decodes the body + throws `HttpError` on non-2xx; `.raw` resolves to the
94
- `Response` undecoded; `.stream(args)` returns a `Subscribable` (SSE/JSONL frames, or
95
- the decoded body once) for `tail()`; `.fetch(req)` is framework-internal dispatch.
96
-
97
- ### Responses — `@documentation response`
98
- - `abide/server/json(data, init?)` — JSON with `Cache-Control: no-store`; `json(undefined)` → 204 that round-trips to `undefined`.
99
- - `abide/server/error(status, message?, init?)` — text/plain error; message defaults to the status reason phrase.
100
- - `abide/server/redirect(url, status=302, init?)` — accepts relative URLs (platform `Response.redirect` doesn't); 301/302/303/307/308.
101
- - `abide/server/jsonl(iterable, init?)` — wrap an `AsyncIterable` as JSON Lines stream; errors → final `{"$error":…}` line.
102
- - `abide/server/sse(iterable, init?)` — wrap an `AsyncIterable` as Server-Sent Events; 15s keepalive; errors → `event: error`.
103
- - `abide/shared/HttpError` — thrown by remote calls on non-2xx; carries `status`, `statusText`, raw `response`.
104
-
105
- ### Sockets — `@documentation sockets`
106
- - `abide/server/socket(opts?)` — declare a `Socket<T>` (an isomorphic `AsyncIterable<T>`) inside `src/server/sockets/<name>.ts`. Opts: `{ schema?, tail?, ttl?, clientPublish?, clients? }`. `tail: n` retains last n frames; `ttl` evicts older. HTTP face at `/__abide/sockets/<name>`: GET = retained tail, POST = publish (gated by `clientPublish`).
107
-
108
- ### Agents — `@documentation agent`
109
- - `abide/server/agent(engine, messages)` — run a model engine against the app's own (already-gated) MCP surface; returns the engine's `AgentFrame` stream. Wrap in `jsonl()`/`sse()` to pick the transport. Types exported: `NeutralMessage`, `AgentFrame` (`text`/`tool_use`/`tool_result`/`done`), `AgentSurface`, `AgentEngine`. Engines live in `@abide/<provider>` packages, never in core.
110
-
111
- ### Request scope — `@documentation request-scope`
112
- Resolve only during an in-flight SSR render or RPC handler (throw outside one):
113
- - `abide/server/request()` → the inbound `Request`.
114
- - `abide/server/cookies()` → Bun `CookieMap`; writes flush as `Set-Cookie` on return.
115
- - `abide/server/server()` → active `Bun.serve` (or a no-op in-process server under CLI/MCP/test).
116
-
117
- ### Config / data — `@documentation configuration` / `reference`
118
- - `abide/server/env(schema, opts?)` — validate `Bun.env` against a Standard Schema at module top level; fails boot loudly. Registered so the bundle launcher derives its setup form.
119
- - `abide/server/appDataDir()` → the running bundle's per-user data dir (cwd-independent, pure).
120
-
121
- ### Observability — `@documentation observability`
122
- - `abide/server/reachable(host)` — server-only outbound reachability; first call probes (HEAD), then warm-polls every TTL. Tuned by `ABIDE_REACHABLE_TTL` / `ABIDE_REACHABLE_TIMEOUT`.
123
- - `abide/shared/health()` → `HealthState` (framework + app `health()` hook payload, typed via generated `health.d.ts`); also at `/__abide/health`.
124
- - `abide/shared/log` — unified logger carrying request-scope context; `log()/warn/error/trace` on the app channel, `log.channel(name)` on a `DEBUG`-gated channel. `ABIDE_LOG_FORMAT=json` for JSON lines.
125
- - `abide/shared/trace()` → current W3C `traceparent` (isomorphic; client reads the SSR-stamped trace).
126
-
127
- ### App / inspector types — `@documentation plumbing`
128
- - `abide/server/AppModule` — shape of `src/app.ts` (error handler, health hook, lifecycle).
129
- - `abide/server/InspectorContext` — inspector wiring type.
130
- - `abide/server/rpc/defineVerb`, `abide/server/sockets/defineSocket`, `abide/server/prompts/definePrompt`, `abide/server/prompts/renderPromptTemplate` — bundler-emitted runtime bindings; you don't call these by hand (the `GET`/`socket`/`.md` files compile down to them).
32
+ The bundler reads these paths; their location is their identity (no manifest to register).
131
33
 
132
- ---
34
+ | Path | Meaning |
35
+ | ------------------------------ | ------------------------------------------------------------------------- |
36
+ | `src/server/rpc/<name>.ts` | One verb export per file; export name is the URL stem (`/rpc/<name>`) |
37
+ | `src/server/sockets/<name>.ts` | One `socket()` export per file; topic `<name>`, ws at `/__abide/sockets` |
38
+ | `src/mcp/prompts/<name>.md` | MCP prompt: frontmatter (description + args) + `{{placeholder}}` body |
39
+ | `src/mcp/resources/<name>` | MCP resource files; embedded in the standalone binary |
40
+ | `src/server/config.ts` | Optional `env(schema)` validation, eager-imported at boot |
41
+ | `src/app.ts` | Optional `AppModule` hooks (`init`/`handle`/`handleError`/`health`) |
42
+ | `src/bundle/window.ts` | Optional desktop `BundleWindow` default export |
43
+ | `src/ui/pages/**/page.abide` | A route; URL is the folder path |
44
+ | `src/ui/pages/**/layout.abide` | Wraps every page at/below its folder; nested chains apply outermost-first |
45
+ | `src/ui/app.html` | Optional custom shell; entry refs rewritten to hashed names |
46
+ | `src/ui/public/` | Static assets served at site root; embedded when compiled |
47
+ | `src/.abide/*.d.ts` | Framework codegen (`routes`/`rpc`/`sockets`/`publicAssets` types) |
48
+ | `dist/_app/` | Client bundle output: hashed chunks + CSS, optional `.gz` siblings |
49
+
50
+ ## CLI
51
+
52
+ | Command | Does |
53
+ | -------------------------------------------- | ----------------------------------------------------------------------------------------------- |
54
+ | `abide scaffold <name>` | Scaffold a project, install deps, and (in a TTY) start dev; `--no-install` / `--no-dev` opt out |
55
+ | `abide dev` | Build the client + run the server with hot reload, watching `src/` |
56
+ | `abide build` | One-shot client build into `dist/_app/` (no server) |
57
+ | `abide start` | Run the production server against a built `dist/` |
58
+ | `abide run <file> [args]` | Run a script under the abide preload (`.abide` compile, `$server` resolution) |
59
+ | `abide compile [--target] [--out]` | Build a standalone server executable (bytecode, embedded assets) |
60
+ | `abide cli [--target] [--out] [--platforms]` | Build the thin remote-client+server CLI binary, optionally cross-compiled |
61
+ | `abide bundle` | Build a self-contained desktop app bundle for this platform |
62
+ | `abide check` | Type-check all `.abide` templates and props; non-zero on errors |
63
+ | `abide lsp` | Run the `.abide` language server over stdio |
64
+ | `abide init-agent` | Write/refresh a `CLAUDE.md` pointer to this surface map |
65
+
66
+ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = ["@abide/abide/preload"]` in `bunfig.toml`'s `[test]` block.
67
+
68
+ ## Server surface — `abide/server/*`
69
+
70
+ ### RPC verbs — @documentation rpc
71
+
72
+ - `abide/server/GET`, `abide/server/POST`, `abide/server/PUT`, `abide/server/PATCH`, `abide/server/DELETE`, `abide/server/HEAD` — declare a verb: `VERB(handler, opts?)`; the preload rewrites each to `defineVerb(method, url, …)` (server) or `remoteProxy` (client). `opts`: `inputSchema`, `outputSchema`, `filesSchema`, `clients`, `crossOrigin`, `maxBodySize`, `timeout`. The returned function is callable in-process and exposes `.raw(args)` (the `Response`), `.stream(args)` (frame iterable), and `.fetch(request)` (router entry).
73
+ - `abide/server/rpc/defineVerb` — the rewrite target the verb helpers expand to: validates args against `inputSchema` (422 on issues), enforces `timeout` (504 + abort), and registers the verb for MCP/CLI/OpenAPI/inspector discovery.
74
+
75
+ ### Responses — @documentation response
76
+
77
+ - `abide/server/json` — `json(data, init?)`: `application/json`, `no-store`; `undefined` → `204` so `T | undefined` round-trips.
78
+ - `abide/server/jsonl` — `jsonl(asyncIterable, init?)`: newline-delimited JSON frames; errors emit a final `{"$error":…}` line.
79
+ - `abide/server/sse` — `sse(asyncIterable, init?)`: `text/event-stream` with 15s keepalives; errors emit an `event: error` frame.
80
+ - `abide/server/error` — `error(status, message?, init?)`: `text/plain` error `Response` typed `never` so error branches don't widen the inferred return.
81
+ - `abide/server/redirect` — `redirect(url, status=302, init?)`: redirect `Response` (accepts relative URLs), typed `never`.
82
+ - `abide/shared/HttpError` — `new HttpError(response)`: carries `status`/`statusText`/`response`; thrown by a proxy call on a non-2xx.
83
+
84
+ ### Request scope — @documentation request-scope
85
+
86
+ - `abide/server/request` — `request()`: the inbound `Request` for the current SSR/RPC pass (`AsyncLocalStorage`); throws outside a request scope.
87
+ - `abide/server/cookies` — `cookies()`: live `Bun.CookieMap` with `.set`/`.delete` writing `Set-Cookie`.
88
+ - `abide/server/server` — `server()`: the active `Bun.serve` instance (an in-process stand-in under CLI/MCP/test dispatch).
89
+
90
+ ### Configuration — @documentation configuration
91
+
92
+ - `abide/server/env` — `env(schema)`: validate `Bun.env` against a Standard Schema at module top level (boot fails loudly); also feeds the bundle setup form.
93
+
94
+ ### Sockets — @documentation sockets
95
+
96
+ - `abide/server/socket` — `socket({ schema?, tail?, ttl?, clientPublish?, clients? })`: declare a broadcast topic. Returns a `Socket<T>` (isomorphic `AsyncIterable<T>`) with `.publish(msg)` and `.tail(count?, hooks?)`. `tail` retains frames for late joiners, `ttl` lazily evicts old ones, `clientPublish` (default `false`) gates the HTTP/ws publish, `schema` validates publishes and flips on the MCP/CLI read faces.
97
+ - `abide/server/sockets/defineSocket` — the server-side construction the preload rewrites `socket()` to; owns the retained buffer, fan-out via `server.publish`, and per-subscriber queues.
98
+
99
+ ### App, agent, inspector — @documentation plumbing
100
+
101
+ - `abide/server/AppModule` — the type of `src/app.ts`'s optional hooks: `init` (boot + cleanup), `handle` (middleware), `handleError`, `health` (merges into `/__abide/health`), `forwardHeaders`.
102
+ - `abide/server/agent` — `agent(engine, messages)`: run a model engine against the current request's MCP surface (caller auth forwarded into every tool call), yielding neutral `AgentFrame`s to wrap in `jsonl`/`sse`.
103
+ - `abide/server/InspectorContext` — the capability bundle (`app`, `loadSurface`, `cacheSnapshot`, `onRecord`) handed to `@abide/inspector` when `ABIDE_ENABLE_INSPECTOR=true`.
104
+
105
+ ### Prompts — @documentation plumbing
106
+
107
+ - `abide/server/prompts/definePrompt` — `definePrompt(name, { description?, jsonSchema?, render })`: the target the resolver compiles `src/mcp/prompts/<name>.md` to; registers an MCP prompt.
108
+ - `abide/server/prompts/renderPromptTemplate` — `renderPromptTemplate(template, args)`: substitute `{{name}}` placeholders (missing → empty string).
133
109
 
134
110
  ## Isomorphic surface — `abide/shared/*`
135
111
 
136
- ### Cache — `@documentation cache`
137
- - `abide/shared/cache(fnOrProducer)` → a memoised callable. Key auto-derives (method+url+args for a remote fn; producer-reference+args for a producer). Options `{ ttl?, tags?, global?, swr? }`: `ttl` ms-past-resolve (omit = forever, `0` = dedupe-only / the **mutation idiom**); `tags` for group invalidation; `global` = process-level store (server); `swr` = stale-while-revalidate — on invalidate, keep the stale value (reader sees `refreshing()`, not `pending()`) and refetch in the background. `swr: true` refetches immediately; `swr: { throttle | debounce }` adds a coalescing window. GET-only, throws on a write.
138
- - `cache.invalidate(selector?, args?)` — end retention early (by fn, fn+args, `{ tags }`, or all).
139
- - `cache.patch(selector, updater, args?)` — optimistic local fold over matching entries.
140
- - `cache.on(subscribable, (frame, ctx) => …)` — event-driven cache maintenance: run a handler per socket/stream frame; `ctx.invalidate` / `ctx.patch` are coverage-tracked and resync on reconnect. Client-only (no-op on server).
112
+ ### Cache — @documentation cache
141
113
 
142
- ### Probes`@documentation probes` (read-only; reading never triggers work)
143
- - `abide/shared/pending(selector?, args?)` — "no value yet" (in-flight call, or stream awaiting first frame).
144
- - `abide/shared/refreshing(selector?, args?)` — "holding a value while a fresher source is in flight".
145
- - `abide/shared/online()` — reactive connectivity probe (server: the calling client's reported state via offline header).
114
+ - `abide/shared/cache``cache(fn)`: a memoizing invoker. `cache(fn)(args)` reads (SSR-snapshotted, hydrated warm); `.raw`/`.stream` mirror the verb; `.invalidate(selector?, args?)` drops entries; `.patch(selector, updater, …)` optimistic-updates; `.on(subscribable, handler)` folds socket frames into cached values.
146
115
 
147
- Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See CONTEXT.md → Probe.
116
+ ### Probes @documentation probes
148
117
 
149
- ### Routing / page `@documentation page` / `url`
150
- - `abide/shared/page` — reactive proxy: `route`, `params`, `url` (browser-space), `navigating`. Isomorphic; re-runs reactive readers on navigation.
151
- - `abide/shared/url(path, query?)` — type-safe URL builder; augmented by generated `RpcRoutes`/`PageRoutes`/`PublicAssets` for autocomplete. Applies mount base.
118
+ - `abide/shared/pending` — `pending(fn?, args?)`: `true` while a matching call is unsettled (read-only probe).
119
+ - `abide/shared/refreshing` — `refreshing(fn?, args?)`: `true` while a matching entry holds a value but a fresher fetch is in flight.
120
+ - `abide/shared/online` — `online()`: `navigator.onLine` (client) / offline-header (SSR); reactive.
152
121
 
153
- ### Templating values `@documentation plumbing`
154
- - `abide/shared/html` — `html\`...\`` tagged template → `RawHtml` (trusted markup); `rawHtmlString(v)`.
155
- - `abide/shared/snippet(payload)` → `Snippet` — a reusable template builder (the `<template name=…>` form).
156
- - `abide/shared/createSubscriber(start)` — the subscription primitive behind sockets/streams/probes.
122
+ ### Templating — @documentation templating
157
123
 
158
- ---
124
+ - `abide/shared/html` — `html\`…\``(or`html(str)`): mark a string as trusted raw HTML so interpolation injects rather than escapes.
125
+ - `abide/shared/snippet` — `snippet(payload)`: brand a payload so an interpolation mounts it instead of escaping (the runtime side of `<template name>`).
126
+ - `abide/shared/withJsonSchema` — `withJsonSchema(schema, toJsonSchema)`: attach a JSON Schema to a Standard Schema for OpenAPI/MCP/CLI/form projection.
127
+
128
+ ### Page & URL — @documentation page · url
129
+
130
+ - `abide/shared/page` — `page`: a reactive proxy of `route`/`params`/`url`/`navigating` (server reads request scope, client reads the router).
131
+ - `abide/shared/url` — `url(path, ...args)`: typed, base-correct URL builder for RPC paths, page routes, and assets.
132
+
133
+ ### Observability — @documentation observability
134
+
135
+ - `abide/shared/health` — `health()`: reactive `{ reachable, … }` polled from `/__abide/health`, composing app `health()` fields and `navigator.onLine`.
136
+ - `abide/shared/log` — `log`/`.warn`/`.error`/`.trace(label, fn)` on the always-on channel; `log.channel(name)` is `DEBUG`-gated; TSV by default, JSON under `ABIDE_LOG_FORMAT=json`.
137
+ - `abide/shared/trace` — `trace()`: the W3C `traceparent` for the current request, or `undefined` outside scope.
138
+ - `abide/shared/createSubscriber` — `createSubscriber(start)`: open-on-first-read / close-on-last-reader resource lifecycle for reactive sources.
139
+ - `abide/server/reachable` — `reachable(host)` (server-only): cached liveness probe; HEADs the origin and background-polls per `ABIDE_REACHABLE_TTL`, so reads resolve instantly off the warm value.
159
140
 
160
141
  ## UI surface — `abide/ui/*` (client-only)
161
142
 
162
- ### Reactive primitives`@documentation plumbing` (declared through `scope()` in a `.abide` `<script>`; the runtime forms are NOT public exports)
163
- Reactive state is owned by a scope and declared explicitly through it: `const count = scope().state(0)` (or a captured handle: `const s = scope(); const count = s.state(0)`). A **bare** `state(...)` / `linked(...)` / `computed(...)` call is a compile error — the surface always shows the scope interaction. The declared binding is then read/written by its bare name (`{count}`, `count = count + 1`); the compiler lowers that to the doc API.
164
- - `scope().state(initial, transform?)` — local truth. Plain `state(x)` lowers to a serializable `model` (= `scope()`) doc slot. `state(x, transform)` is a write-coercion gate (`.value` cell).
165
- - `scope().linked(seed, transform?)` — a `.value` cell seeded reactively from upstream (Angular's `linkedSignal`): owns a local value, reseeds when the `seed` thunk's deps change, edits stay local. Non-serializing — reseeds on resume.
166
- - `scope().computed(compute)` → a read-only computed **doc slot** (`scope().derive("name", compute)`), referenced as `name()` (string-free reader): re-computed on resume, never serialized/journalled. Always read-only (no `set` overload) — a writable computed is expressed at the binding site (`bind:value={{ get, set }}`), where the write actually originates.
167
- - `const { a = default, b: alias } = props<Shape>()` → the prop reader (the only one): destructure the parent-passed props; each binding is a read-only computed doc slot over its parent thunk (read as `a()`), an `= default` supplies the fallback when the prop is absent, `b: alias` renames. The optional `<Shape>` type argument is the parent-facing prop shape (default `Record<string, any>`). Bare `prop(...)` is a compile error pointing here.
168
- - `abide/ui/effect(fn)` → run now, re-run on dependency change; `fn` may return teardown / be async; returns dispose. (`effect` IS still a public export.)
169
- - `abide/ui/scope(address?)` → `Scope` — **the data + capability seam; the only public way to reach reactive state.** The reactive document (immutable path-addressed tree, every change a patch — JSON Pointer keys via `escapeKey`, so a key may contain `/`) is now *internal* (`doc`/`createDoc`): a scope wraps one and mirrors its interface — `read`/`replace`/`add`/`remove`/`cell`/`derive`/`snapshot` (data; `derive(path, compute)` is a computed slot, never stored/serialized/journalled), `child`/`root`/`parent` (tree), `dispose`. Every applied patch is announced on an internal process-global patch bus (`PATCH_BUS`) carrying `{ doc, patch, inverse }` — the single tap undo / persistence / sync consume; computed recomputes never reach it. Capabilities route where the scope's changes go — `record({ limit? })` to an undo journal (then `undo`/`redo`/`canUndo`/`canRedo`), `persist(key?)` to durable storage (key defaults to the scope `id`), `broadcast(transport)` to peers. A passable value (`<Child parentScope={scope}/>`). `scope()` = the ambient current scope — `mount`/`hydrate` (client) and `enterScope`/`exitScope` (SSR, per render) establish one per component; the lowering emits `const model = scope()`, so `scope()`'s data IS the component's `state`; `scope('/')` = the root. Resolves correctly inside event handlers, effects, and attach teardowns (they pin the scope they were registered under).
170
-
171
- ### Local-first queue — `@documentation ui`
172
- - `abide/ui/outbox({ key, send, store?, online?, onDrop? })` → `Outbox<T>` — durable FIFO queue for local-first writes; distinct from `scope().broadcast()` (which replicates *state*, not outbound mutations). `enqueue(payload)` records a serializable mutation (persisted via `persist`, so it survives a reload) and drains FIFO while `online()`; success dequeues, offline-mid-send keeps the head and retries on reconnect (auto-drains on the online edge), an online rejection drops the entry and reports via `onDrop` (roll back the optimistic change there). At-least-once → `send` should be idempotent. `pending()` is a reactive read for a sync indicator. Built on `doc` + `persist` + `online` (the queue is itself a persisted doc).
173
-
174
- ### Streams
175
- - `abide/ui/tail(subscribable)` → latest frame (`T | undefined`); `tail(x, { last: n })` → window `T[]`. Reactive latest-wins read of a socket/stream. `tail.status` / `tail.error` expose richer stream state. See CONTEXT.md → Tail.
176
-
177
- ### Navigation / lifecycle — `@documentation plumbing`
178
- - `abide/ui/navigate(path, replace?)` — client navigation.
179
- - `abide/ui/router(...)`, `abide/ui/startClient(...)`, `abide/ui/renderToStream(render)` — bootstrap/render runtime (compiler/launcher uses these).
180
-
181
- ### DOM + render runtime — `@documentation plumbing` (compiler-emitted; you don't hand-write these)
182
- `abide/ui/dom/{mount,mountChild,hydrate,text,appendText,appendTextAt,appendSnippet,appendStatic,cloneStatic,skeleton,anchorCursor,mountSlot,attr,on,attach,each,eachAsync,when,awaitBlock,tryBlock,switchBlock,applyResolved}` and `abide/ui/runtime/{nextBlockId,enterRenderPass,exitRenderPass}`. These are what `analyzeComponent → generateBuild/generateSSR` lower a `.abide` file into — every bound element builds through the parser-backed `skeleton` (one clone + located holes / `<!--a-->` anchors); there is no imperative element builder. Read them only to understand compiler output.
183
- - `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the browser-side implementations the bundler swaps in for `GET(...)` / `socket(...)`.
184
-
185
- ### `.abide` component format (see `src/lib/ui/README.md`)
186
- Valid HTML with `<script>` + native `<template>` control flow + scoped `<style>`.
187
- - **Bindings:** `{expr}` text, `name={expr}` attr, `onclick={fn}`, `bind:value={…}` / `bind:checked` / `bind:group`, `attach={fn}` (node-lifetime attachment — the dual of `on`; the `use:`-action / `{@attach}` equivalent, lowered to `ui/dom/attach`). A two-way `bind:` target is an **lvalue** (`count`, `model.lines[i]`) or an **accessor object** `bind:value={{ get, set }}` (reads via `get()`, writes via `set(v)`) — the only way to two-way-bind derived state; binding a read-only `computed` is a compile error.
188
- - **Control flow (native `<template>`):** `if` with a nested `else` (the `<template else>` is a CHILD of its `<template if>`, not a sibling), `each={list} as="x" key="x.id"`, `await={p}`/`then`/`catch`, `switch`/`case`/`default`.
189
- - **A branch is a lexical scope:** any control-flow branch may host its own nested `<script>` and `<style>`. The nested `<script>` declares branch-local **plain** signals (`state`/`computed`/`linked`) — owned by the branch's render scope, re-seeded each mount (not the serializable top-level `doc`) — with the branch's binding in scope (`then`/`catch`'s `as` value, the `each` `as`/`key` row), so it can derive from the awaited/iterated value; its bindings cover the branch subtree and later siblings auto-deref them. The nested `<style>` is scoped to that branch alone (its own `data-a-<hash>`), not the whole component.
190
- - **Components:** capitalised tags (`<Layout title=…>`); children fill `<slot/>`; props are reactive (passed as thunks). A component has no directives — every attribute is a prop under its written name (so `onclick=`/`bind:open=`/`attach=` pass through as props, e.g. callbacks, not the DOM-element directives those are on a lowercase tag) and is type-checked against the child's declared props. `const { name } = props<Shape>()` reads typed component props (parent-supplied thunks, reactive + read-only); route params come from the `page` proxy (`page.params.name`), not `props()`.
191
- - **Snippets / named slots:** `<template name="x" args={…}>` declares a reusable named builder (the `snippet()` form), rendered like a function — covers named slots / `{@render}`.
192
- - **Reactivity:** write plain assignment (`count += 1`, `items.push(x)`); the compiler lowers it to patches. Deep-field edits wake only that field.
193
- - **SSR:** byte-identical HTML string; `renderToStream` ships the shell then streams `<template await>` fragments out of order; `hydrate` adopts the server DOM in place — static structure, `if`/`else`, keyed `each`, `switch`, `try`, and child components/slots all claim existing nodes (no re-render; focus/scroll/input preserved). The one cold-rebuild case left is a genuinely-pending `await` that is neither resumable (`RESUME[id]`) nor cache-warm — it discards the boundary and builds the pending branch fresh; an `await` over a `cache`d read is warm on resume and adopts without a flash.
143
+ ### Reactive state@documentation reactive-state
194
144
 
195
- ---
145
+ - `abide/ui/scope` — `scope()`: the sole reactive surface. `.state(initial?, transform?)` (writable cell), `.computed(fn)` (read-only), `.linked(seed, transform?)` (local draft reseeded from upstream). Bare `state`/`computed`/`linked` are a compile error; a writable computed is expressed at the binding (`bind:value={{ get, set }}`). Also carries doc/persistence/undo/broadcast methods.
196
146
 
197
- ## Build / tooling `@documentation plumbing`
198
- - `abide/ui-plugin` — Bun loader plugin for `.abide` files.
199
- - `abide/resolver-plugin` — resolves the `$server`/`$ui`/virtual modules and swaps server↔client runtime per target.
200
- - `abide/preload` — Bun preload registering both plugins (use in `bunfig.toml` `[test]`).
201
- - `abide/build`, `abide/compile` — programmatic build / standalone-executable compile.
202
- - `abide/tsconfig` — base tsconfig for consumer apps.
147
+ ### Effect@documentation effect
203
148
 
204
- ## Desktop bundle `@documentation bundle`
205
- - `abide/bundle/BundleWindow` — `src/bundle/window.ts` default-export shape (`title`, `width`, `height`, `menu`, `config` schema for the first-run setup form).
206
- - `abide/bundle/BundleMenu`, `abide/bundle/BundleMenuItem` — custom menu types.
207
- - `abide/bundle/onMenu(handler)` / `onMenu(name, handler)` — subscribe to native menu clicks (returns unsubscribe; drop into an `effect`). Inert outside the bundle.
208
- - `abide/bundle/bundled()` — true inside the desktop webview (isomorphic detection).
149
+ - `abide/ui/effect``effect(fn)`: run now, re-run when a read cell changes; `fn` may be async and may return a teardown. Returns a disposer. In scope inside `.abide` without import.
209
150
 
210
- ## MCP`@documentation plumbing`
211
- - `abide/mcp/createMcpServer(opts)` — framework-internal; the `abide:mcp` virtual constructs it. Tools derive from verbs/sockets flagged `clients.mcp: true`; auth inherits from the inbound request; optional `authorize` hook. Served at `/__abide/mcp`.
151
+ ### Tail & navigate @documentation tail · navigate
212
152
 
213
- ## Testing`@documentation testing`
214
- - `abide/test/createTestApp()` `TestApp` with typed `RpcClient` / `SocketClient` — in-process dispatch, no network.
215
- - `abide/test/createScriptedSurface(...)` — records tool dispatches for engine tests.
216
- - `abide/test/assertAgentFrameConformance(...)` — the frame contract every engine must satisfy (exactly one `done`, last; every `tool_use` answered by a same-id/name `tool_result`).
153
+ - `abide/ui/tail``tail(subscribable)`: latest frame of a `Socket`/stream (reactive); window form `tail(s, { last })` → array; `tail.error`/`tail.status` probes.
154
+ - `abide/ui/navigate` `navigate(path, replace?)`: client navigation through the router.
217
155
 
218
- ---
156
+ ### Outbox — @documentation ui
157
+
158
+ - `abide/ui/outbox` — `outbox({ key, send, store?, online?, onDrop? })`: a durable FIFO mutation queue (`.enqueue`/`.pending`/`.flush`) for offline-tolerant writes.
159
+
160
+ ### Runtime & client entry — @documentation plumbing
161
+
162
+ - `abide/ui/enterScope`, `abide/ui/exitScope` — push/pop the lexical scope around an SSR render.
163
+ - `abide/ui/router`, `abide/ui/startClient`, `abide/ui/renderToStream` — the client router, the official client entry (reads `window.__SSR__`, seeds cache, starts the router), and the out-of-order SSR streamer.
164
+ - `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the bundler-emitted client substitutes that swap a server verb/socket import for a `fetch` proxy / ws-multiplexed `Socket`.
165
+ - `abide/ui/dom/*` — the compiled-template DOM runtime (one node-builder per construct): `mount`, `mountChild`, `mountSlot`, `hydrate`, `skeleton`, `cloneStatic`, `anchorCursor`, `text`, `appendText`, `appendTextAt`, `appendSnippet`, `appendStatic`, `attr`, `on`, `attach`, `each`, `eachAsync`, `when`, `awaitBlock`, `tryBlock`, `switchBlock`, `applyResolved`.
166
+ - `abide/ui/runtime/*` — render-pass helpers: `escapeKey` (JSON-Pointer key escaping), `nextBlockId`, `enterRenderPass`, `exitRenderPass`.
167
+
168
+ ## Build / tooling — @documentation building
169
+
170
+ - `abide/build` — `build({ cwd?, minify?, compress?, clean? })`: build the client bundle to `dist/_app`, emitting gzip siblings when `compress`.
171
+ - `abide/compile` — `compile({ cwd?, target?, outfile?, buildClient? })`: produce a standalone Bun server executable (bytecode, embedded compressed assets).
172
+ - `abide/preload` — the Bun preload (`bunfig.toml`) that installs the `.abide`, resolver, and CSS-noop plugins and rewrites verb/socket imports.
173
+ - `abide/resolver-plugin` — `abideResolverPlugin({ cwd?, embedAssets?, target? })`: virtualizes every generated module and rewrites verbs/sockets per side.
174
+ - `abide/ui-plugin` — `abideUiPlugin`: the Bun loader compiling `.abide` SFCs to ES modules (scoped `<style>` via virtual imports; `layout.abide` → `<abide-outlet>`).
175
+ - `abide/tsconfig` — `tsconfig.app.json` for consumers to extend (`bundler` resolution, `strict`, `noEmit`, `bun` types).
176
+
177
+ ## Desktop bundle — @documentation bundle
178
+
179
+ - `abide/bundle/BundleWindow` — the type for `src/bundle/window.ts`'s default export: `{ title?, width?, height?, menu?, config? }`, baked into the launcher.
180
+ - `abide/bundle/BundleMenu`, `abide/bundle/BundleMenuItem` — a custom top-level menu and its entries (`separator` / `emit` a `abide:menu` event / `navigate`; optional Cmd `shortcut`).
181
+ - `abide/bundle/onMenu` — `onMenu(handler)` / `onMenu(name, handler)`: subscribe to menu clicks (inert in SSR and plain browser tabs); returns an unsubscribe.
182
+ - `abide/bundle/bundled` — `bundled()`: `true` when running inside the abide desktop bundle (isomorphic).
183
+ - `abide/server/appDataDir` — `appDataDir()`: the per-user data dir keyed by program name (pure; where abide stores `.env` / last-connection).
184
+
185
+ ## MCP — @documentation mcp
186
+
187
+ - `abide/mcp/createMcpServer` — `createMcpServer(opts?)`: the framework-internal MCP server behind `/__abide/mcp`; tools derive from `clients.mcp` verbs (auto-on for read-only) and sockets (`<name>-tail`, `<name>-publish`), with inbound-request auth and an optional `authorize` hook.
188
+
189
+ ## Testing — @documentation testing
190
+
191
+ - `abide/test/createTestApp` — `createTestApp(cwd?)`: a test harness with typed `app.rpc.<verb>` / `app.sockets.<name>` maps over the project's virtual modules.
192
+ - `abide/test/createScriptedSurface` — `createScriptedSurface(tools?)`: a scripted `AgentSurface` recording every tool `call` for agent-engine tests.
193
+ - `abide/test/assertAgentFrameConformance` — `assertAgentFrameConformance(stream)`: assert a frame stream meets the neutral `AgentFrame` contract (one terminal `done`, paired `tool_use`/`tool_result`).
194
+
195
+ ## Generated machine surfaces
196
+
197
+ | Route | Purpose |
198
+ | ---------------------------------- | ------------------------------------------------------------------------------- |
199
+ | `GET /openapi.json` | OpenAPI spec of the public RPC surface, built on first request |
200
+ | `POST /__abide/mcp` | MCP JSON-RPC endpoint (tools from verbs/sockets, prompts, resources) |
201
+ | `GET/POST /__abide/sockets` | WebSocket multiplex upgrade (one connection per client) |
202
+ | `GET/POST /__abide/sockets/<name>` | A socket's HTTP face: `GET` retained tail, `POST` publish |
203
+ | `GET /__abide/health` | Health probe JSON (identity + app `health()` fields), pre-auth |
204
+ | `GET /__abide/identity` | Legacy-shaped alias of the health payload |
205
+ | `GET /__abide/cli` | Platform-detecting install script; `/__abide/cli/<platform>` streams the binary |
206
+ | `GET /__abide/inspector` | Opt-in inspector UI + data (when enabled and `@abide/inspector` installed) |
207
+ | `GET /__abide/hot/<id>` | Dev-only component hot-module fetch (`.abide` HMR) |
208
+
209
+ ## Environment variables
219
210
 
220
- ## Generated machine surfaces (runtime, from the user's app)
221
- - `/openapi.json` full OpenAPI for every exposed verb.
222
- - `/__abide/mcp` MCP endpoint (tools/sockets/prompts).
223
- - `/__abide/health` health payload. `/__abide/identity` app identity.
224
- - `/__abide/sockets` ws multiplex (per-socket HTTP face at `/__abide/sockets/<name>`).
225
- - `/__abide/cli` CLI dispatch endpoint. `/__abide/hot` dev hot-reload. `/__abide/inspector` inspector stream (gated by `ABIDE_ENABLE_INSPECTOR=true`).
226
-
227
- ## Environment variables (consumer-facing)
228
-
229
- | Var | Effect |
230
- |---|---|
231
- | `PORT` | TCP port the server binds |
232
- | `APP_URL` | public URL; its pathname becomes the mount base (e.g. `…/v2` → `/v2`) |
233
- | `ABIDE_APP_URL` / `ABIDE_APP_TOKEN` | remote server URL + bearer token for the CLI client / bundle |
234
- | `ABIDE_DATA_DIR` | override the per-user data dir |
235
- | `ABIDE_CLIENT_TIMEOUT` | client-side RPC timeout (ms); distinct from per-verb `timeout` |
236
- | `ABIDE_IDLE_TIMEOUT` | Bun per-connection idle timeout (s) |
237
- | `ABIDE_MAX_REQUEST_BODY_SIZE` | server-wide request body ceiling |
238
- | `ABIDE_REACHABLE_TTL` / `ABIDE_REACHABLE_TIMEOUT` | `reachable()` poll cadence / per-HEAD bound (ms) |
239
- | `ABIDE_LOG_FORMAT` | `json` for one JSON object per log line (default: tsv) |
240
- | `ABIDE_ENABLE_INSPECTOR` | `true` to enable the inspector endpoint |
241
- | `ABIDE_INSPECT` | enable Bun inspector on the build |
242
- | `DEBUG` | enable diagnostic log channels (e.g. `abide:cache`, `abide:build`); browser uses the `abide-debug` localStorage key |
211
+ | Var | Effect |
212
+ | ----------------------------- | ----------------------------------------------------------------------- |
213
+ | `PORT` | Bind port; unset scans upward from 3000 |
214
+ | `APP_URL` | Base URL whose pathname becomes the mount base (default `/`) |
215
+ | `ABIDE_APP_URL` | Base URL baked into the downloaded CLI binary |
216
+ | `ABIDE_APP_TOKEN` | Bearer token baked into the CLI binary (only if the request was authed) |
217
+ | `ABIDE_CLIENT_TIMEOUT` | Client RPC timeout, ms (1–600000); unset is unbounded |
218
+ | `ABIDE_MAX_REQUEST_BODY_SIZE` | Server request-body ceiling (default ~128MB) |
219
+ | `ABIDE_IDLE_TIMEOUT` | Per-connection idle timeout, s (0–255, default 10) |
220
+ | `ABIDE_REACHABLE_TIMEOUT` | Per-probe `reachable()` timeout, ms (default 3000) |
221
+ | `ABIDE_REACHABLE_TTL` | `reachable()` poll cadence, ms (default 30000) |
222
+ | `ABIDE_DATA_DIR` | Override the app data dir (used as-is, no program name appended) |
223
+ | `ABIDE_LOG_FORMAT` | `json` emits log records as JSON instead of TSV |
224
+ | `DEBUG` | Gate diagnostic log channels (e.g. `abide`, `abide:build`, `-abide`) |
225
+ | `ABIDE_ENABLE_INSPECTOR` | `true` activates the opt-in inspector surface |
226
+ | `ABIDE_INSPECT` | Enable webview devtools in the desktop bundle |
243
227
 
244
228
  ---
245
229
 
246
- *Maintenance (abide repo only): this file mirrors `package.json` `exports`. After
247
- adding/renaming an export, run `bun run scripts/readmeSurfaces.ts` from
248
- `packages/abide/` (it lists every export by `@documentation` slug and fails on any
249
- untagged one) and reflect the change here.*
230
+ This map mirrors `package.json`'s `exports`. After adding or renaming an export, run `bun run packages/abide/scripts/readmeSurfaces.ts` to re-derive the slugs and catch any untagged export, then regenerate this file.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # abide
2
2
 
3
+ ## 0.36.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`ec48c80`](https://github.com/briancray/abide/commit/ec48c805b019a299cf193c0228646af18605938a) - scope-bound RPC abort + shared control-flow teardown ([`de3b2d4`](https://github.com/briancray/abide/commit/de3b2d44b944a0906eccab3be564caa96ada8bc1))
8
+
9
+ ### Patch Changes
10
+
11
+ - [`ec48c80`](https://github.com/briancray/abide/commit/ec48c805b019a299cf193c0228646af18605938a) - regenerate AGENTS.md and README for current surface ([`08b5fc9`](https://github.com/briancray/abide/commit/08b5fc9a1822dc6477963bdce2a0b9160ad9e323))
12
+
3
13
  ## 0.35.0
4
14
 
5
15
  ### Minor Changes