@hilbras/remembra 0.1.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 ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to Remembra will be documented in this file.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
+
6
+ ## [0.1.0] — 2026-09-23
7
+
8
+ ### Added
9
+ - Initial release: MCP memory server for AI assistants.
10
+ - Four memory types: `fact`, `decision`, `role`, `history`.
11
+ - Hybrid scopes: `global` + per-project isolation (no cross-project leaks).
12
+ - File-based storage (`~/.remembra`, markdown + frontmatter), override via `REMEMBRA_HOME`.
13
+ - Layered retrieval: roles always surface → scope → importance → recency → keywords.
14
+ - MCP tools: `memory_store`, `memory_search`, `memory_list`, `memory_forget`.
15
+ - Client setup docs for OpenCode, Claude Code, Cline, and Kimi Code.
16
+ - Test suite (store round-trip, scope isolation, role priority, ranking, delete).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hilbras
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 ADDED
@@ -0,0 +1,129 @@
1
+ # Remembra 🧠
2
+
3
+ **External memory for AI assistants.** Remembra stores facts, decisions, roles, and history
4
+ outside the context window, and hands back only what's relevant — so your AI stops forgetting
5
+ when the conversation gets long.
6
+
7
+ [![npm](https://img.shields.io/npm/v/%40hilbras/remembra.svg)](https://www.npmjs.com/package/@hilbras/remembra)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
+ [![MCP](https://img.shields.io/badge/MCP-compatible-brightgreen.svg)](https://modelcontextprotocol.io)
10
+
11
+ One memory server, many clients:
12
+
13
+ | Client | Connection |
14
+ |--------|-----------|
15
+ | OpenCode | MCP (stdio) |
16
+ | Claude Code | MCP (stdio) |
17
+ | Cline | MCP (stdio) |
18
+ | Kimi Code | MCP (stdio) |
19
+ | ChatGPT | HTTP API + Custom GPT action *(v1.5)* |
20
+
21
+ ## The problem
22
+
23
+ Every AI assistant has a context window. Fill it, and the model starts forgetting earlier
24
+ decisions, repeating questions, and losing track of roles and preferences. Remembra moves
25
+ long-term memory **out of the window and into storage**, then injects only the relevant
26
+ subset each session.
27
+
28
+ ## How it works
29
+
30
+ ```
31
+ Conversation ──► memory_store ──► ~/.remembra/ (markdown files)
32
+
33
+ New session ──► memory_search ◄─────────┘ ──► relevant subset injected
34
+ ```
35
+
36
+ Every memory has a **type**, a **scope**, tags, and an importance score:
37
+
38
+ - **fact** — stable knowledge ("Project uses PostgreSQL 16")
39
+ - **decision** — choices already made ("Chose JWT over sessions")
40
+ - **role** — standing instructions ("Answer concisely"; *always* injected)
41
+ - **history** — condensed chronology of past work
42
+
43
+ **Scopes:**
44
+ - `global` — relevant everywhere (preferences, roles)
45
+ - any project path/id — only loaded when working in that scope, never leaks into other projects
46
+
47
+ **Retrieval ranking:** roles always surface → scope match → importance → recency → keyword overlap.
48
+
49
+ ## Quick start
50
+
51
+ ```bash
52
+ npm install -g @hilbras/remembra
53
+ ```
54
+
55
+ **Claude Code:**
56
+
57
+ ```bash
58
+ claude mcp add remembra -- remembra
59
+ ```
60
+
61
+ **OpenCode** (`~/.config/opencode/opencode.json`):
62
+
63
+ ```json
64
+ {
65
+ "mcp": {
66
+ "remembra": {
67
+ "type": "local",
68
+ "command": ["remembra"]
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ More clients (Cline, Kimi Code) in **[docs/clients.md](docs/clients.md)**.
75
+
76
+ ## Tools
77
+
78
+ | Tool | Purpose |
79
+ |------|---------|
80
+ | `memory_store` | Save a fact / decision / role / history |
81
+ | `memory_search` | Retrieve relevant memories (pass `scope` = current project) |
82
+ | `memory_list` | Browse stored memories |
83
+ | `memory_forget` | Delete by id |
84
+
85
+ Full reference: **[docs/tools.md](docs/tools.md)**
86
+
87
+ ## Documentation
88
+
89
+ | Doc | What's inside |
90
+ |-----|--------------|
91
+ | [Memory model](docs/memory-model.md) | Types, scopes, ranking, storage format |
92
+ | [Tool reference](docs/tools.md) | Every MCP tool with arguments |
93
+ | [Client setup](docs/clients.md) | Config for each supported tool |
94
+ | [Contributing](CONTRIBUTING.md) | Dev workflow and guidelines |
95
+ | [Changelog](CHANGELOG.md) | Release history |
96
+
97
+ ## Storage
98
+
99
+ Memories live as readable markdown files you can inspect, edit, and version:
100
+
101
+ ```
102
+ ~/.remembra/
103
+ ├── global/ # always-relevant memories
104
+ └── scopes/
105
+ └── <project>/ # project-scoped memories
106
+ ```
107
+
108
+ Override the location with `REMEMBRA_HOME`.
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ git clone https://github.com/Hilbras/Remembra.git
114
+ cd Remembra
115
+ npm install
116
+ npm run build # compile
117
+ npm test # run tests
118
+ ```
119
+
120
+ ## Roadmap
121
+
122
+ - **v1** *(current)* — MCP server for coding tools, file storage, layered retrieval
123
+ - **v1.5** — HTTP API + ChatGPT Custom GPT action
124
+ - **v2** — automatic session-digest extraction, embeddings behind `memory_search`
125
+ - **v3** — SQLite for scale, duplicate merging, memory decay
126
+
127
+ ## License
128
+
129
+ [MIT](LICENSE) © Hilbras
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { MemoryStore } from "./store.js";
6
+ import { search } from "./retrieval.js";
7
+ import { StoreInput } from "./types.js";
8
+ const store = new MemoryStore(MemoryStore.defaultRoot());
9
+ const server = new McpServer({ name: "remembra", version: "0.1.0" });
10
+ server.registerTool("memory_store", {
11
+ title: "Store a memory",
12
+ description: "Persist a fact, decision, role or history entry so it survives context window resets. " +
13
+ "Use type 'fact' for stable knowledge, 'decision' for choices already made, " +
14
+ "'role' for standing instructions/roles, 'history' for condensed chronology of past work.",
15
+ inputSchema: {
16
+ type: z.enum(["fact", "decision", "role", "history"]),
17
+ content: z.string().describe("The memory itself, written as a standalone statement"),
18
+ scope: z
19
+ .string()
20
+ .optional()
21
+ .describe("'global' for always-relevant memories, or a project path/id for project-scoped ones"),
22
+ tags: z.array(z.string()).optional(),
23
+ importance: z.number().int().min(1).max(5).optional().describe("1=minor, 5=critical (default 3)"),
24
+ source: z.string().optional().describe("Originating session or client"),
25
+ },
26
+ }, async ({ type, content, scope, tags, importance, source }) => {
27
+ const memory = await store.store(StoreInput.parse({ type, content, scope, tags, importance, source }));
28
+ return { content: [{ type: "text", text: `Stored ${memory.type} memory ${memory.id} (scope: ${memory.scope})` }] };
29
+ });
30
+ server.registerTool("memory_search", {
31
+ title: "Search memories",
32
+ description: "Retrieve relevant memories from external storage. Call this at the start of a session " +
33
+ "(or whenever prior context might exist) to recover facts, decisions, roles and history.",
34
+ inputSchema: {
35
+ query: z.string().optional().describe("Keywords to match (omit to get a scope/recency-ranked list)"),
36
+ scope: z.string().optional().describe("Current project path or workspace id to filter by"),
37
+ type: z.enum(["fact", "decision", "role", "history"]).optional(),
38
+ limit: z.number().int().min(1).max(50).optional(),
39
+ },
40
+ }, async ({ query, scope, type, limit }) => {
41
+ const results = search(await store.all(), { query, scope, type, limit });
42
+ const text = results.length === 0
43
+ ? "No matching memories."
44
+ : results
45
+ .map((m) => `[${m.id}] ${m.type.toUpperCase()} (scope: ${m.scope}, importance: ${m.importance}, ${m.updatedAt.slice(0, 10)})\n${m.content}`)
46
+ .join("\n\n");
47
+ return { content: [{ type: "text", text }] };
48
+ });
49
+ server.registerTool("memory_list", {
50
+ title: "List memories",
51
+ description: "List stored memories, optionally filtered by scope or type.",
52
+ inputSchema: {
53
+ scope: z.string().optional(),
54
+ type: z.enum(["fact", "decision", "role", "history"]).optional(),
55
+ },
56
+ }, async ({ scope, type }) => {
57
+ let memories = await store.all();
58
+ if (scope)
59
+ memories = memories.filter((m) => m.scope === scope || m.scope === "global");
60
+ if (type)
61
+ memories = memories.filter((m) => m.type === type);
62
+ memories.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
63
+ const text = memories.length === 0
64
+ ? "No memories stored yet."
65
+ : memories.map((m) => `[${m.id}] ${m.type} (${m.scope}): ${m.content.split("\n")[0]}`).join("\n");
66
+ return { content: [{ type: "text", text }] };
67
+ });
68
+ server.registerTool("memory_forget", {
69
+ title: "Delete a memory",
70
+ description: "Permanently delete a memory by its id.",
71
+ inputSchema: { id: z.string() },
72
+ }, async ({ id }) => {
73
+ const ok = await store.forget(id);
74
+ return {
75
+ content: [{ type: "text", text: ok ? `Deleted memory ${id}.` : `No memory with id ${id}.` }],
76
+ isError: !ok,
77
+ };
78
+ });
79
+ const transport = new StdioServerTransport();
80
+ await server.connect(transport);
81
+ console.error(`Remembra memory server running (root: ${MemoryStore.defaultRoot()})`);
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { Memory, SearchQuery } from "./types.js";
2
+ /**
3
+ * Layered retrieval (decision from Q4):
4
+ * 1. Roles always pass (they are instructions).
5
+ * 2. Scope match (global + current scope beat other scopes).
6
+ * 3. Importance, recency, and keyword overlap contribute to the score.
7
+ *
8
+ * Embeddings can replace the keyword part later without changing callers.
9
+ */
10
+ export declare function search(memories: Memory[], q: SearchQuery): Memory[];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Layered retrieval (decision from Q4):
3
+ * 1. Roles always pass (they are instructions).
4
+ * 2. Scope match (global + current scope beat other scopes).
5
+ * 3. Importance, recency, and keyword overlap contribute to the score.
6
+ *
7
+ * Embeddings can replace the keyword part later without changing callers.
8
+ */
9
+ export function search(memories, q) {
10
+ const now = Date.now();
11
+ const terms = (q.query ?? "")
12
+ .toLowerCase()
13
+ .split(/\s+/)
14
+ .filter((t) => t.length > 1);
15
+ const scored = memories
16
+ .filter((m) => (q.type ? m.type === q.type : true))
17
+ .map((m) => ({ m, score: score(m, terms, q.scope, now) }))
18
+ .filter(({ m, score }) => m.type === "role" || score > 0)
19
+ .sort((a, b) => b.score - a.score || b.m.updatedAt.localeCompare(a.m.updatedAt));
20
+ return scored.slice(0, q.limit ?? 10).map(({ m }) => m);
21
+ }
22
+ function score(m, terms, scope, now) {
23
+ let s = 0;
24
+ if (m.type === "role")
25
+ s += 1000;
26
+ if (m.scope === "global")
27
+ s += 100;
28
+ if (scope && m.scope === scope)
29
+ s += 150;
30
+ if (scope && m.scope !== scope && m.scope !== "global")
31
+ return 0; // other projects' memories don't leak in
32
+ s += m.importance * 10;
33
+ // Recency: half-life of ~30 days.
34
+ const ageDays = (now - Date.parse(m.updatedAt)) / 86_400_000;
35
+ if (!Number.isNaN(ageDays))
36
+ s += Math.max(0, 20 - ageDays / 3);
37
+ // Keyword overlap over content + tags.
38
+ if (terms.length > 0) {
39
+ const hay = (m.content + " " + m.tags.join(" ")).toLowerCase();
40
+ let hits = 0;
41
+ for (const t of terms)
42
+ if (hay.includes(t))
43
+ hits++;
44
+ s += (hits / terms.length) * 60;
45
+ }
46
+ else {
47
+ s += 10; // no query: everything eligible scores a little
48
+ }
49
+ return s;
50
+ }
51
+ //# sourceMappingURL=retrieval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrieval.js","sourceRoot":"","sources":["../src/retrieval.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { Memory, StoreInput } from "./types.js";
2
+ /**
3
+ * File-based memory store.
4
+ *
5
+ * Layout (default root: ~/.remembra):
6
+ * global/<id>.md — memories valid everywhere
7
+ * scopes/<scope>/<id>.md — memories scoped to a project/workspace
8
+ *
9
+ * Each file is markdown with YAML-ish frontmatter for human readability
10
+ * and simple greppability.
11
+ */
12
+ export declare class MemoryStore {
13
+ private readonly root;
14
+ constructor(root: string);
15
+ static defaultRoot(): string;
16
+ private fileFor;
17
+ store(input: StoreInput): Promise<Memory>;
18
+ forget(id: string): Promise<boolean>;
19
+ /** Load every memory across all scopes (global + scoped). */
20
+ all(): Promise<Memory[]>;
21
+ get(id: string): Promise<Memory | null>;
22
+ private findFile;
23
+ }
package/dist/store.js ADDED
@@ -0,0 +1,143 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { randomUUID } from "node:crypto";
5
+ /**
6
+ * File-based memory store.
7
+ *
8
+ * Layout (default root: ~/.remembra):
9
+ * global/<id>.md — memories valid everywhere
10
+ * scopes/<scope>/<id>.md — memories scoped to a project/workspace
11
+ *
12
+ * Each file is markdown with YAML-ish frontmatter for human readability
13
+ * and simple greppability.
14
+ */
15
+ export class MemoryStore {
16
+ root;
17
+ constructor(root) {
18
+ this.root = root;
19
+ }
20
+ static defaultRoot() {
21
+ return process.env.REMEMBRA_HOME ?? path.join(os.homedir(), ".remembra");
22
+ }
23
+ fileFor(m) {
24
+ if (m.scope === "global") {
25
+ return path.join(this.root, "global", `${m.id}.md`);
26
+ }
27
+ const safeScope = m.scope.replace(/[^a-zA-Z0-9._/-]/g, "_");
28
+ return path.join(this.root, "scopes", safeScope, `${m.id}.md`);
29
+ }
30
+ async store(input) {
31
+ const now = new Date().toISOString();
32
+ const memory = {
33
+ id: randomUUID().slice(0, 8),
34
+ type: input.type,
35
+ content: input.content,
36
+ scope: input.scope,
37
+ tags: input.tags,
38
+ importance: input.importance,
39
+ createdAt: now,
40
+ updatedAt: now,
41
+ source: input.source,
42
+ };
43
+ const file = this.fileFor(memory);
44
+ await fs.mkdir(path.dirname(file), { recursive: true });
45
+ await fs.writeFile(file, render(memory), "utf8");
46
+ return memory;
47
+ }
48
+ async forget(id) {
49
+ const file = await this.findFile(id);
50
+ if (!file)
51
+ return false;
52
+ await fs.unlink(file);
53
+ return true;
54
+ }
55
+ /** Load every memory across all scopes (global + scoped). */
56
+ async all() {
57
+ const files = [];
58
+ for (const dir of [path.join(this.root, "global"), path.join(this.root, "scopes")]) {
59
+ files.push(...(await walk(dir)));
60
+ }
61
+ const memories = await Promise.all(files.map((f) => parse(f)));
62
+ return memories.filter((m) => m !== null);
63
+ }
64
+ async get(id) {
65
+ const file = await this.findFile(id);
66
+ if (!file)
67
+ return null;
68
+ return parse(file);
69
+ }
70
+ async findFile(id) {
71
+ const files = await walk(path.join(this.root, "global"), path.join(this.root, "scopes"));
72
+ return files.find((f) => path.basename(f, ".md") === id) ?? null;
73
+ }
74
+ }
75
+ async function walk(...dirs) {
76
+ const out = [];
77
+ for (const dir of dirs) {
78
+ let entries;
79
+ try {
80
+ entries = await fs.readdir(dir, { withFileTypes: true });
81
+ }
82
+ catch {
83
+ continue;
84
+ }
85
+ for (const e of entries) {
86
+ const full = path.join(dir, e.name);
87
+ if (e.isDirectory())
88
+ out.push(...(await walk(full)));
89
+ else if (e.name.endsWith(".md"))
90
+ out.push(full);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+ function render(m) {
96
+ const lines = [
97
+ "---",
98
+ `id: ${m.id}`,
99
+ `type: ${m.type}`,
100
+ `scope: ${m.scope}`,
101
+ `tags: [${m.tags.join(", ")}]`,
102
+ `importance: ${m.importance}`,
103
+ `created: ${m.createdAt}`,
104
+ `updated: ${m.updatedAt}`,
105
+ m.source ? `source: ${m.source}` : undefined,
106
+ "---",
107
+ "",
108
+ m.content,
109
+ "",
110
+ ];
111
+ return lines.filter((l) => l !== undefined).join("\n");
112
+ }
113
+ async function parse(file) {
114
+ try {
115
+ const raw = await fs.readFile(file, "utf8");
116
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
117
+ if (!match)
118
+ return null;
119
+ const meta = {};
120
+ for (const line of match[1].split("\n")) {
121
+ const idx = line.indexOf(":");
122
+ if (idx === -1)
123
+ continue;
124
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
125
+ }
126
+ const tagsRaw = (meta.tags ?? "[]").replace(/^\[|\]$/g, "");
127
+ return {
128
+ id: meta.id ?? path.basename(file, ".md"),
129
+ type: (meta.type ?? "fact"),
130
+ content: match[2].trim(),
131
+ scope: meta.scope ?? "global",
132
+ tags: tagsRaw ? tagsRaw.split(",").map((t) => t.trim()) : [],
133
+ importance: Number(meta.importance ?? 3),
134
+ createdAt: meta.created ?? new Date(0).toISOString(),
135
+ updatedAt: meta.updated ?? meta.created ?? new Date(0).toISOString(),
136
+ source: meta.source,
137
+ };
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC;;;;;;;;;GASG;AACH,MAAM,OAAO,WAAW;IACO;IAA7B,YAA6B,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IAE7C,MAAM,CAAC,WAAW;QAChB,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;IAC3E,CAAC;IAEO,OAAO,CAAC,CAAS;QACvB,IAAI,CAAC,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;QAC5D,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAiB;QAC3B,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,MAAM,GAAW;YACrB,EAAE,EAAE,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;YAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;YACd,MAAM,EAAE,KAAK,CAAC,MAAM;SACrB,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAClC,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QACjD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAU;QACrB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI;YAAE,OAAO,KAAK,CAAC;QACxB,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,GAAG;QACP,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;YACnF,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/D,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,EAAU;QAClB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,EAAU;QAC/B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;QACzF,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC;IACnE,CAAC;CACF;AAED,KAAK,UAAU,IAAI,CAAC,GAAG,IAAc;IACnC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,OAAO,CAAC;QACZ,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,CAAC,CAAC,WAAW,EAAE;gBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;iBAChD,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,MAAM,KAAK,GAAG;QACZ,KAAK;QACL,OAAO,CAAC,CAAC,EAAE,EAAE;QACb,SAAS,CAAC,CAAC,IAAI,EAAE;QACjB,UAAU,CAAC,CAAC,KAAK,EAAE;QACnB,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QAC9B,eAAe,CAAC,CAAC,UAAU,EAAE;QAC7B,YAAY,CAAC,CAAC,SAAS,EAAE;QACzB,YAAY,CAAC,CAAC,SAAS,EAAE;QACzB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS;QAC5C,KAAK;QACL,EAAE;QACF,CAAC,CAAC,OAAO;QACT,EAAE;KACH,CAAC;IACF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzD,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,IAAY;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAChE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,MAAM,IAAI,GAA2B,EAAE,CAAC;QACxC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;gBAAE,SAAS;YACzB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/D,CAAC;QACD,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC5D,OAAO;YACL,EAAE,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;YACzC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,MAAM,CAAmB;YAC7C,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;YACxB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,QAAQ;YAC7B,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;YAC5D,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;YACxC,SAAS,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;YACpD,SAAS,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;YACpE,MAAM,EAAE,IAAI,CAAC,MAAM;SACpB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ /** The four memory types Remembra stores. */
3
+ export declare const MemoryType: z.ZodEnum<["fact", "decision", "role", "history"]>;
4
+ export type MemoryType = z.infer<typeof MemoryType>;
5
+ /**
6
+ * Scope of a memory.
7
+ * - "global": always relevant (user preferences, roles, general facts)
8
+ * - any other string: project/workspace scope (e.g. a repo path or project id)
9
+ */
10
+ export type MemoryScope = string;
11
+ export interface Memory {
12
+ id: string;
13
+ type: MemoryType;
14
+ content: string;
15
+ scope: MemoryScope;
16
+ tags: string[];
17
+ importance: number;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ source?: string;
21
+ }
22
+ export declare const StoreInput: z.ZodObject<{
23
+ type: z.ZodEnum<["fact", "decision", "role", "history"]>;
24
+ content: z.ZodString;
25
+ scope: z.ZodDefault<z.ZodString>;
26
+ tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
27
+ importance: z.ZodDefault<z.ZodNumber>;
28
+ source: z.ZodOptional<z.ZodString>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ type: "fact" | "decision" | "role" | "history";
31
+ content: string;
32
+ scope: string;
33
+ tags: string[];
34
+ importance: number;
35
+ source?: string | undefined;
36
+ }, {
37
+ type: "fact" | "decision" | "role" | "history";
38
+ content: string;
39
+ scope?: string | undefined;
40
+ tags?: string[] | undefined;
41
+ importance?: number | undefined;
42
+ source?: string | undefined;
43
+ }>;
44
+ export type StoreInput = z.infer<typeof StoreInput>;
45
+ export interface SearchQuery {
46
+ query?: string;
47
+ scope?: string;
48
+ type?: MemoryType;
49
+ limit?: number;
50
+ }
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ /** The four memory types Remembra stores. */
3
+ export const MemoryType = z.enum(["fact", "decision", "role", "history"]);
4
+ export const StoreInput = z.object({
5
+ type: MemoryType,
6
+ content: z.string().min(1),
7
+ scope: z.string().default("global"),
8
+ tags: z.array(z.string()).default([]),
9
+ importance: z.number().int().min(1).max(5).default(3),
10
+ source: z.string().optional(),
11
+ });
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,6CAA6C;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAsB1E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IACnC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACrD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC"}
@@ -0,0 +1,90 @@
1
+ # Client Setup
2
+
3
+ Remembra speaks MCP over stdio — one server process, any compatible client.
4
+
5
+ > Replace `/home/gin/work/Hilbras/Memory` with wherever you installed Remembra,
6
+ > or use the global install: `npx @hilbras/remembra`.
7
+
8
+ ## OpenCode
9
+
10
+ Add to `~/.config/opencode/opencode.json` (global) or a project's `opencode.json`:
11
+
12
+ ```json
13
+ {
14
+ "mcp": {
15
+ "remembra": {
16
+ "type": "local",
17
+ "command": ["node", "/home/gin/work/Hilbras/Memory/dist/index.js"]
18
+ }
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## Claude Code
24
+
25
+ ```bash
26
+ claude mcp add remembra -- node /home/gin/work/Hilbras/Memory/dist/index.js
27
+ ```
28
+
29
+ Or in `.mcp.json` (project) / `~/.claude.json` (user):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "remembra": {
35
+ "command": "node",
36
+ "args": ["/home/gin/work/Hilbras/Memory/dist/index.js"]
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## Cline
43
+
44
+ Cline settings → MCP Servers → Add:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "remembra": {
50
+ "command": "node",
51
+ "args": ["/home/gin/work/Hilbras/Memory/dist/index.js"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Kimi Code
58
+
59
+ Add to the MCP config file used by Kimi CLI:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "remembra": {
65
+ "command": "node",
66
+ "args": ["/home/gin/work/Hilbras/Memory/dist/index.js"]
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## ChatGPT
73
+
74
+ *Planned for v1.5* — an HTTP layer exposing the same handlers, wired to a
75
+ Custom GPT action with API-key auth.
76
+
77
+ ## Environment
78
+
79
+ | Variable | Default | Purpose |
80
+ |----------|---------|---------|
81
+ | `REMEMBRA_HOME` | `~/.remembra` | Where memory files live |
82
+
83
+ ## Tips
84
+
85
+ - **Pass `scope`** — every client should give Remembra its current project path when
86
+ calling `memory_search`, so project memories don't mix across repos.
87
+ - **Roles are always injected** — put standing instructions in `type: "role"` and they'll
88
+ never be filtered out by ranking.
89
+ - **One server, shared brain** — all clients write to the same storage, so a decision made
90
+ in Claude Code is visible in OpenCode.
@@ -0,0 +1,114 @@
1
+ # The Memory Model
2
+
3
+ Remembra stores four kinds of memories. Every memory you save carries one of these types,
4
+ and the type controls how it's used at retrieval time.
5
+
6
+ ## Types
7
+
8
+ ### `fact`
9
+ Stable knowledge about the world, a project, or the user.
10
+
11
+ ```
12
+ type: fact
13
+ content: The API rate limit is 100 requests/minute.
14
+ content: User prefers concise answers without filler.
15
+ ```
16
+
17
+ ### `decision`
18
+ Choices that have already been made — so they don't get re-litigated every session.
19
+
20
+ ```
21
+ type: decision
22
+ content: Chose PostgreSQL over MongoDB for the events table (decided 2026-09-20).
23
+ ```
24
+
25
+ ### `role`
26
+ Standing instructions and roles. **Roles always surface in search results** — they are
27
+ treated as instructions, not suggestions.
28
+
29
+ ```
30
+ type: role
31
+ content: You are the architect. Never modify test files without asking.
32
+ ```
33
+
34
+ ### `history`
35
+ Condensed chronology of what already happened — a lean summary instead of a raw transcript.
36
+
37
+ ```
38
+ type: history
39
+ content: Phase 1: built the auth API, hit a token refresh bug, resolved by rotating keys.
40
+ ```
41
+
42
+ ## Scopes
43
+
44
+ Every memory belongs to exactly one scope:
45
+
46
+ | Scope | Meaning | Injected when |
47
+ |-------|---------|---------------|
48
+ | `global` | Relevant everywhere (preferences, roles, general facts) | Any session |
49
+ | `<project path or id>` | Belongs to one project/workspace | Only sessions in that scope |
50
+
51
+ Scope isolation is strict: memories scoped to `/repo/a` are **never** returned to a session
52
+ in `/repo/b`. Global memories are always visible.
53
+
54
+ ```json
55
+ { "type": "decision", "content": "...", "scope": "/home/gin/work/Hilbras/Memory" }
56
+ ```
57
+
58
+ ## Metadata
59
+
60
+ | Field | Type | Notes |
61
+ |-------|------|-------|
62
+ | `type` | `fact \| decision \| role \| history` | required |
63
+ | `content` | string | required — written as a standalone statement |
64
+ | `scope` | string | defaults to `global` |
65
+ | `tags` | string[] | boosts keyword matching |
66
+ | `importance` | 1–5 | defaults to 3; higher ranks higher |
67
+ | `source` | string | originating session/client (optional) |
68
+ | `id` | 8-char id | assigned automatically |
69
+ | `createdAt` / `updatedAt` | ISO timestamps | assigned automatically |
70
+
71
+ ## Retrieval ranking
72
+
73
+ When `memory_search` runs, memories are scored in layers:
74
+
75
+ 1. **Roles always pass** (+1000) — instructions never get filtered out.
76
+ 2. **Scope gate** — other projects' memories are excluded entirely;
77
+ the current scope scores highest (+150), `global` always passes (+100).
78
+ 3. **Importance** — up to +50 for importance 5.
79
+ 4. **Recency** — decays over roughly a 30-day half-life (up to +20).
80
+ 5. **Keyword overlap** — up to +60 based on the fraction of query terms matched
81
+ in content and tags.
82
+
83
+ Embeddings are planned for v2 and will slot in behind the same `memory_search`
84
+ interface without changing any client.
85
+
86
+ ## Storage format
87
+
88
+ Memories are plain markdown files with frontmatter — greppable, editable by hand,
89
+ git-friendly:
90
+
91
+ ```markdown
92
+ ---
93
+ id: 7133edba
94
+ type: decision
95
+ scope: global
96
+ tags: [architecture]
97
+ importance: 4
98
+ created: 2026-09-23T02:51:41.999Z
99
+ updated: 2026-09-23T02:51:41.999Z
100
+ ---
101
+
102
+ Chose file-based storage for v1
103
+ ```
104
+
105
+ Layout:
106
+
107
+ ```
108
+ $REMEMBRA_HOME/ # defaults to ~/.remembra
109
+ ├── global/
110
+ │ └── <id>.md
111
+ └── scopes/
112
+ └── <scope>/
113
+ └── <id>.md
114
+ ```
package/docs/tools.md ADDED
@@ -0,0 +1,75 @@
1
+ # Tool Reference
2
+
3
+ Remembra exposes four MCP tools. All of them work the same way across every
4
+ MCP-compatible client.
5
+
6
+ ## `memory_store`
7
+
8
+ Persist a memory so it survives context-window resets.
9
+
10
+ | Argument | Type | Required | Default | Description |
11
+ |----------|------|----------|---------|-------------|
12
+ | `type` | `fact \| decision \| role \| history` | ✅ | — | What kind of memory this is |
13
+ | `content` | string | ✅ | — | The memory, written as a standalone statement |
14
+ | `scope` | string | no | `global` | `global`, or a project path/id |
15
+ | `tags` | string[] | no | `[]` | Keywords that boost retrieval |
16
+ | `importance` | 1–5 | no | `3` | Ranking weight |
17
+ | `source` | string | no | — | Originating session/client |
18
+
19
+ **When to use which type:**
20
+ - Something the model should *know* → `fact`
21
+ - Something already *decided* → `decision`
22
+ - A standing *instruction* or persona → `role`
23
+ - A summary of *what happened* → `history`
24
+
25
+ Returns the assigned memory id.
26
+
27
+ ## `memory_search`
28
+
29
+ Retrieve relevant memories. Call at the **start of a session** (to recover prior context)
30
+ and whenever earlier work might be referenced.
31
+
32
+ | Argument | Type | Required | Description |
33
+ |----------|------|----------|-------------|
34
+ | `query` | string | no | Keywords; omit for a scope/recency-ranked list |
35
+ | `scope` | string | no | Current project path/id — pass it whenever you have one |
36
+ | `type` | memory type | no | Restrict to one type |
37
+ | `limit` | 1–50 | no | Max results (default 10) |
38
+
39
+ Returns formatted memories:
40
+
41
+ ```
42
+ [7133edba] DECISION (scope: global, importance: 4, 2026-09-23)
43
+ Chose file-based storage for v1
44
+ ```
45
+
46
+ ## `memory_list`
47
+
48
+ Browse what's stored — useful for auditing or showing the user their memory.
49
+
50
+ | Argument | Type | Required | Description |
51
+ |----------|------|----------|-------------|
52
+ | `scope` | string | no | Filter by scope (includes `global`) |
53
+ | `type` | memory type | no | Filter by type |
54
+
55
+ Returns one line per memory, newest first.
56
+
57
+ ## `memory_forget`
58
+
59
+ Permanently delete a memory.
60
+
61
+ | Argument | Type | Required | Description |
62
+ |----------|------|----------|-------------|
63
+ | `id` | string | ✅ | Memory id (from `memory_store` or `memory_list`) |
64
+
65
+ Returns an error result if no memory matches the id.
66
+
67
+ ---
68
+
69
+ ## Suggested session flow
70
+
71
+ ```
72
+ 1. memory_search { scope: <current project> } → recover roles, facts, decisions
73
+ 2. ... work happens; model calls memory_store when something worth keeping emerges ...
74
+ 3. (v2) automatic session digest sweeps anything missed
75
+ ```
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@hilbras/remembra",
3
+ "version": "0.1.0",
4
+ "description": "External memory for AI assistants — remember facts, decisions, roles and history across sessions. MCP server for OpenCode, Claude Code, Cline, Kimi Code and more.",
5
+ "type": "module",
6
+ "bin": {
7
+ "remembra": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "!dist/test",
14
+ "docs",
15
+ "README.md",
16
+ "LICENSE",
17
+ "CHANGELOG.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "start": "node dist/index.js",
23
+ "test": "node --test \"dist/test/*.test.js\"",
24
+ "prepublishOnly": "npm run build && npm test"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Hilbras/Remembra.git"
29
+ },
30
+ "homepage": "https://github.com/Hilbras/Remembra#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/Hilbras/Remembra/issues"
33
+ },
34
+ "keywords": [
35
+ "mcp",
36
+ "model-context-protocol",
37
+ "memory",
38
+ "ai",
39
+ "assistant",
40
+ "context",
41
+ "external-memory",
42
+ "long-term-memory",
43
+ "llm",
44
+ "opencode",
45
+ "claude-code",
46
+ "cline"
47
+ ],
48
+ "author": "Hilbras",
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "dependencies": {
57
+ "@modelcontextprotocol/sdk": "^1.12.0",
58
+ "zod": "^3.24.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^24.0.0",
62
+ "typescript": "^5.8.0"
63
+ }
64
+ }