relembra 0.0.0-stage → 1.0.0-beta.2

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/dist/cli/args.d.ts +26 -0
  4. package/dist/cli/args.js +109 -0
  5. package/dist/cli/commands.d.ts +40 -0
  6. package/dist/cli/commands.js +114 -0
  7. package/dist/cli/confirm.d.ts +13 -0
  8. package/dist/cli/confirm.js +80 -0
  9. package/dist/cli/errors.d.ts +44 -0
  10. package/dist/cli/errors.js +109 -0
  11. package/dist/cli/main.d.ts +3 -0
  12. package/dist/cli/main.js +768 -0
  13. package/dist/cli/mcp-lines.d.ts +40 -0
  14. package/dist/cli/mcp-lines.js +87 -0
  15. package/dist/cli/mcp.d.ts +42 -0
  16. package/dist/cli/mcp.js +570 -0
  17. package/dist/cli/scope.d.ts +70 -0
  18. package/dist/cli/scope.js +325 -0
  19. package/dist/cli/stdin.d.ts +13 -0
  20. package/dist/cli/stdin.js +68 -0
  21. package/dist/cli/wake.d.ts +25 -0
  22. package/dist/cli/wake.js +46 -0
  23. package/dist/errors.d.ts +206 -0
  24. package/dist/errors.js +228 -0
  25. package/dist/guard.d.ts +14 -0
  26. package/dist/guard.js +218 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.js +10 -0
  29. package/dist/model.d.ts +280 -0
  30. package/dist/model.js +32 -0
  31. package/dist/ops.d.ts +93 -0
  32. package/dist/ops.js +129 -0
  33. package/dist/optmem.d.ts +50 -0
  34. package/dist/optmem.js +261 -0
  35. package/dist/pi-durable/index.d.ts +57 -0
  36. package/dist/pi-durable/index.js +225 -0
  37. package/dist/ports.d.ts +289 -0
  38. package/dist/ports.js +5 -0
  39. package/dist/postgres/client.d.ts +24 -0
  40. package/dist/postgres/client.js +14 -0
  41. package/dist/postgres/index.d.ts +69 -0
  42. package/dist/postgres/index.js +492 -0
  43. package/dist/postgres/migrations.d.ts +29 -0
  44. package/dist/postgres/migrations.js +188 -0
  45. package/dist/postgres/queries.d.ts +6 -0
  46. package/dist/postgres/queries.js +12 -0
  47. package/dist/postgres/term-index.d.ts +14 -0
  48. package/dist/postgres/term-index.js +107 -0
  49. package/dist/ranking.d.ts +57 -0
  50. package/dist/ranking.js +202 -0
  51. package/dist/relembra.d.ts +402 -0
  52. package/dist/relembra.js +1187 -0
  53. package/dist/render.d.ts +111 -0
  54. package/dist/render.js +265 -0
  55. package/dist/sqlite/database.d.ts +83 -0
  56. package/dist/sqlite/database.js +487 -0
  57. package/dist/sqlite/index.d.ts +32 -0
  58. package/dist/sqlite/index.js +484 -0
  59. package/dist/sqlite/migrations.d.ts +18 -0
  60. package/dist/sqlite/migrations.js +190 -0
  61. package/dist/sqlite/term-index.d.ts +33 -0
  62. package/dist/sqlite/term-index.js +139 -0
  63. package/dist/sqlite/wal-image.d.ts +9 -0
  64. package/dist/sqlite/wal-image.js +68 -0
  65. package/dist/testing/conformance.d.ts +46 -0
  66. package/dist/testing/conformance.js +2266 -0
  67. package/dist/testing/index.d.ts +3 -0
  68. package/dist/testing/index.js +2 -0
  69. package/dist/testing/memory-store.d.ts +3 -0
  70. package/dist/testing/memory-store.js +260 -0
  71. package/dist/transfer.d.ts +93 -0
  72. package/dist/transfer.js +769 -0
  73. package/dist/version.d.ts +2 -0
  74. package/dist/version.js +2 -0
  75. package/docs/FORMAT.md +610 -0
  76. package/package.json +96 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mneves75
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,329 @@
1
- # Temporary Holding Version
1
+ # relembra
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Memory for AI agents that stays scoped, bounded, durable, erasable and measured. A TypeScript
4
+ library with zero runtime dependencies, a SQLite store, and a CLI that any coding agent can call
5
+ from a session hook.
6
+
7
+ The host supplies identity (a policy that turns its own authenticated context into a principal)
8
+ and storage (an adapter). relembra enforces the rules:
9
+
10
+ - **Scoped.** Every memory belongs to a scope. A principal reads and writes only the scopes its
11
+ policy grants; `find`, `wake` and `brief` never return memories from any other scope.
12
+ - **Bounded.** A memory is one line of at most 280 bytes. `wake` (the block a session starts with)
13
+ and `brief` (a block for one topic) never exceed their byte budgets.
14
+ - **Curated.** A guard refuses credentials, multi-line text and invisible, bidirectional or
15
+ control characters before they become memories. Every memory carries provenance and a trust tier (`owner`,
16
+ `agent`, `untrusted`).
17
+ - **Durable.** An append-only event log with revisions, idempotent operations and an authority
18
+ fence for multi-writer hosts. SQLite runs in WAL mode with `synchronous=FULL`.
19
+ - **Erasable.** `retract` hides a memory; `redact` turns its text into a tombstone everywhere,
20
+ including the summaries derived from it, and then scrubs the SQLite WAL. Retention rules expire
21
+ memories when the host calls `sweep` on its own schedule; the library runs no timer. Renderings
22
+ given a carrier record which carriers received which memories, and `deliverErasures(hook)` hands
23
+ each redaction's carriers to the host until it answers acknowledged or unsupported per carrier;
24
+ relembra never reports an external copy as erased.
25
+ - **Measured.** An evaluation harness with leak, scope, retrieval and wake-coverage gates, plus
26
+ controls that must fail. Results: [docs/eval/](docs/eval/).
27
+
28
+ Version: 1.0.0-beta.2, a beta ([changelog](CHANGELOG.md),
29
+ [releases](https://github.com/mneves75/relembra/releases)). Release procedure:
30
+ [docs/RELEASE.md](docs/RELEASE.md).
31
+ Design: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
32
+ Vocabulary: [GLOSSARY.md](GLOSSARY.md).
33
+
34
+ ## Library
35
+
36
+ Requires Node 24.14 or later, or Bun 1.4 or later. The beta is on npm under the `beta` tag:
37
+
38
+ ```sh
39
+ npm install relembra@beta # or: bun add relembra@beta
40
+ ```
41
+
42
+ ```ts
43
+ import { createRelembra } from "relembra";
44
+ import { openSqliteStore } from "relembra/sqlite";
45
+
46
+ const store = openSqliteStore({ path: "memory.db" });
47
+ const memory = createRelembra({ store, policy: (context) => principalFor(context) });
48
+
49
+ await memory.note(context, { scope: "project-a", text: "Tests need TZ=UTC.", provenance });
50
+ const { text } = await memory.wake(context, { scope: "project-a" });
51
+ ```
52
+
53
+ The full walk-through (note, find, wake, brief, the guard, redact) runs as
54
+ `bun examples/quickstart.ts`.
55
+
56
+ Entry points:
57
+
58
+ | Import | What |
59
+ |---|---|
60
+ | `relembra` | `createRelembra`, the error types and the domain types |
61
+ | `relembra/sqlite` | `openSqliteStore`, the SQLite adapter (`node:sqlite`) |
62
+ | `relembra/postgres` | `openPostgresStore`, the Postgres adapter over a SQL client the host passes |
63
+ | `relembra/pi-durable` | `createMemoryExtension`, memory tools for a pi-durable Harness |
64
+ | `relembra/testing` | an in-memory store and the conformance suite a store adapter must pass |
65
+
66
+ Export, import and OptMem import: [docs/FORMAT.md](docs/FORMAT.md).
67
+
68
+ ### Postgres
69
+
70
+ To share one memory between machines, pass your own Postgres client; relembra installs no
71
+ driver. The client needs two methods: `query(text, params)` resolving rows, and
72
+ `transaction(work)` running `work` on one connection between BEGIN and COMMIT.
73
+
74
+ ```ts
75
+ import { SQL } from "bun";
76
+ import { openPostgresStore } from "relembra/postgres";
77
+
78
+ const sql = new SQL(process.env.DATABASE_URL);
79
+ const store = await openPostgresStore({
80
+ schema: "relembra", // created if missing, migrated on open
81
+ client: {
82
+ query: async (text, params = []) => [...(await sql.unsafe(text, [...params]))],
83
+ transaction: (work) =>
84
+ sql.begin((tx) => work({ query: async (t, p = []) => [...(await tx.unsafe(t, [...p]))] })),
85
+ },
86
+ });
87
+ ```
88
+
89
+ The same client over [`pg`](https://node-postgres.com) or [postgres.js](https://github.com/porsager/postgres)
90
+ (these two wrappers are not run by this repository's tests):
91
+
92
+ ```ts
93
+ // pg
94
+ const client = {
95
+ query: async (text, params) => (await pool.query(text, params)).rows,
96
+ async transaction(work) {
97
+ const connection = await pool.connect();
98
+ try {
99
+ await connection.query("BEGIN");
100
+ const result = await work({ query: async (t, p) => (await connection.query(t, p)).rows });
101
+ await connection.query("COMMIT");
102
+ return result;
103
+ } catch (error) {
104
+ await connection.query("ROLLBACK").catch(() => {});
105
+ throw error;
106
+ } finally {
107
+ connection.release();
108
+ }
109
+ },
110
+ };
111
+ // postgres.js
112
+ const client = {
113
+ query: (text, params) => sql.unsafe(text, params),
114
+ transaction: (work) => sql.begin((tx) => work({ query: (t, p) => tx.unsafe(t, p) })),
115
+ };
116
+ ```
117
+
118
+ Writes from every machine run one at a time behind a lock row, and are dated by the database
119
+ clock. A redaction removes the text from every row relembra keeps; Postgres keeps old row
120
+ versions in its files until VACUUM, and the WAL and backups keep theirs, which are the database
121
+ operator's to erase. TLS on the connection, encryption of the database and its backups, and
122
+ auditing who reads or exports what are the host's and the operator's too: relembra's event log
123
+ records changes, not reads.
124
+
125
+ ### Sharing across machines
126
+
127
+ [`examples/shared-postgres`](examples/shared-postgres) is two minimal hosts sharing one memory.
128
+ Each runs as its own process with its own policy, principal and connection pool; only the
129
+ database links them. Host A reads, writes and owns `shared` and `private-a`; host B reads and
130
+ writes `shared` and owns `private-b`. Each finds what the other notes in `shared`, neither sees
131
+ the other's private scope, and once A redacts a shared memory B's view no longer holds its text
132
+ (`test/postgres-sharing.test.ts`). From a clone of the repository:
133
+
134
+ ```sh
135
+ export DATABASE_URL=postgres://user:password@db.example:5432/app
136
+ bun examples/shared-postgres/host-a.ts note shared "Releases need two approvals."
137
+ bun examples/shared-postgres/host-b.ts find approvals
138
+ ```
139
+
140
+ Trust: each host's policy decides which principal it acts as, and relembra enforces that
141
+ principal's scopes on every call made through it. The database credentials are not scoped.
142
+ Whoever holds them can read, change or delete every scope's memories directly, and relembra's
143
+ scope rules do not apply to them. In the example both hosts hold the same credentials, so
144
+ `private-a` is hidden from host B's relembra calls, not from whoever runs host B. To keep a scope
145
+ from a machine, do not give that machine the credentials.
146
+
147
+ The CLI stays SQLite-only: no flag, environment variable or configuration points it at Postgres.
148
+ A Postgres configuration for the CLI would be a change of its own.
149
+
150
+ ### pi-durable
151
+
152
+ `relembra/pi-durable` gives a [pi-durable](https://github.com/earendil-works/pi) Harness the tools
153
+ `memory_note`, `memory_find` and `memory_brief`, and a system prompt section carrying the wake,
154
+ under the key `relembra_memory` (`MEMORY_SECTION_KEY`; section keys are shared by every extension). pi-durable is an optional peer dependency, pinned to exactly `1.4.0`, the version the tests
155
+ run against.
156
+
157
+ ```ts
158
+ import { createMemoryExtension } from "relembra/pi-durable";
159
+
160
+ registry.install(
161
+ createMemoryExtension({
162
+ relembra: memory,
163
+ hostNamespace: "host-1/agent.sqlite", // one per pi-durable storage
164
+ resolve: (conversationId) => ({ context, scope: "project-a", provenance }),
165
+ }),
166
+ );
167
+ ```
168
+
169
+ - **Crash-safe writes.** The tools are `replay: "safe"`. `memory_note`'s op id hashes the host
170
+ namespace with the call's conversation, task and call ids, so a call that pi-durable reruns
171
+ after a crash returns the memory, id and timestamp its first run committed.
172
+ - **One owner per storage.** pi-durable has no cross-process locking: one process owns a storage
173
+ at a time. pi-durable numbers conversations and tasks per storage, so each storage needs its own
174
+ `hostNamespace`; an independently writable copy gets a new one.
175
+ - **Takeover needs a fencing epoch.** To move the authority to another process, give the host's
176
+ principal an authority fence and call `advanceFence(store, { key, epoch })` on takeover. A
177
+ paused writer that resumes with the old epoch commits nothing.
178
+ - **Egress.** `memory_find`, `memory_brief` and the wake go through the host's egress filter and
179
+ byte budget. A refusal (guard, fence, egress) is an error result for the model. Each model request,
180
+ a resumed one included, carries the wake rendered for it, once; a wake the host refuses shows
181
+ `<wake unavailable="true"/>`, so a revoked wake is not resent from the history. Compaction
182
+ requests leave out the conversation's system messages, the wake included; they carry only
183
+ pi-durable's own summarization prompt and the serialized transcript.
184
+ - **Bounded errors.** Invalid tool arguments get a fixed error that quotes none of the input.
185
+ - **Erasure.** Every rendering a conversation receives is recorded for the carrier
186
+ `<hostNamespace>/<conversationId>` (kind `pi-durable`) first, so `hostNamespace` must pass the
187
+ identifier rule within 512 bytes. After a redaction, `memory.deliverErasures(hook)` hands the
188
+ host each conversation that held the text.
189
+ - **No way back past pi-durable 1.4.0.** Opening a pi-durable SQLite storage with 1.4.0 migrates
190
+ it to schema version 2, which 1.3.0 and earlier cannot open (pi-durable's CHANGELOG, 1.4.0).
191
+
192
+ ## CLI
193
+
194
+ A single binary over the SQLite store, built with Bun. The scope comes from the git repository
195
+ (or `RELEMBRA_SCOPE`, set by the host) and the principal is the OS user; no argument changes
196
+ either. Writes carry `agent` trust at most, and owner trust needs a person typing a code at the
197
+ terminal. It is a single-user model: anything running as the same OS user has the same reach.
198
+
199
+ `RELEMBRA_GLOBAL_SCOPE` names one more scope every session reads, for rules and lessons that
200
+ should reach every project: `wake`, the session hook, `find`, `brief`, `recall` and `zoom` (and
201
+ the MCP server's reads) cover it beside the project's scope, while writes stay in the project's
202
+ scope. Outside a repository it is the scope itself. The wake gives each of the two scopes half
203
+ its budget and passes the room one leaves unused to the other, so neither can crowd the other
204
+ out. Set `RELEMBRA_SCOPE` to the global scope to change what it holds.
205
+
206
+ Each [GitHub release](https://github.com/mneves75/relembra/releases) carries a native binary
207
+ per platform, `SHA256SUMS` and the library archive npm serves:
208
+
209
+ | Platform | Binary |
210
+ |---|---|
211
+ | macOS arm64 | `relembra-darwin-arm64` |
212
+ | Linux x64 (glibc) | `relembra-linux-x64` |
213
+ | Linux arm64 (glibc) | `relembra-linux-arm64` |
214
+ | Windows x64 | `relembra-windows-x64.exe` |
215
+ | Windows arm64 | `relembra-windows-arm64.exe` |
216
+
217
+ On macOS and Linux, download the binary and checksum file, then install them with `install.sh`
218
+ from a checkout of the same tag:
219
+
220
+ ```sh
221
+ sh install.sh relembra-linux-x64 SHA256SUMS # verifies the checksum, installs to ~/.local/bin
222
+ ```
223
+
224
+ On Windows, check the hash with `Get-FileHash` and copy the binary onto `PATH` as
225
+ `relembra.exe`; the session hook needs Git Bash ([docs/CLI.md](docs/CLI.md#windows)).
226
+
227
+ Before replacing an installed executable, retain a backup and its hash. Verify installed bytes
228
+ against the release checksum and export metadata from a disposable store
229
+ (`relembra: "1.0.0-beta.2"`); there is no `--version` flag. The [release procedure](docs/RELEASE.md) covers isolated checks and
230
+ rollback without opening a live memory store. After verification, `relembra init` prints the
231
+ session hook for your agent.
232
+
233
+ To build it yourself, run `bun run build:cli` from a clone with the pinned Bun
234
+ (`build/bun-toolchain.txt`); it writes `bin/relembra` (`relembra.exe` on Windows) and
235
+ `bin/relembra.sha256`.
236
+
237
+ Commands, flags, exit codes and the JSON envelope: [docs/CLI.md](docs/CLI.md).
238
+
239
+ ## MCP
240
+
241
+ `relembra mcp` is an MCP server over stdio for clients that speak protocol revision 2026-07-28
242
+ (older revisions are refused with the supported list). It is a subcommand of the same binary,
243
+ so it takes its scope, principal and store from the launch environment exactly as the CLI does,
244
+ and no tool argument changes them. It writes at `agent`
245
+ trust at most and never asks for owner trust. Its tools are `note`, `find`, `brief`, `wake` and
246
+ `retract`; every tool that returns memory text returns an escaped, egress-filtered rendering
247
+ within a byte budget, recorded for this server process as a carrier (`mcp:<random UUID>`), which a
248
+ redaction then lists as pending: the server cannot make a client forget, and never says it did.
249
+ Register it with your client as the command `relembra mcp`, started in the
250
+ project's directory or with `RELEMBRA_SCOPE` set. The contract (methods, tools, op ids, errors,
251
+ framing, pending requests) is in [docs/CLI.md](docs/CLI.md#relembra-mcp).
252
+
253
+ The server holds at most 64 requests queued or running. One more gets the error `-31000`,
254
+ `data: { maxPending: 64 }`, at once and is never queued. The revision defines no overload code and
255
+ reserves `-32020` to `-32099` for its own, so `-31000` sits outside `-32768` to `-32000`, as its
256
+ Error Codes section asks. A client that stops reading its responses stops being read.
257
+
258
+ Qualified with the official MCP TypeScript SDK client, `@modelcontextprotocol/client` **2.4.0**
259
+ (with `@modelcontextprotocol/core` 2.4.0), pinned to protocol revision 2026-07-28
260
+ (`versionNegotiation: { mode: { pin: "2026-07-28" } }`): `test/cli/mcp-sdk.test.ts` runs a stdio
261
+ round trip through it against the built binary. It is a development dependency only; the library
262
+ and the binary have no runtime dependencies. A client that speaks only the earlier,
263
+ `initialize`-based revisions cannot use this server: `initialize` is refused with `-32022`, which
264
+ names the supported version.
265
+
266
+ Implemented from these pages of the specification (read 2026-10-31):
267
+
268
+ - [Key Changes](https://modelcontextprotocol.io/specification/2026-07-28/changelog): no
269
+ `initialize` handshake, per-request `_meta`, required `server/discover`, `resultType`, `ttlMs`
270
+ and `cacheScope`, the `-32020` to `-32099` allocation.
271
+ - [Base Protocol](https://modelcontextprotocol.io/specification/2026-07-28/basic/index): Messages
272
+ (ids, Result Responses, ResultType, Error Responses, Error Codes), Statelessness, and `_meta`
273
+ ("Per-request protocol fields", "Per-response protocol fields").
274
+ - [Versioning and Compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning):
275
+ Protocol Version Negotiation, and "Backward Compatibility with Initialization-Based Versions"
276
+ for the answer to `initialize`. The revision has no lifecycle page:
277
+ `/specification/2026-07-28/basic/lifecycle` redirects here.
278
+ - [stdio](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio): the
279
+ framing, stdout and stderr rules, Cancellation and Shutdown.
280
+ - [Cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation).
281
+ - [Discovery](https://modelcontextprotocol.io/specification/2026-07-28/server/discover):
282
+ `DiscoverResult`.
283
+ - [Tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools): Listing Tools,
284
+ Calling Tools, Tool, Tool Result, Structured Content, Output Schema, Error Handling.
285
+ - [Caching](https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching):
286
+ Cacheable Results, `ttlMs`, Cache Scope.
287
+ - [schema.ts](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts):
288
+ `RequestMetaObject`, `Result`, `DiscoverResult`, `CacheableResult`, `ListToolsResult`,
289
+ `CallToolResult`, `Tool`, `ToolAnnotations`, `UnsupportedProtocolVersionError`, and the error
290
+ code constants.
291
+
292
+ Where these pages go beyond the ticket, the server follows them: cache fields go on
293
+ `server/discover` and `tools/list` only (the Caching page lists no other operation this server
294
+ offers), `serverInfo` is in every result's `_meta` (a SHOULD), and `initialize` is answered with
295
+ `-32022` naming the supported version (a SHOULD for a server that speaks no legacy revision).
296
+
297
+ The 2026-07-28 revision has no per-call identifier a server could derive an op id from, and the
298
+ JSON-RPC id changes on every retry. So instead of the plan's `[host, conversation, task, call]`
299
+ tuple from a durable journal (IMPLEMENTATION-PLAN §4.3), a `note` takes an optional
300
+ `idempotencyKey` from the client and derives its op id with the same function, from
301
+ `["relembra-mcp", scope, "note", key]`. Without a key, a retry after a lost response writes twice.
302
+
303
+ ## Development
304
+
305
+ ```sh
306
+ bun install
307
+ bun run build:cli # build the CLI first: the CLI tests run the binary
308
+ scripts/with-postgres.sh bun run test:all # Bun suite, beside a throwaway Postgres 17 (needs Docker)
309
+ bun run test:node # the Node suite (SQLite adapter and library on Node)
310
+ bun run typecheck && bun run lint
311
+ bun run eval # the evaluation harness
312
+ scripts/ci.sh # all of the above, the merge gate, on the committed candidate
313
+ scripts/check-release.sh # the gate, then the strict latency benchmark on the same candidate
314
+ ```
315
+
316
+ The landing page is `site/`: static files, no build step besides `bun run build:site`, which bundles
317
+ the library into `site/assets/relembra.js` so the page's demos run the real code in the browser
318
+ (`test/site.test.ts` fails while that file is stale). Preview it with
319
+ `python3 -m http.server --directory site`, or open `site/index.html` directly.
320
+ The `pages` workflow (`gh workflow run pages.yml --ref main`) publishes it to GitHub Pages with
321
+ OIDC, no stored token: `scripts/site/assemble.sh <new directory>` builds what ships from the
322
+ commit's `site/` plus the explainer video, which is git-ignored on `main` and kept on the
323
+ `gh-pages` branch, checked against `scripts/site/video.sha256`.
324
+
325
+ Contributing: [CONTRIBUTING.md](CONTRIBUTING.md). Security reports: [SECURITY.md](SECURITY.md).
326
+
327
+ ## Licence
328
+
329
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,26 @@
1
+ import { type CommandSpec } from "./commands.js";
2
+ export interface Parsed {
3
+ readonly command: CommandSpec;
4
+ readonly positionals: readonly string[];
5
+ /** Each given flag's values; a boolean flag has one empty value. */
6
+ readonly flags: ReadonlyMap<string, readonly string[]>;
7
+ }
8
+ export type ParseResult = {
9
+ readonly kind: "help";
10
+ } | {
11
+ readonly kind: "command";
12
+ readonly parsed: Parsed;
13
+ };
14
+ /** Whether `--json` appears before any `--`, so even a usage error can answer in JSON. */
15
+ export declare function wantsJson(argv: readonly string[]): boolean;
16
+ /**
17
+ * Parses `argv` against the command table. Every token that starts with `-` before `--` must be
18
+ * a flag the command declares (or a global flag): an unknown flag is a usage error, never
19
+ * ignored, so no spelling of an undeclared option can change what a command does.
20
+ */
21
+ export declare function parseArgs(argv: readonly string[]): ParseResult;
22
+ /** The single value of a flag, or undefined when it was not given. */
23
+ export declare function flagValue(parsed: Parsed, name: string): string | undefined;
24
+ export declare function hasFlag(parsed: Parsed, name: string): boolean;
25
+ /** A positive integer flag value, at most `max`; undefined when the flag is absent. */
26
+ export declare function intFlag(parsed: Parsed, name: string, max: number): number | undefined;
@@ -0,0 +1,109 @@
1
+ import { COMMANDS } from "./commands.js";
2
+ import { usage } from "./errors.js";
3
+ /** Whether `--json` appears before any `--`, so even a usage error can answer in JSON. */
4
+ export function wantsJson(argv) {
5
+ for (const arg of argv) {
6
+ if (arg === "--")
7
+ return false;
8
+ if (arg === "--json")
9
+ return true;
10
+ }
11
+ return false;
12
+ }
13
+ /**
14
+ * Parses `argv` against the command table. Every token that starts with `-` before `--` must be
15
+ * a flag the command declares (or a global flag): an unknown flag is a usage error, never
16
+ * ignored, so no spelling of an undeclared option can change what a command does.
17
+ */
18
+ export function parseArgs(argv) {
19
+ if (argv.includes("--help") && !argv.slice(0, argv.indexOf("--help")).includes("--")) {
20
+ return { kind: "help" };
21
+ }
22
+ const [name, ...rest] = argv.filter((arg, index) => !(arg === "--json" && !argv.slice(0, index).includes("--")));
23
+ if (name === undefined)
24
+ throw usage("missing-command", "no command given; see relembra --help");
25
+ const command = COMMANDS.find((spec) => spec.name === name);
26
+ if (command === undefined)
27
+ throw usage("unknown-command", `unknown command: ${name.slice(0, 40)}`);
28
+ const positionals = [];
29
+ const flags = new Map();
30
+ for (let index = 0; index < rest.length; index++) {
31
+ const arg = rest[index];
32
+ if (arg === "--") {
33
+ positionals.push(...rest.slice(index + 1));
34
+ break;
35
+ }
36
+ if (!arg.startsWith("-") || arg === "-") {
37
+ positionals.push(arg);
38
+ continue;
39
+ }
40
+ const equals = arg.indexOf("=");
41
+ const flagName = equals < 0 ? arg : arg.slice(0, equals);
42
+ const spec = command.flags.find((flag) => flag.name === flagName);
43
+ if (spec === undefined) {
44
+ throw usage("unknown-flag", `${command.name} takes no flag ${flagName.slice(0, 40)}`);
45
+ }
46
+ let value = "";
47
+ if (spec.value) {
48
+ if (equals >= 0)
49
+ value = arg.slice(equals + 1);
50
+ else {
51
+ const next = rest[index + 1];
52
+ if (next === undefined)
53
+ throw usage("missing-value", `${flagName} needs a value`);
54
+ value = next;
55
+ index++;
56
+ }
57
+ }
58
+ else if (equals >= 0) {
59
+ throw usage("unexpected-value", `${flagName} takes no value`);
60
+ }
61
+ const values = flags.get(flagName) ?? [];
62
+ if (values.length > 0 && !spec.repeat) {
63
+ throw usage("repeated-flag", `${flagName} may be given once`);
64
+ }
65
+ flags.set(flagName, [...values, value]);
66
+ }
67
+ checkPositionals(command, positionals);
68
+ return { kind: "command", parsed: { command, positionals, flags } };
69
+ }
70
+ function checkPositionals(command, positionals) {
71
+ const count = positionals.length;
72
+ const max = {
73
+ none: 0,
74
+ id: 1,
75
+ text: 1,
76
+ "id-text": 2,
77
+ words: Number.POSITIVE_INFINITY,
78
+ path: 1,
79
+ }[command.positionals];
80
+ const min = { none: 0, id: 1, text: 0, "id-text": 1, words: 1, path: 0 }[command.positionals];
81
+ if (count > max) {
82
+ throw usage("extra-argument", command.positionals === "text" || command.positionals === "id-text"
83
+ ? `${command.name} takes the text as one argument; quote it`
84
+ : `${command.name} takes at most ${max} argument${max === 1 ? "" : "s"}`);
85
+ }
86
+ if (count < min)
87
+ throw usage("missing-argument", `usage: ${command.usage}`);
88
+ }
89
+ /** The single value of a flag, or undefined when it was not given. */
90
+ export function flagValue(parsed, name) {
91
+ return parsed.flags.get(name)?.[0];
92
+ }
93
+ export function hasFlag(parsed, name) {
94
+ return parsed.flags.has(name);
95
+ }
96
+ /** A positive integer flag value, at most `max`; undefined when the flag is absent. */
97
+ export function intFlag(parsed, name, max) {
98
+ const value = flagValue(parsed, name);
99
+ if (value === undefined)
100
+ return undefined;
101
+ if (!/^[0-9]{1,15}$/.test(value)) {
102
+ throw usage("bad-value", `${name} takes a whole number from 1 to ${max}`);
103
+ }
104
+ const number = Number(value);
105
+ if (number < 1 || number > max) {
106
+ throw usage("bad-value", `${name} takes a whole number from 1 to ${max}`);
107
+ }
108
+ return number;
109
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The CLI's command table: every command, its arguments and its flags. docs/CLI.md documents
3
+ * exactly this table, and test/cli/contract.test.ts checks the two against each other.
4
+ * No flag names a scope, a home or a user: those come from the environment only.
5
+ */
6
+ export interface FlagSpec {
7
+ /** `--name`. */
8
+ readonly name: string;
9
+ /** Whether the flag takes a value (`--limit 5`, `--limit=5`). */
10
+ readonly value: boolean;
11
+ /** Whether the flag may be given more than once. */
12
+ readonly repeat?: boolean;
13
+ }
14
+ /** What a command takes as positional arguments. */
15
+ export type Positionals =
16
+ /** None. */
17
+ "none"
18
+ /** Exactly one memory id. */
19
+ | "id"
20
+ /** At most one text argument; stdin when absent. */
21
+ | "text"
22
+ /** A memory id, then at most one text argument; stdin when absent. */
23
+ | "id-text"
24
+ /** One or more words, joined by single spaces. */
25
+ | "words"
26
+ /** At most one file path. */
27
+ | "path";
28
+ export interface CommandSpec {
29
+ readonly name: string;
30
+ readonly positionals: Positionals;
31
+ readonly flags: readonly FlagSpec[];
32
+ /** One line for `relembra --help`. */
33
+ readonly usage: string;
34
+ }
35
+ export declare const COMMANDS: readonly CommandSpec[];
36
+ /** Flags every command takes. */
37
+ export declare const GLOBAL_FLAGS: readonly ["--json", "--help"];
38
+ /** The environment the CLI reads, in the order docs/CLI.md lists it. */
39
+ export declare const ENV_VARS: readonly ["RELEMBRA_HOME", "RELEMBRA_SCOPE", "RELEMBRA_GLOBAL_SCOPE", "RELEMBRA_AGENT", "RELEMBRA_HARNESS", "RELEMBRA_MODEL", "BUN_OPTIONS", "BUN_BYTECODE_ORDER_OUT", "PATH", "HOME", "XDG_CONFIG_HOME", "SystemRoot"];
40
+ export declare function usageText(): string;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The CLI's command table: every command, its arguments and its flags. docs/CLI.md documents
3
+ * exactly this table, and test/cli/contract.test.ts checks the two against each other.
4
+ * No flag names a scope, a home or a user: those come from the environment only.
5
+ */
6
+ const PROVENANCE = [
7
+ { name: "--agent", value: true },
8
+ { name: "--harness", value: true },
9
+ { name: "--model", value: true },
10
+ { name: "--source", value: true },
11
+ { name: "--trust", value: true },
12
+ { name: "--op-id", value: true },
13
+ ];
14
+ export const COMMANDS = [
15
+ { name: "init", positionals: "none", flags: [], usage: "relembra init" },
16
+ {
17
+ name: "note",
18
+ positionals: "text",
19
+ flags: [...PROVENANCE, { name: "--pin", value: false }],
20
+ usage: "relembra note [text] [--pin] [--trust t] [--op-id id] [--agent a] [--harness h] [--model m] [--source s]",
21
+ },
22
+ {
23
+ name: "find",
24
+ positionals: "words",
25
+ flags: [{ name: "--limit", value: true }],
26
+ usage: "relembra find <query…> [--limit n]",
27
+ },
28
+ {
29
+ name: "recall",
30
+ positionals: "none",
31
+ flags: [
32
+ { name: "--limit", value: true },
33
+ { name: "--retracted", value: false },
34
+ ],
35
+ usage: "relembra recall [--limit n] [--retracted]",
36
+ },
37
+ {
38
+ name: "wake",
39
+ positionals: "none",
40
+ flags: [{ name: "--budget", value: true }],
41
+ usage: "relembra wake [--budget bytes]",
42
+ },
43
+ {
44
+ name: "brief",
45
+ positionals: "words",
46
+ flags: [{ name: "--budget", value: true }],
47
+ usage: "relembra brief <topic…> [--budget bytes]",
48
+ },
49
+ { name: "zoom", positionals: "id", flags: [], usage: "relembra zoom <id>" },
50
+ { name: "retract", positionals: "id", flags: [], usage: "relembra retract <id>" },
51
+ { name: "redact", positionals: "id", flags: [], usage: "relembra redact <id>" },
52
+ {
53
+ name: "supersede",
54
+ positionals: "id-text",
55
+ flags: [...PROVENANCE, { name: "--pin", value: false }],
56
+ usage: "relembra supersede <id> [text] [--pin] [provenance flags as for note]",
57
+ },
58
+ {
59
+ name: "pending",
60
+ positionals: "none",
61
+ flags: [{ name: "--limit", value: true }],
62
+ usage: "relembra pending [--limit n]",
63
+ },
64
+ {
65
+ name: "summarize",
66
+ positionals: "text",
67
+ flags: [...PROVENANCE, { name: "--of", value: true, repeat: true }],
68
+ usage: "relembra summarize [text] --of <id>@<revision>… [provenance flags as for note]",
69
+ },
70
+ {
71
+ name: "sweep",
72
+ positionals: "none",
73
+ flags: [{ name: "--max-age-days", value: true }],
74
+ usage: "relembra sweep --max-age-days n",
75
+ },
76
+ { name: "check", positionals: "none", flags: [], usage: "relembra check" },
77
+ { name: "hook", positionals: "none", flags: [], usage: "relembra hook" },
78
+ { name: "mcp", positionals: "none", flags: [], usage: "relembra mcp" },
79
+ { name: "export", positionals: "none", flags: [], usage: "relembra export" },
80
+ {
81
+ name: "import",
82
+ positionals: "path",
83
+ flags: [{ name: "--optmem", value: true }],
84
+ usage: "relembra import <file> | relembra import --optmem <dir>",
85
+ },
86
+ ];
87
+ /** Flags every command takes. */
88
+ export const GLOBAL_FLAGS = ["--json", "--help"];
89
+ /** The environment the CLI reads, in the order docs/CLI.md lists it. */
90
+ export const ENV_VARS = [
91
+ "RELEMBRA_HOME",
92
+ "RELEMBRA_SCOPE",
93
+ "RELEMBRA_GLOBAL_SCOPE",
94
+ "RELEMBRA_AGENT",
95
+ "RELEMBRA_HARNESS",
96
+ "RELEMBRA_MODEL",
97
+ "BUN_OPTIONS",
98
+ "BUN_BYTECODE_ORDER_OUT",
99
+ "PATH",
100
+ "HOME",
101
+ "XDG_CONFIG_HOME",
102
+ "SystemRoot",
103
+ ];
104
+ export function usageText() {
105
+ return [
106
+ "relembra: a curated, scoped memory for AI agents",
107
+ "",
108
+ ...COMMANDS.map((command) => ` ${command.usage}`),
109
+ "",
110
+ "Every command takes --json. Scope comes from the git project and the OS user, or",
111
+ "RELEMBRA_SCOPE; reads also cover RELEMBRA_GLOBAL_SCOPE when it is set. The store lives",
112
+ "in RELEMBRA_HOME (default ~/.relembra). See docs/CLI.md.",
113
+ ].join("\n");
114
+ }
@@ -0,0 +1,13 @@
1
+ /** How long the person at the terminal has to type the code. */
2
+ export declare const CONFIRM_TIMEOUT_MS = 60000;
3
+ /**
4
+ * Asks the person at the controlling terminal to confirm an owner-trust action: shows `lines`
5
+ * (already guard-checked, so they render plainly), prints a random code and requires it typed
6
+ * back within CONFIRM_TIMEOUT_MS. Reads and writes /dev/tty only, never stdin or stdout, so an
7
+ * agent whose tool call has no terminal cannot answer, and one sharing the user's terminal never
8
+ * sees the code. Throws owner-confirmation-required: `no-tty`, `mismatch` or `timeout`.
9
+ *
10
+ * Not a security boundary: a process running as the same OS user can open the store directly,
11
+ * and one that can drive a pseudo-terminal can read the code off it (docs/CLI.md, Trust).
12
+ */
13
+ export declare function confirmOwner(lines: readonly string[]): void;