@hilbras/remembra 3.6.0 → 3.8.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/CHANGELOG.md +121 -0
- package/README.md +9 -1
- package/dist/backend.d.ts +17 -0
- package/dist/crypto.d.ts +10 -0
- package/dist/crypto.js +77 -0
- package/dist/crypto.js.map +1 -0
- package/dist/diff.d.ts +15 -0
- package/dist/diff.js +135 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +3 -8
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -1
- package/dist/http.d.ts +5 -1
- package/dist/http.js +84 -4
- package/dist/http.js.map +1 -1
- package/dist/index.js +64 -4
- package/dist/index.js.map +1 -1
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +3 -0
- package/dist/llm.js.map +1 -1
- package/dist/log.d.ts +19 -0
- package/dist/log.js +28 -0
- package/dist/log.js.map +1 -0
- package/dist/metrics.d.ts +22 -0
- package/dist/metrics.js +121 -0
- package/dist/metrics.js.map +1 -0
- package/dist/redact.d.ts +33 -0
- package/dist/redact.js +104 -0
- package/dist/redact.js.map +1 -0
- package/dist/service.d.ts +75 -0
- package/dist/service.js +230 -10
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +17 -1
- package/dist/store.js +134 -6
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +74 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/docs/architecture.md +52 -3
- package/docs/chatgpt.md +25 -0
- package/docs/clients.md +4 -0
- package/docs/lifecycle.md +10 -3
- package/docs/memory-model.md +42 -3
- package/docs/observability.md +125 -0
- package/docs/security.md +63 -3
- package/docs/tools.md +48 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,127 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
|
7
7
|
> (3.0.0 = v3). Earlier releases used independent semver: 0.1.0 = v1,
|
|
8
8
|
> 0.2.0 = v1.5, 0.3.0 = v2, 0.4.0 = v3.
|
|
9
9
|
|
|
10
|
+
## [3.8.0] — 2026-09-23
|
|
11
|
+
|
|
12
|
+
**Phase 8 of the deep audit — Advanced Capabilities.** All five roadmap
|
|
13
|
+
items covered; both storage-altering features are **opt-in** (scope review:
|
|
14
|
+
plain markdown and byte-faithful storage remain the defaults).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Relationship graph** — `related: [ids]` frontmatter (directed, single
|
|
18
|
+
write; backlinks derived at read time), `memory_relate` MCP tool
|
|
19
|
+
(add/remove, targets validated, self-links rejected, idempotent), plus the
|
|
20
|
+
missing read surfaces: **`memory_get`** / `GET /memories/:id` return a
|
|
21
|
+
memory with resolved `related` + `backlinks`. Retrieval ranking untouched —
|
|
22
|
+
graph is structure for the consumer, not a score.
|
|
23
|
+
- **Confidence scores** — `confidence: 0–1` frontmatter: explicit stores
|
|
24
|
+
default `1.0`, digest extractions `0.7` (the extraction LLM may supply its
|
|
25
|
+
own via the extended prompt/schema). Surfaced everywhere and carried through
|
|
26
|
+
merge/export/import; **deliberately not ranked** — importance answers
|
|
27
|
+
"relevant?", confidence answers "true?" (Phase 4 weight decisions stay
|
|
28
|
+
closed).
|
|
29
|
+
- **PII redaction filter** (opt-in `REMEMBRA_REDACT=1`) — pattern filter at
|
|
30
|
+
the *ingest layer* (`memory_store`, digest items, merge output): emails,
|
|
31
|
+
Luhn-valid cards, SSNs, phone numbers, provider tokens / ≥40-char entropy
|
|
32
|
+
blobs → typed placeholders (`<EMAIL>` …). Cards must pass Luhn; phone
|
|
33
|
+
matching requires separators + 10–15 digits, so dates/versions never match.
|
|
34
|
+
Irreversible by design; `remembra_redactions_total{kind}` + `redacted`
|
|
35
|
+
log event (counts only, never matched text). Reverses the former "no PII
|
|
36
|
+
filter" non-goal — documented with its limits in `docs/security.md`.
|
|
37
|
+
- **Encrypted storage mode** (opt-in `REMEMBRA_ENCRYPT_KEY`) — AES-256-GCM
|
|
38
|
+
per file via `node:crypto` (zero deps): magic-header detection, transparent
|
|
39
|
+
decrypt-on-read, encrypt-on-write, mixed plain/cipher trees supported;
|
|
40
|
+
`remembra encrypt` / `remembra decrypt` migrate the whole tree (incl.
|
|
41
|
+
history) idempotently under the advisory lock. Missing/wrong key fails
|
|
42
|
+
**loudly**: `ENCRYPTED_NO_KEY` (HTTP 503, `/health` `storage` field) —
|
|
43
|
+
never warn-skipped into silent partial results. GCM makes wrong-key ≡
|
|
44
|
+
tampered. Reverses the former "no encryption at rest" non-goal with an
|
|
45
|
+
explicit threat-model section (protects stolen backups / copied dirs, not
|
|
46
|
+
a runtime attacker with your env).
|
|
47
|
+
- **Diff/history view** — content-changing updates snapshot the raw
|
|
48
|
+
on-disk pre-image into `.history/<id>/<epochMs>-<seq>.md` first (content-
|
|
49
|
+
equality gate: embedding backfills and linking never snapshot);
|
|
50
|
+
`memory_history` MCP tool + `GET /memories/:id/history?limit=` return
|
|
51
|
+
versions newest-first, each with a **unified line diff** against its
|
|
52
|
+
predecessor (own ~60-line LCS, zero deps, cell-budget fallback for huge
|
|
53
|
+
contents). Pruned to `REMEMBRA_HISTORY_LIMIT` (default 20, `0` disables);
|
|
54
|
+
`.history` is never walked by `all()`/search.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
- Error classification gains `ENCRYPTED_NO_KEY` (503) and documents why it
|
|
58
|
+
breaks the skip-malformed-file rule; `metrics` route enum gains
|
|
59
|
+
`memory_sub` (the relate/history sub-routes).
|
|
60
|
+
- Extraction system prompt optionally returns `confidence` (0–1).
|
|
61
|
+
|
|
62
|
+
### Docs
|
|
63
|
+
`tools` (+3 tools, 9 total, encrypt/decrypt CLI), `memory-model`
|
|
64
|
+
(relationships, version history, metadata rows, layout incl. `.history`),
|
|
65
|
+
`security` (encryption + redaction sections replace the two reversed
|
|
66
|
+
non-goals, checklist items), `architecture` (encryption format, history
|
|
67
|
+
snapshot design, error table), `clients` (+3 env vars), `observability`
|
|
68
|
+
(+4 counters, `memory_sub`, `redacted` event), `chatgpt` (+3 routes),
|
|
69
|
+
`lifecycle` (merge → history cross-ref), README (9 tools, 11 routes).
|
|
70
|
+
|
|
71
|
+
### Tests
|
|
72
|
+
- **148 tests** (+16: redaction patterns + false-positive guards + service
|
|
73
|
+
on/off, digest redaction & confidence pass-through, confidence round-trip,
|
|
74
|
+
relations incl. backlinks/validation/idempotence/persistence, diff unit,
|
|
75
|
+
merge → snapshot → diff view, embedding-gate + limit pruning, encryption
|
|
76
|
+
round-trip / missing-key loud failure / wrong-key / mixed trees /
|
|
77
|
+
idempotent both-way migration incl. history, HTTP GET/relate/history
|
|
78
|
+
routes + `memory_sub` metric label). Suite verified stable across 3
|
|
79
|
+
consecutive runs.
|
|
80
|
+
|
|
81
|
+
## [3.7.0] — 2026-09-23
|
|
82
|
+
|
|
83
|
+
**Phase 7 of the deep audit** — observability. All five roadmap items
|
|
84
|
+
covered (error-rate alerting taken as the *pragmatic substrate*: counters +
|
|
85
|
+
documented rules, no built-in notifier — decided in scope review).
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- **Structured JSON logging** (`src/log.ts`) — every server-side event goes
|
|
89
|
+
through one logger on **stderr** (stdout stays reserved for MCP stdio/CLI).
|
|
90
|
+
Format: `REMEMBRA_LOG=json|text` forces it; unset → auto — JSON when stderr
|
|
91
|
+
is piped (containers, CI, shippers), text on a TTY. Text mode prints the
|
|
92
|
+
exact legacy strings, so existing greps and the Phase 2/3 tests still hold.
|
|
93
|
+
JSON lines carry `{"ts","level","event","msg",...fields}`.
|
|
94
|
+
- **`GET /metrics`** (`src/metrics.ts`) — zero-dependency Prometheus text
|
|
95
|
+
endpoint: request/digest/search/cache counters, latency histograms,
|
|
96
|
+
`errors_total{code,transport}`, cache-entry gauge, `remembra_info`. Route
|
|
97
|
+
labels are a fixed low-cardinality enum (never raw paths). Sits **after**
|
|
98
|
+
the API-key check — keyed deployments must not leak counters; `/health`
|
|
99
|
+
stays exempt.
|
|
100
|
+
- **Search query logging** — one structured `search` event per call (scope,
|
|
101
|
+
term count, results, limit, `duration_ms`). Raw query text only under
|
|
102
|
+
`REMEMBRA_DEBUG` (Phase 2 log-hygiene rule unchanged).
|
|
103
|
+
- **`/health` readiness probe** — now runs a real storage read: `200 ok`
|
|
104
|
+
with `version`/`uptime_s`/`storage`/`cache` fields, **`503 unready`** with
|
|
105
|
+
the failing error code when storage cannot be read — the probe fails
|
|
106
|
+
instead of lying. Field `status:"ok"` kept for existing consumers.
|
|
107
|
+
- **Alerting infrastructure** — `docs/observability.md`: metric reference,
|
|
108
|
+
scrape config (incl. `x-api-key`), and ready-to-paste Prometheus rules
|
|
109
|
+
(down, internal error rate with 4xx excluded, readiness, lock contention,
|
|
110
|
+
search p95, cache thrash). No built-in notifier by design: a local tool
|
|
111
|
+
alerts through the operator's existing stack.
|
|
112
|
+
- `src/version.ts` single-sources `VERSION` (MCP server id, `/health`,
|
|
113
|
+
`remembra_info`); a test pins it to `package.json`.
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
- **Prometheus bucket labels** — histogram series emitted `{le="…"route="…"}`
|
|
117
|
+
(missing comma), caught by the exposition-format test.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
- Conversion to `logEvent` at 8 sites (listening banners, shutdown, parse
|
|
121
|
+
skip, crash recovery, embedding/touch/merge/decay failures) — messages
|
|
122
|
+
unchanged in text mode.
|
|
123
|
+
|
|
124
|
+
### Tests
|
|
125
|
+
- **132 tests** (+13: log formats + auto-detection, `/metrics` content,
|
|
126
|
+
label enum + exposition grammar, auth on `/metrics`, counter movement for
|
|
127
|
+
store/search/cache, MCP + HTTP error counters, hygiene-first query logging
|
|
128
|
+
(and `REMEMBRA_DEBUG` opt-in), healthy + broken-storage `/health`,
|
|
129
|
+
version pin). Suite verified stable across 3 consecutive runs.
|
|
130
|
+
|
|
10
131
|
## [3.6.0] — 2026-09-23
|
|
11
132
|
|
|
12
133
|
**Phase 6 of the deep audit** — testing depth. All six roadmap items now
|
package/README.md
CHANGED
|
@@ -85,10 +85,14 @@ Then wire a Custom GPT to the API — full walkthrough in **[docs/chatgpt.md](do
|
|
|
85
85
|
|
|
86
86
|
| Method | Route | Purpose |
|
|
87
87
|
|--------|-------|---------|
|
|
88
|
-
| GET | `/health` | Liveness (no auth) |
|
|
88
|
+
| GET | `/health` | Liveness + readiness (no auth) |
|
|
89
|
+
| GET | `/metrics` | Prometheus metrics (auth when keyed) |
|
|
89
90
|
| POST | `/memories` | Store a memory |
|
|
90
91
|
| GET | `/memories/search?query=&scope=` | Search |
|
|
91
92
|
| GET | `/memories?scope=&type=` | List |
|
|
93
|
+
| GET | `/memories/:id` | One memory + related links + backlinks |
|
|
94
|
+
| POST | `/memories/:id/relate` | Link / unlink memories |
|
|
95
|
+
| GET | `/memories/:id/history` | Version history with line diffs |
|
|
92
96
|
| POST | `/memories/digest` | LLM extract + store from a transcript |
|
|
93
97
|
| POST | `/maintain` | Decay sweep + vector backfill |
|
|
94
98
|
| DELETE | `/memories/:id` | Forget |
|
|
@@ -123,6 +127,9 @@ tampered snapshot is rejected atomically, never half-imported. Of course,
|
|
|
123
127
|
| `memory_digest` | Extract + store memories from a transcript (LLM) |
|
|
124
128
|
| `memory_search` | Retrieve relevant memories (pass `scope` = current project) |
|
|
125
129
|
| `memory_list` | Browse stored memories |
|
|
130
|
+
| `memory_get` | Fetch one memory with its links and backlinks |
|
|
131
|
+
| `memory_relate` | Link / unlink memories (relationship graph) |
|
|
132
|
+
| `memory_history` | Version history of a memory with line diffs |
|
|
126
133
|
| `memory_maintain` | Archive decayed / delete expired / backfill vectors |
|
|
127
134
|
| `memory_forget` | Delete by id |
|
|
128
135
|
|
|
@@ -139,6 +146,7 @@ Full reference: **[docs/tools.md](docs/tools.md)**
|
|
|
139
146
|
| [AI providers](docs/providers.md) | Digest LLM + embeddings configuration |
|
|
140
147
|
| [Lifecycle](docs/lifecycle.md) | Decay, archiving, auto-delete, contradiction merging |
|
|
141
148
|
| [Security](docs/security.md) | Trust model, enforced protections, deployment checklist |
|
|
149
|
+
| [Observability](docs/observability.md) | Structured logs, Prometheus metrics, health/readiness, alert rules |
|
|
142
150
|
| [Architecture](docs/architecture.md) | Backend interface, locking, crash recovery, error codes |
|
|
143
151
|
| [Contributing](CONTRIBUTING.md) | Dev workflow and guidelines |
|
|
144
152
|
| [Changelog](CHANGELOG.md) | Release history |
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { Memory, StoreInput } from "./types.js";
|
|
2
|
+
/** One superseded pre-image from `.history/<id>/` (audit Phase 8). */
|
|
3
|
+
export interface HistoryEntry {
|
|
4
|
+
/** Snapshot file name (`${epochMs}-${seq}.md`). */
|
|
5
|
+
file: string;
|
|
6
|
+
/** The pre-image's own `updatedAt` — when this version was current. */
|
|
7
|
+
at?: string;
|
|
8
|
+
/** When the snapshot was taken (from the file name's epoch prefix). */
|
|
9
|
+
snapshotAt?: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
2
12
|
/**
|
|
3
13
|
* Storage abstraction (audit: Phase 3 — separate MemoryStore abstraction to
|
|
4
14
|
* enable a future DB swap).
|
|
@@ -18,6 +28,11 @@ export interface MemoryBackend {
|
|
|
18
28
|
store(input: StoreInput, embedding?: number[], opts?: {
|
|
19
29
|
provenance?: Memory["provenance"];
|
|
20
30
|
}): Promise<Memory>;
|
|
31
|
+
/** Optional observability hook — the file backend exposes parse-cache stats. */
|
|
32
|
+
cacheStats?(): {
|
|
33
|
+
size: number;
|
|
34
|
+
capacity: number;
|
|
35
|
+
};
|
|
21
36
|
get(id: string): Promise<Memory | null>;
|
|
22
37
|
all(includeArchived?: boolean): Promise<Memory[]>;
|
|
23
38
|
update(memory: Memory): Promise<Memory>;
|
|
@@ -26,4 +41,6 @@ export interface MemoryBackend {
|
|
|
26
41
|
touch(id: string): Promise<void>;
|
|
27
42
|
forget(id: string): Promise<boolean>;
|
|
28
43
|
importMemory(m: Memory): Promise<boolean>;
|
|
44
|
+
/** Optional (audit Phase 8): superseded pre-images, newest first. */
|
|
45
|
+
history?(id: string): Promise<HistoryEntry[]>;
|
|
29
46
|
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function encryptionEnabled(): boolean;
|
|
2
|
+
/** Cheap detection: does this file already hold ciphertext? */
|
|
3
|
+
export declare function isEncrypted(buf: Buffer): boolean;
|
|
4
|
+
export declare function encryptBuffer(plain: Buffer): Buffer;
|
|
5
|
+
/**
|
|
6
|
+
* Decrypt a file buffer. Throws `ENCRYPTED_NO_KEY` when the key is missing
|
|
7
|
+
* or wrong (GCM authentication failure covers both — a tampered file must
|
|
8
|
+
* fail identically).
|
|
9
|
+
*/
|
|
10
|
+
export declare function decryptBuffer(buf: Buffer): Buffer;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted storage mode (audit Phase 8).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in: `REMEMBRA_ENCRYPT_KEY` set → memory files are written as
|
|
5
|
+
* AES-256-GCM ciphertext (node:crypto — zero dependencies) and decrypted
|
|
6
|
+
* transparently on read. Unset → plain markdown, exactly as before.
|
|
7
|
+
*
|
|
8
|
+
* Key material: a 64-hex-char (32-byte) key, used directly — no KDF, no
|
|
9
|
+
* salt (a high-entropy symmetric key needs neither; per-file random nonces
|
|
10
|
+
* provide uniqueness). Generate one with:
|
|
11
|
+
* node -p 'require("node:crypto").randomBytes(32).toString("hex")'
|
|
12
|
+
*
|
|
13
|
+
* On-disk format (files keep their .md names; detection is by magic bytes):
|
|
14
|
+
* MAGIC(7) "RMBENC1" | nonce(12) | tag(16) | ciphertext
|
|
15
|
+
* The AAD is empty — files move between trees (archive/revive) freely.
|
|
16
|
+
*
|
|
17
|
+
* Failure mode without the key: reading an encrypted file throws
|
|
18
|
+
* `ENCRYPTED_NO_KEY` (HTTP 503, health `storage: ENCRYPTED_NO_KEY`) instead
|
|
19
|
+
* of warn-skipping — a store you cannot read must fail loudly, never
|
|
20
|
+
* silently serve partial data.
|
|
21
|
+
*
|
|
22
|
+
* Migration: `remembra encrypt` / `remembra decrypt` rewrite the tree under
|
|
23
|
+
* the storage lock (idempotent; already-converted files are skipped).
|
|
24
|
+
*/
|
|
25
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
|
|
26
|
+
import { RemembraError } from "./errors.js";
|
|
27
|
+
const MAGIC = Buffer.from("RMBENC1", "ascii");
|
|
28
|
+
const NONCE_LEN = 12;
|
|
29
|
+
const TAG_LEN = 16;
|
|
30
|
+
export function encryptionEnabled() {
|
|
31
|
+
return (process.env.REMEMBRA_ENCRYPT_KEY ?? "").length > 0;
|
|
32
|
+
}
|
|
33
|
+
function rawKey() {
|
|
34
|
+
const hex = process.env.REMEMBRA_ENCRYPT_KEY ?? "";
|
|
35
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
36
|
+
throw new RemembraError("INVALID_INPUT", "REMEMBRA_ENCRYPT_KEY must be 64 hex chars (32 bytes). Generate one: " +
|
|
37
|
+
`node -p 'require("node:crypto").randomBytes(32).toString("hex")'`);
|
|
38
|
+
}
|
|
39
|
+
return Buffer.from(hex, "hex");
|
|
40
|
+
}
|
|
41
|
+
/** Cheap detection: does this file already hold ciphertext? */
|
|
42
|
+
export function isEncrypted(buf) {
|
|
43
|
+
return buf.length > MAGIC.length && buf.subarray(0, MAGIC.length).equals(MAGIC);
|
|
44
|
+
}
|
|
45
|
+
export function encryptBuffer(plain) {
|
|
46
|
+
const nonce = randomBytes(NONCE_LEN);
|
|
47
|
+
const cipher = createCipheriv("aes-256-gcm", rawKey(), nonce);
|
|
48
|
+
const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
49
|
+
return Buffer.concat([MAGIC, nonce, cipher.getAuthTag(), ct]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decrypt a file buffer. Throws `ENCRYPTED_NO_KEY` when the key is missing
|
|
53
|
+
* or wrong (GCM authentication failure covers both — a tampered file must
|
|
54
|
+
* fail identically).
|
|
55
|
+
*/
|
|
56
|
+
export function decryptBuffer(buf) {
|
|
57
|
+
if (!isEncrypted(buf))
|
|
58
|
+
return buf; // plain file first: plain mode must never throw
|
|
59
|
+
if (!encryptionEnabled()) {
|
|
60
|
+
throw new RemembraError("ENCRYPTED_NO_KEY", "storage contains encrypted memories but REMEMBRA_ENCRYPT_KEY is not set");
|
|
61
|
+
}
|
|
62
|
+
if (buf.length < MAGIC.length + NONCE_LEN + TAG_LEN + 1) {
|
|
63
|
+
throw new RemembraError("ENCRYPTED_NO_KEY", "encrypted file is truncated");
|
|
64
|
+
}
|
|
65
|
+
const nonce = buf.subarray(MAGIC.length, MAGIC.length + NONCE_LEN);
|
|
66
|
+
const tag = buf.subarray(MAGIC.length + NONCE_LEN, MAGIC.length + NONCE_LEN + TAG_LEN);
|
|
67
|
+
const ct = buf.subarray(MAGIC.length + NONCE_LEN + TAG_LEN);
|
|
68
|
+
try {
|
|
69
|
+
const decipher = createDecipheriv("aes-256-gcm", rawKey(), nonce);
|
|
70
|
+
decipher.setAuthTag(tag);
|
|
71
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new RemembraError("ENCRYPTED_NO_KEY", "cannot decrypt: wrong REMEMBRA_ENCRYPT_KEY or corrupted file");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-based unified diff (audit Phase 8 — diff/history view).
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency LCS over lines producing standard unified format with 2
|
|
5
|
+
* lines of context, so any client that renders `diff -u` output renders our
|
|
6
|
+
* history view for free.
|
|
7
|
+
*
|
|
8
|
+
* Guard: the LCS table is O(n·m) — contents above the cell budget fall back
|
|
9
|
+
* to a coarse whole-block diff instead of allocating hundreds of megabytes.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Unified diff of `prev` → `next`. Empty string when identical.
|
|
13
|
+
* `fromLabel`/`toLabel` name the sides (defaults: previous/current).
|
|
14
|
+
*/
|
|
15
|
+
export declare function unifiedDiff(prev: string, next: string, fromLabel?: string, toLabel?: string): string;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-based unified diff (audit Phase 8 — diff/history view).
|
|
3
|
+
*
|
|
4
|
+
* Zero-dependency LCS over lines producing standard unified format with 2
|
|
5
|
+
* lines of context, so any client that renders `diff -u` output renders our
|
|
6
|
+
* history view for free.
|
|
7
|
+
*
|
|
8
|
+
* Guard: the LCS table is O(n·m) — contents above the cell budget fall back
|
|
9
|
+
* to a coarse whole-block diff instead of allocating hundreds of megabytes.
|
|
10
|
+
*/
|
|
11
|
+
const MAX_CELLS = 4_000_000;
|
|
12
|
+
const CONTEXT = 2;
|
|
13
|
+
function lcsOps(a, b) {
|
|
14
|
+
const n = a.length;
|
|
15
|
+
const m = b.length;
|
|
16
|
+
// dp[i][j] = LCS length of a[i:], b[j:]
|
|
17
|
+
const dp = new Uint32Array((n + 1) * (m + 1));
|
|
18
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
19
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
20
|
+
dp[i * (m + 1) + j] =
|
|
21
|
+
a[i] === b[j] ? dp[(i + 1) * (m + 1) + j + 1] + 1 : Math.max(dp[(i + 1) * (m + 1) + j], dp[i * (m + 1) + j + 1]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const ops = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
let j = 0;
|
|
27
|
+
while (i < n && j < m) {
|
|
28
|
+
if (a[i] === b[j]) {
|
|
29
|
+
ops.push({ kind: "keep", line: a[i] });
|
|
30
|
+
i++;
|
|
31
|
+
j++;
|
|
32
|
+
}
|
|
33
|
+
else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + j + 1]) {
|
|
34
|
+
ops.push({ kind: "remove", line: a[i] });
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
ops.push({ kind: "add", line: b[j] });
|
|
39
|
+
j++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
while (i < n)
|
|
43
|
+
ops.push({ kind: "remove", line: a[i++] });
|
|
44
|
+
while (j < m)
|
|
45
|
+
ops.push({ kind: "add", line: b[j++] });
|
|
46
|
+
return ops;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Unified diff of `prev` → `next`. Empty string when identical.
|
|
50
|
+
* `fromLabel`/`toLabel` name the sides (defaults: previous/current).
|
|
51
|
+
*/
|
|
52
|
+
export function unifiedDiff(prev, next, fromLabel = "previous", toLabel = "current") {
|
|
53
|
+
if (prev === next)
|
|
54
|
+
return "";
|
|
55
|
+
const a = prev.split("\n");
|
|
56
|
+
const b = next.split("\n");
|
|
57
|
+
if ((a.length + 1) * (b.length + 1) > MAX_CELLS) {
|
|
58
|
+
// Coarse fallback: claim a full rewrite rather than allocating the table.
|
|
59
|
+
return [
|
|
60
|
+
`--- ${fromLabel}`,
|
|
61
|
+
`+++ ${toLabel}`,
|
|
62
|
+
`@@ -1,${a.length} +1,${b.length} @@ (diff omitted: content too large for line diff)`,
|
|
63
|
+
...a.map((l) => `-${l}`),
|
|
64
|
+
...b.map((l) => `+${l}`),
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
const ops = lcsOps(a, b);
|
|
68
|
+
const header = [`--- ${fromLabel}`, `+++ ${toLabel}`];
|
|
69
|
+
let out = header.join("\n");
|
|
70
|
+
// Group changed regions (with context) into hunks.
|
|
71
|
+
let idx = 0;
|
|
72
|
+
while (idx < ops.length) {
|
|
73
|
+
if (ops[idx].kind === "keep") {
|
|
74
|
+
idx++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Expand over the changed run.
|
|
78
|
+
let end = idx;
|
|
79
|
+
while (end < ops.length) {
|
|
80
|
+
if (ops[end].kind !== "keep") {
|
|
81
|
+
end++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Peek: a keep run shorter than 2·context followed by more changes
|
|
85
|
+
// belongs to the same hunk.
|
|
86
|
+
let run = 0;
|
|
87
|
+
let probe = end;
|
|
88
|
+
while (probe < ops.length && ops[probe].kind === "keep") {
|
|
89
|
+
run++;
|
|
90
|
+
probe++;
|
|
91
|
+
}
|
|
92
|
+
if (probe < ops.length && run <= CONTEXT * 2) {
|
|
93
|
+
end = probe; // small keep-gap: bridge it and keep consuming changes
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const start = Math.max(0, idx - CONTEXT);
|
|
99
|
+
const stop = Math.min(ops.length, end + CONTEXT);
|
|
100
|
+
// Line numbers (1-based) for the hunk header.
|
|
101
|
+
let aLine = 0;
|
|
102
|
+
let bLine = 0;
|
|
103
|
+
for (let k = 0; k < start; k++) {
|
|
104
|
+
if (ops[k].kind !== "add")
|
|
105
|
+
aLine++;
|
|
106
|
+
if (ops[k].kind !== "remove")
|
|
107
|
+
bLine++;
|
|
108
|
+
}
|
|
109
|
+
const aStart = aLine + 1;
|
|
110
|
+
const bStart = bLine + 1;
|
|
111
|
+
let aCount = 0;
|
|
112
|
+
let bCount = 0;
|
|
113
|
+
const hunk = [];
|
|
114
|
+
for (let k = start; k < stop; k++) {
|
|
115
|
+
const op = ops[k];
|
|
116
|
+
if (op.kind === "keep") {
|
|
117
|
+
hunk.push(` ${op.line}`);
|
|
118
|
+
aCount++;
|
|
119
|
+
bCount++;
|
|
120
|
+
}
|
|
121
|
+
else if (op.kind === "remove") {
|
|
122
|
+
hunk.push(`-${op.line}`);
|
|
123
|
+
aCount++;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
hunk.push(`+${op.line}`);
|
|
127
|
+
bCount++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
out += `\n@@ -${aStart},${aCount} +${bStart},${bCount} @@\n${hunk.join("\n")}`;
|
|
131
|
+
idx = stop;
|
|
132
|
+
}
|
|
133
|
+
return out + "\n";
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=diff.js.map
|
package/dist/diff.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Structured error classification (audit: Phase 2 — structured error
|
|
3
|
-
* classification for store operations).
|
|
4
|
-
*
|
|
5
|
-
* Every failure that callers should react to carries a stable `code`.
|
|
6
|
-
* HTTP maps codes to statuses; MCP tools surface them as `[CODE] message`.
|
|
7
|
-
*/
|
|
8
|
-
export type ErrorCode = "INVALID_INPUT" | "SNAPSHOT_INVALID" | "SCOPE_ESCAPES_ROOT" | "NOT_FOUND" | "CONFLICT" | "LOCK_TIMEOUT" | "IO_ERROR" | "LLM_ERROR";
|
|
1
|
+
export type ErrorCode = "INVALID_INPUT" | "SNAPSHOT_INVALID" | "SCOPE_ESCAPES_ROOT" | "NOT_FOUND" | "CONFLICT" | "LOCK_TIMEOUT" | "IO_ERROR" | "LLM_ERROR" | "ENCRYPTED_NO_KEY";
|
|
9
2
|
export declare class RemembraError extends Error {
|
|
10
3
|
readonly code: ErrorCode;
|
|
11
4
|
constructor(code: ErrorCode, message: string, options?: {
|
|
@@ -18,6 +11,8 @@ export declare function statusFor(err: RemembraError): number;
|
|
|
18
11
|
export declare function inputError(err: unknown, code: "INVALID_INPUT" | "SNAPSHOT_INVALID"): unknown;
|
|
19
12
|
/** Uniform single-line rendering for MCP tool failures. */
|
|
20
13
|
export declare function formatToolError(err: unknown): string;
|
|
14
|
+
/** Stable low-cardinality label for metrics (`remembra_errors_total{code}`). */
|
|
15
|
+
export declare function errorLabel(err: unknown): ErrorCode | "INVALID_INPUT" | "INTERNAL";
|
|
21
16
|
/** MCP tool error result shape (structural — no SDK import needed). */
|
|
22
17
|
export declare function toolFail(err: unknown): {
|
|
23
18
|
content: {
|
package/dist/errors.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Every failure that callers should react to carries a stable `code`.
|
|
6
6
|
* HTTP maps codes to statuses; MCP tools surface them as `[CODE] message`.
|
|
7
7
|
*/
|
|
8
|
+
import { metrics } from "./metrics.js";
|
|
8
9
|
export class RemembraError extends Error {
|
|
9
10
|
code;
|
|
10
11
|
constructor(code, message, options) {
|
|
@@ -25,6 +26,7 @@ const HTTP_STATUS = {
|
|
|
25
26
|
LOCK_TIMEOUT: 423, // Locked
|
|
26
27
|
IO_ERROR: 500,
|
|
27
28
|
LLM_ERROR: 502,
|
|
29
|
+
ENCRYPTED_NO_KEY: 503, // Service Unavailable — storage unreadable without the key
|
|
28
30
|
};
|
|
29
31
|
export function statusFor(err) {
|
|
30
32
|
return HTTP_STATUS[err.code] ?? 500;
|
|
@@ -53,8 +55,17 @@ export function formatToolError(err) {
|
|
|
53
55
|
return `[INVALID_INPUT] ${zodSummary(err)}`;
|
|
54
56
|
return `[INTERNAL] ${err instanceof Error ? err.message : String(err)}`;
|
|
55
57
|
}
|
|
58
|
+
/** Stable low-cardinality label for metrics (`remembra_errors_total{code}`). */
|
|
59
|
+
export function errorLabel(err) {
|
|
60
|
+
if (isRemembraError(err))
|
|
61
|
+
return err.code;
|
|
62
|
+
if (isZodLike(err))
|
|
63
|
+
return "INVALID_INPUT";
|
|
64
|
+
return "INTERNAL";
|
|
65
|
+
}
|
|
56
66
|
/** MCP tool error result shape (structural — no SDK import needed). */
|
|
57
67
|
export function toolFail(err) {
|
|
68
|
+
metrics.inc("remembra_errors_total", { code: errorLabel(err), transport: "mcp" });
|
|
58
69
|
return { content: [{ type: "text", text: formatToolError(err) }], isError: true };
|
|
59
70
|
}
|
|
60
71
|
//# sourceMappingURL=errors.js.map
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/http.d.ts
CHANGED
|
@@ -23,12 +23,16 @@ export declare function resolveListen(host: string | undefined, hasKey: boolean)
|
|
|
23
23
|
* Minimal HTTP API over the same handlers the MCP tools use.
|
|
24
24
|
*
|
|
25
25
|
* Routes:
|
|
26
|
-
* GET /health → liveness (no auth)
|
|
26
|
+
* GET /health → liveness + readiness (no auth): 200 ok / 503 unready
|
|
27
|
+
* GET /metrics → Prometheus text format (auth when keyed)
|
|
27
28
|
* POST /memories → store a memory
|
|
28
29
|
* GET /memories/search → ?query=&scope=&type=&limit=
|
|
29
30
|
* GET /memories → ?scope=&type=&includeArchived=&offset=&limit=
|
|
30
31
|
* POST /memories/digest → LLM extraction
|
|
31
32
|
* POST /maintain → decay sweep + vector backfill
|
|
33
|
+
* GET /memories/:id → one memory + related + backlinks (Phase 8)
|
|
34
|
+
* POST /memories/:id/relate → link/unlink memories (Phase 8)
|
|
35
|
+
* GET /memories/:id/history→ version history + line diffs (Phase 8)
|
|
32
36
|
* DELETE /memories/:id → forget
|
|
33
37
|
*/
|
|
34
38
|
export declare function createHttpServer(service: MemoryService, opts?: HttpOptions): http.Server;
|