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.
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/dist/cli/args.d.ts +26 -0
- package/dist/cli/args.js +109 -0
- package/dist/cli/commands.d.ts +40 -0
- package/dist/cli/commands.js +114 -0
- package/dist/cli/confirm.d.ts +13 -0
- package/dist/cli/confirm.js +80 -0
- package/dist/cli/errors.d.ts +44 -0
- package/dist/cli/errors.js +109 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.js +768 -0
- package/dist/cli/mcp-lines.d.ts +40 -0
- package/dist/cli/mcp-lines.js +87 -0
- package/dist/cli/mcp.d.ts +42 -0
- package/dist/cli/mcp.js +570 -0
- package/dist/cli/scope.d.ts +70 -0
- package/dist/cli/scope.js +325 -0
- package/dist/cli/stdin.d.ts +13 -0
- package/dist/cli/stdin.js +68 -0
- package/dist/cli/wake.d.ts +25 -0
- package/dist/cli/wake.js +46 -0
- package/dist/errors.d.ts +206 -0
- package/dist/errors.js +228 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +218 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +10 -0
- package/dist/model.d.ts +280 -0
- package/dist/model.js +32 -0
- package/dist/ops.d.ts +93 -0
- package/dist/ops.js +129 -0
- package/dist/optmem.d.ts +50 -0
- package/dist/optmem.js +261 -0
- package/dist/pi-durable/index.d.ts +57 -0
- package/dist/pi-durable/index.js +225 -0
- package/dist/ports.d.ts +289 -0
- package/dist/ports.js +5 -0
- package/dist/postgres/client.d.ts +24 -0
- package/dist/postgres/client.js +14 -0
- package/dist/postgres/index.d.ts +69 -0
- package/dist/postgres/index.js +492 -0
- package/dist/postgres/migrations.d.ts +29 -0
- package/dist/postgres/migrations.js +188 -0
- package/dist/postgres/queries.d.ts +6 -0
- package/dist/postgres/queries.js +12 -0
- package/dist/postgres/term-index.d.ts +14 -0
- package/dist/postgres/term-index.js +107 -0
- package/dist/ranking.d.ts +57 -0
- package/dist/ranking.js +202 -0
- package/dist/relembra.d.ts +402 -0
- package/dist/relembra.js +1187 -0
- package/dist/render.d.ts +111 -0
- package/dist/render.js +265 -0
- package/dist/sqlite/database.d.ts +83 -0
- package/dist/sqlite/database.js +487 -0
- package/dist/sqlite/index.d.ts +32 -0
- package/dist/sqlite/index.js +484 -0
- package/dist/sqlite/migrations.d.ts +18 -0
- package/dist/sqlite/migrations.js +190 -0
- package/dist/sqlite/term-index.d.ts +33 -0
- package/dist/sqlite/term-index.js +139 -0
- package/dist/sqlite/wal-image.d.ts +9 -0
- package/dist/sqlite/wal-image.js +68 -0
- package/dist/testing/conformance.d.ts +46 -0
- package/dist/testing/conformance.js +2266 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/memory-store.d.ts +3 -0
- package/dist/testing/memory-store.js +260 -0
- package/dist/transfer.d.ts +93 -0
- package/dist/transfer.js +769 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/docs/FORMAT.md +610 -0
- 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
|
-
#
|
|
1
|
+
# relembra
|
|
2
2
|
|
|
3
|
-
|
|
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;
|
package/dist/cli/args.js
ADDED
|
@@ -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;
|