@matrixorigin/thememoria 0.4.0 → 0.4.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.
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Group 3: Cloud API Communication
3
+ * "Does the plugin talk to Memoria API correctly?"
4
+ *
5
+ * Bug 2 regression: no X-User-Id header should be sent (API key determines identity).
6
+ */
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import { MemoriaHttpTransport } from "../http-client.js";
9
+ import { buildApiConfig, mockFetch, type MockFetchCall } from "./helpers.js";
10
+
11
+ let fetchMock: ReturnType<typeof mockFetch>;
12
+ let transport: MemoriaHttpTransport;
13
+ const originalFetch = globalThis.fetch;
14
+
15
+ beforeEach(() => {
16
+ fetchMock = mockFetch();
17
+ transport = new MemoriaHttpTransport(buildApiConfig(), "test-user");
18
+ });
19
+
20
+ afterEach(() => {
21
+ globalThis.fetch = originalFetch;
22
+ });
23
+
24
+ describe("Group 3: Cloud API Communication", () => {
25
+ // ── Constructor & lifecycle ──────────────────────────────
26
+
27
+ describe("Constructor & lifecycle", () => {
28
+ it("3.1 missing apiUrl throws", () => {
29
+ expect(() => new MemoriaHttpTransport(
30
+ buildApiConfig({ apiUrl: undefined }), "u",
31
+ )).toThrow(/apiUrl/);
32
+ });
33
+
34
+ it("3.2 missing apiKey throws", () => {
35
+ expect(() => new MemoriaHttpTransport(
36
+ buildApiConfig({ apiKey: undefined }), "u",
37
+ )).toThrow(/apiKey/);
38
+ });
39
+
40
+ it("3.3 trailing slash stripped from apiUrl", async () => {
41
+ const t = new MemoriaHttpTransport(
42
+ buildApiConfig({ apiUrl: "https://example.com///" }), "u",
43
+ );
44
+ fetchMock.respondWith(200, { status: "ok" });
45
+ await t.healthCheck();
46
+ expect(fetchMock.lastCall()!.url).toMatch(/^https:\/\/example\.com\/health/);
47
+ });
48
+
49
+ it("3.4 isAlive() always returns true", () => {
50
+ expect(transport.isAlive()).toBe(true);
51
+ });
52
+
53
+ it("3.5 close() is a no-op", () => {
54
+ expect(() => transport.close()).not.toThrow();
55
+ });
56
+ });
57
+
58
+ // ── Auth & headers (Bug 2 regression) ────────────────────
59
+
60
+ describe("Auth & headers", () => {
61
+ it("3.6 every request has Authorization: Bearer <key>", async () => {
62
+ fetchMock.respondWith(200, { results: [] });
63
+ await transport.callTool("memory_retrieve", { query: "test", top_k: 5 });
64
+ expect(fetchMock.lastCall()!.headers["Authorization"]).toBe("Bearer sk-test-key-123");
65
+ });
66
+
67
+ it("3.7 every request has Content-Type: application/json", async () => {
68
+ fetchMock.respondWith(200, { results: [] });
69
+ await transport.callTool("memory_retrieve", { query: "test", top_k: 5 });
70
+ expect(fetchMock.lastCall()!.headers["Content-Type"]).toBe("application/json");
71
+ });
72
+
73
+ it("3.8 NO X-User-Id header sent (Bug 2 fix)", async () => {
74
+ fetchMock.respondWith(200, { results: [] });
75
+ await transport.callTool("memory_retrieve", { query: "test", top_k: 5 });
76
+ expect(fetchMock.lastCall()!.headers["X-User-Id"]).toBeUndefined();
77
+ });
78
+
79
+ it("3.8b every request has X-Memoria-Tool: openclaw", async () => {
80
+ fetchMock.respondWith(200, { results: [] });
81
+ await transport.callTool("memory_retrieve", { query: "test", top_k: 5 });
82
+ expect(fetchMock.lastCall()!.headers["X-Memoria-Tool"]).toBe("openclaw");
83
+ });
84
+ });
85
+
86
+ // ── Tool → endpoint mapping (table-driven) ──────────────
87
+
88
+ describe("Tool → endpoint mapping", () => {
89
+ const cases: Array<{
90
+ id: string;
91
+ tool: string;
92
+ args: Record<string, unknown>;
93
+ method: string;
94
+ pathPattern: RegExp;
95
+ }> = [
96
+ { id: "3.9", tool: "memory_store", args: { content: "hi" }, method: "POST", pathPattern: /\/v1\/memories$/ },
97
+ { id: "3.10", tool: "memory_retrieve", args: { query: "q" }, method: "POST", pathPattern: /\/v1\/memories\/retrieve$/ },
98
+ { id: "3.11", tool: "memory_search", args: { query: "q" }, method: "POST", pathPattern: /\/v1\/memories\/search$/ },
99
+ { id: "3.12", tool: "memory_list", args: { limit: 10 }, method: "GET", pathPattern: /\/v1\/memories\?limit=10/ },
100
+ { id: "3.13", tool: "memory_profile", args: {}, method: "GET", pathPattern: /\/v1\/profiles\/me$/ },
101
+ { id: "3.14", tool: "memory_correct", args: { memory_id: "abc", new_content: "x" }, method: "PUT", pathPattern: /\/v1\/memories\/abc\/correct$/ },
102
+ { id: "3.15", tool: "memory_correct", args: { query: "q", new_content: "x" }, method: "POST", pathPattern: /\/v1\/memories\/correct$/ },
103
+ { id: "3.16", tool: "memory_purge", args: { topic: "old" }, method: "POST", pathPattern: /\/v1\/memories\/purge$/ },
104
+ { id: "3.17", tool: "memory_observe", args: { messages: [] }, method: "POST", pathPattern: /\/v1\/observe$/ },
105
+ { id: "3.18", tool: "memory_governance", args: {}, method: "POST", pathPattern: /\/v1\/governance$/ },
106
+ { id: "3.19", tool: "memory_snapshot", args: { name: "s1" }, method: "POST", pathPattern: /\/v1\/snapshots$/ },
107
+ { id: "3.20", tool: "memory_snapshots", args: {}, method: "GET", pathPattern: /\/v1\/snapshots\?/ },
108
+ { id: "3.21", tool: "memory_rollback", args: { name: "s1" }, method: "POST", pathPattern: /\/v1\/snapshots\/s1\/rollback$/ },
109
+ { id: "3.22", tool: "memory_branch", args: { name: "b1" }, method: "POST", pathPattern: /\/v1\/branches$/ },
110
+ { id: "3.23", tool: "memory_branches", args: {}, method: "GET", pathPattern: /\/v1\/branches$/ },
111
+ { id: "3.24", tool: "memory_checkout", args: { name: "b1" }, method: "POST", pathPattern: /\/v1\/branches\/b1\/checkout$/ },
112
+ { id: "3.25", tool: "memory_branch_delete", args: { name: "b1" }, method: "DELETE", pathPattern: /\/v1\/branches\/b1$/ },
113
+ { id: "3.26", tool: "memory_merge", args: { source: "b1" }, method: "POST", pathPattern: /\/v1\/branches\/b1\/merge$/ },
114
+ { id: "3.27", tool: "memory_diff", args: { source: "b1" }, method: "GET", pathPattern: /\/v1\/branches\/b1\/diff\?/ },
115
+ ];
116
+
117
+ for (const { id, tool, args, method, pathPattern } of cases) {
118
+ it(`${id} ${tool} → ${method} ${pathPattern.source}`, async () => {
119
+ fetchMock.respondWith(200, tool === "memory_store"
120
+ ? { memory_id: "new-id", content: args.content ?? "" }
121
+ : tool.includes("retrieve") || tool.includes("search") || tool === "memory_list"
122
+ ? { results: [] }
123
+ : tool === "memory_profile"
124
+ ? { profile: "test" }
125
+ : tool === "memory_purge"
126
+ ? { purged: 1 }
127
+ : tool === "memory_correct"
128
+ ? { memory_id: "abc", content: "x" }
129
+ : tool === "memory_snapshots"
130
+ ? { snapshots: [] }
131
+ : tool === "memory_branches"
132
+ ? { branches: [] }
133
+ : { ok: true });
134
+ await transport.callTool(tool, args);
135
+ const call = fetchMock.lastCall()!;
136
+ expect(call.method).toBe(method);
137
+ expect(call.url).toMatch(pathPattern);
138
+ });
139
+ }
140
+
141
+ it("3.28 memory_rebuild_index returns cloud message, no HTTP call", async () => {
142
+ fetchMock.reset();
143
+ const result = await transport.callTool("memory_rebuild_index", {});
144
+ expect(fetchMock.calls.length).toBe(0);
145
+ const text = JSON.parse((result as any).content[0].text);
146
+ expect(text.message).toMatch(/cloud/i);
147
+ });
148
+
149
+ it("3.29 healthCheck → GET /health/instance", async () => {
150
+ fetchMock.respondWith(200, { status: "ok", instance_id: "i-1", db: true });
151
+ const result = await transport.healthCheck();
152
+ expect(fetchMock.lastCall()!.method).toBe("GET");
153
+ expect(fetchMock.lastCall()!.url).toMatch(/\/health\/instance$/);
154
+ expect(result.status).toBe("ok");
155
+ expect(result.instance_id).toBe("i-1");
156
+ });
157
+
158
+ it("3.30 unknown tool throws", async () => {
159
+ await expect(transport.callTool("memory_nonexistent", {}))
160
+ .rejects.toThrow(/Unknown Memoria tool/);
161
+ });
162
+ });
163
+
164
+ // ── MCP envelope compatibility ───────────────────────────
165
+
166
+ describe("MCP envelope", () => {
167
+ it("3.31 callTool wraps result in MCP content block", async () => {
168
+ fetchMock.respondWith(200, { memory_id: "x", content: "hello" });
169
+ const result = await transport.callTool("memory_store", { content: "hello" }) as any;
170
+ expect(result.content).toBeInstanceOf(Array);
171
+ expect(result.content[0].type).toBe("text");
172
+ expect(typeof result.content[0].text).toBe("string");
173
+ });
174
+
175
+ it("3.32 string result wrapped as-is", async () => {
176
+ fetchMock.respondWith(200, { profile: "User likes cats" });
177
+ const result = await transport.callTool("memory_profile", {}) as any;
178
+ expect(result.content[0].text).toBe("User likes cats");
179
+ });
180
+ });
181
+
182
+ // ── Response text formatting ─────────────────────────────
183
+
184
+ describe("Response text formatting", () => {
185
+ it("3.34 memory_store → 'Stored memory <id>: <content>'", async () => {
186
+ fetchMock.respondWith(200, { memory_id: "m1", content: "hello world" });
187
+ const result = await transport.callTool("memory_store", { content: "hello world" }) as any;
188
+ expect(result.content[0].text).toBe("Stored memory m1: hello world");
189
+ });
190
+
191
+ it("3.35 memory_retrieve with results → '[id] (type) content' per line", async () => {
192
+ fetchMock.respondWith(200, {
193
+ results: [
194
+ { memory_id: "m1", memory_type: "semantic", content: "First" },
195
+ { memory_id: "m2", memory_type: "profile", content: "Second" },
196
+ ],
197
+ });
198
+ const result = await transport.callTool("memory_retrieve", { query: "q" }) as any;
199
+ const text = result.content[0].text;
200
+ expect(text).toContain("[m1] (semantic) First");
201
+ expect(text).toContain("[m2] (profile) Second");
202
+ });
203
+
204
+ it("3.36 memory_retrieve empty → 'No relevant memories found.'", async () => {
205
+ fetchMock.respondWith(200, { results: [] });
206
+ const result = await transport.callTool("memory_retrieve", { query: "q" }) as any;
207
+ expect(result.content[0].text).toBe("No relevant memories found.");
208
+ });
209
+
210
+ it("3.38 memory_profile with profile → profile text", async () => {
211
+ fetchMock.respondWith(200, { profile: "Likes TypeScript" });
212
+ const result = await transport.callTool("memory_profile", {}) as any;
213
+ expect(result.content[0].text).toBe("Likes TypeScript");
214
+ });
215
+
216
+ it("3.39 memory_profile no profile → 'No profile memories found.'", async () => {
217
+ fetchMock.respondWith(200, { profile: null });
218
+ const result = await transport.callTool("memory_profile", {}) as any;
219
+ expect(result.content[0].text).toBe("No profile memories found.");
220
+ });
221
+
222
+ it("3.40 memory_correct → 'Corrected memory <id>: <content>'", async () => {
223
+ fetchMock.respondWith(200, { memory_id: "m1", content: "updated" });
224
+ const result = await transport.callTool("memory_correct", {
225
+ memory_id: "m1", new_content: "updated",
226
+ }) as any;
227
+ expect(result.content[0].text).toBe("Corrected memory m1: updated");
228
+ });
229
+
230
+ it("3.41 memory_purge → 'Purged N memory(ies).'", async () => {
231
+ fetchMock.respondWith(200, { purged: 3 });
232
+ const result = await transport.callTool("memory_purge", { topic: "old" }) as any;
233
+ expect(result.content[0].text).toBe("Purged 3 memory(ies).");
234
+ });
235
+
236
+ it("3.42 memory_snapshots with items → formatted list", async () => {
237
+ fetchMock.respondWith(200, {
238
+ snapshots: [
239
+ { name: "snap1", timestamp: "2026-03-23T10:00:00Z" },
240
+ { name: "snap2", timestamp: "2026-03-23T11:00:00Z" },
241
+ ],
242
+ });
243
+ const result = await transport.callTool("memory_snapshots", {}) as any;
244
+ const text = result.content[0].text;
245
+ expect(text).toContain("Snapshots (2):");
246
+ expect(text).toContain("snap1 (2026-03-23T10:00:00Z)");
247
+ expect(text).toContain("snap2 (2026-03-23T11:00:00Z)");
248
+ });
249
+
250
+ it("3.43 memory_snapshots empty → 'Snapshots (0):'", async () => {
251
+ fetchMock.respondWith(200, { snapshots: [] });
252
+ const result = await transport.callTool("memory_snapshots", {}) as any;
253
+ expect(result.content[0].text).toBe("Snapshots (0):");
254
+ });
255
+
256
+ it("3.44 memory_branches with items → formatted list with active marker", async () => {
257
+ fetchMock.respondWith(200, {
258
+ branches: [
259
+ { name: "main", active: true },
260
+ { name: "experiment", active: false },
261
+ ],
262
+ });
263
+ const result = await transport.callTool("memory_branches", {}) as any;
264
+ const text = result.content[0].text;
265
+ expect(text).toContain("Branches:");
266
+ expect(text).toContain("main ← active");
267
+ expect(text).toContain("experiment");
268
+ expect(text).not.toContain("experiment ← active");
269
+ });
270
+
271
+ it("3.45 memory_branches empty → default main active", async () => {
272
+ fetchMock.respondWith(200, { branches: [] });
273
+ const result = await transport.callTool("memory_branches", {}) as any;
274
+ expect(result.content[0].text).toContain("main ← active");
275
+ });
276
+ });
277
+
278
+ // ── Error handling ───────────────────────────────────────
279
+
280
+ describe("Error handling", () => {
281
+ it("3.46 non-2xx HTTP → error with method, path, status", async () => {
282
+ fetchMock.respondWith(500, "Internal Server Error");
283
+ await expect(transport.callTool("memory_retrieve", { query: "q" }))
284
+ .rejects.toThrow(/POST.*\/v1\/memories\/retrieve.*500/);
285
+ });
286
+
287
+ it("3.48 empty response body → { ok: true }", async () => {
288
+ // Override fetch to return empty body
289
+ globalThis.fetch = async () => new Response("", { status: 200 });
290
+ const t = new MemoriaHttpTransport(buildApiConfig(), "u");
291
+ const result = await t.callTool("memory_consolidate", {}) as any;
292
+ // Should not throw, result wraps { ok: true }
293
+ expect(result.content[0].text).toContain("ok");
294
+ });
295
+ });
296
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Group 4: Response Parsing
3
+ * "Can the plugin understand what Memoria returns?"
4
+ *
5
+ * Tests the text parsers that bridge Memoria's output to typed data structures.
6
+ * These parsers are used by both embedded (MCP binary) and api (HTTP) modes.
7
+ *
8
+ * Note: parsers are not exported from client.ts, so we test them indirectly
9
+ * through MemoriaClient methods with a mocked transport.
10
+ * For direct parser testing, we import the module and use the internal functions
11
+ * via a re-export helper or test the formatted text round-trip.
12
+ */
13
+ import { describe, it, expect, afterEach } from "vitest";
14
+ import { MemoriaHttpTransport } from "../http-client.js";
15
+ import { buildApiConfig, mockFetch } from "./helpers.js";
16
+
17
+ // We test parsers indirectly: http-client produces formatted text that
18
+ // matches the Rust MCP binary format, and client.ts parsers consume it.
19
+ // Here we verify the http-client output IS parseable by checking the
20
+ // text format matches the expected patterns.
21
+
22
+ const originalFetch = globalThis.fetch;
23
+ let fetchHelper: ReturnType<typeof mockFetch>;
24
+
25
+ function setup() {
26
+ fetchHelper = mockFetch();
27
+ return new MemoriaHttpTransport(buildApiConfig(), "test-user");
28
+ }
29
+
30
+ function teardown() {
31
+ globalThis.fetch = originalFetch;
32
+ }
33
+
34
+ describe("Group 4: Response Parsing (format round-trip)", () => {
35
+ afterEach(teardown);
36
+
37
+ // ── Memory list parsing ──────────────────────────────────
38
+
39
+ describe("Memory list text format", () => {
40
+ it("4.1 single memory → [id] (type) content", async () => {
41
+ const t = setup();
42
+ fetchHelper.respondWith(200, {
43
+ results: [{ memory_id: "abc-123", memory_type: "semantic", content: "Hello world" }],
44
+ });
45
+ const result = await t.callTool("memory_retrieve", { query: "q" }) as any;
46
+ const text = result.content[0].text;
47
+ expect(text).toMatch(/^\[abc-123\] \(semantic\) Hello world$/);
48
+ });
49
+
50
+ it("4.2 multiple memories → one per line", async () => {
51
+ const t = setup();
52
+ fetchHelper.respondWith(200, {
53
+ results: [
54
+ { memory_id: "m1", memory_type: "semantic", content: "First" },
55
+ { memory_id: "m2", memory_type: "profile", content: "Second" },
56
+ ],
57
+ });
58
+ const result = await t.callTool("memory_search", { query: "q" }) as any;
59
+ const lines = result.content[0].text.split("\n");
60
+ expect(lines).toHaveLength(2);
61
+ expect(lines[0]).toMatch(/^\[m1\] \(semantic\) First$/);
62
+ expect(lines[1]).toMatch(/^\[m2\] \(profile\) Second$/);
63
+ });
64
+
65
+ it("4.3 empty results → 'No relevant memories found.'", async () => {
66
+ const t = setup();
67
+ fetchHelper.respondWith(200, { results: [] });
68
+ const result = await t.callTool("memory_retrieve", { query: "q" }) as any;
69
+ expect(result.content[0].text).toBe("No relevant memories found.");
70
+ });
71
+
72
+ it("4.4 array response shape (no results wrapper)", async () => {
73
+ const t = setup();
74
+ fetchHelper.respondWith(200, [
75
+ { memory_id: "m1", memory_type: "semantic", content: "Direct array" },
76
+ ]);
77
+ const result = await t.callTool("memory_retrieve", { query: "q" }) as any;
78
+ expect(result.content[0].text).toContain("[m1] (semantic) Direct array");
79
+ });
80
+ });
81
+
82
+ // ── Store/correct/purge parsing ──────────────────────────
83
+
84
+ describe("Store/correct/purge text format", () => {
85
+ it("4.5 store → 'Stored memory <id>: <content>'", async () => {
86
+ const t = setup();
87
+ fetchHelper.respondWith(200, { memory_id: "new-1", content: "Test memory" });
88
+ const result = await t.callTool("memory_store", { content: "Test memory" }) as any;
89
+ expect(result.content[0].text).toBe("Stored memory new-1: Test memory");
90
+ });
91
+
92
+ it("4.6 store with missing id → uses empty string", async () => {
93
+ const t = setup();
94
+ fetchHelper.respondWith(200, { content: "No id returned" });
95
+ const result = await t.callTool("memory_store", { content: "No id returned" }) as any;
96
+ expect(result.content[0].text).toBe("Stored memory : No id returned");
97
+ });
98
+
99
+ it("4.7 correct by id → 'Corrected memory <id>: <content>'", async () => {
100
+ const t = setup();
101
+ fetchHelper.respondWith(200, { memory_id: "m1", content: "Fixed" });
102
+ const result = await t.callTool("memory_correct", {
103
+ memory_id: "m1", new_content: "Fixed",
104
+ }) as any;
105
+ expect(result.content[0].text).toBe("Corrected memory m1: Fixed");
106
+ });
107
+
108
+ it("4.8 correct by query → same format", async () => {
109
+ const t = setup();
110
+ fetchHelper.respondWith(200, { memory_id: "m2", content: "Updated" });
111
+ const result = await t.callTool("memory_correct", {
112
+ query: "old content", new_content: "Updated",
113
+ }) as any;
114
+ expect(result.content[0].text).toBe("Corrected memory m2: Updated");
115
+ });
116
+
117
+ it("4.9 purge → 'Purged N memory(ies).'", async () => {
118
+ const t = setup();
119
+ fetchHelper.respondWith(200, { purged: 5 });
120
+ const result = await t.callTool("memory_purge", { topic: "test" }) as any;
121
+ expect(result.content[0].text).toBe("Purged 5 memory(ies).");
122
+ });
123
+
124
+ it("4.10 purge with zero → 'Purged 0 memory(ies).'", async () => {
125
+ const t = setup();
126
+ fetchHelper.respondWith(200, { purged: 0 });
127
+ const result = await t.callTool("memory_purge", { topic: "nothing" }) as any;
128
+ expect(result.content[0].text).toBe("Purged 0 memory(ies).");
129
+ });
130
+ });
131
+
132
+ // ── Snapshot list parsing ────────────────────────────────
133
+
134
+ describe("Snapshot list text format", () => {
135
+ it("4.11 snapshots → 'Snapshots (N):\\n name (ts)'", async () => {
136
+ const t = setup();
137
+ fetchHelper.respondWith(200, {
138
+ snapshots: [
139
+ { name: "before-refactor", timestamp: "2026-03-23T10:00:00Z" },
140
+ ],
141
+ });
142
+ const result = await t.callTool("memory_snapshots", {}) as any;
143
+ const text = result.content[0].text;
144
+ expect(text).toMatch(/^Snapshots \(1\):/);
145
+ expect(text).toContain("before-refactor (2026-03-23T10:00:00Z)");
146
+ });
147
+
148
+ it("4.12 empty snapshots → 'Snapshots (0):'", async () => {
149
+ const t = setup();
150
+ fetchHelper.respondWith(200, { snapshots: [] });
151
+ const result = await t.callTool("memory_snapshots", {}) as any;
152
+ expect(result.content[0].text).toBe("Snapshots (0):");
153
+ });
154
+ });
155
+
156
+ // ── Branch list parsing ──────────────────────────────────
157
+
158
+ describe("Branch list text format", () => {
159
+ it("4.14 branches with active marker", async () => {
160
+ const t = setup();
161
+ fetchHelper.respondWith(200, {
162
+ branches: [
163
+ { name: "main", active: true },
164
+ { name: "experiment", active: false },
165
+ ],
166
+ });
167
+ const result = await t.callTool("memory_branches", {}) as any;
168
+ const text = result.content[0].text;
169
+ expect(text).toContain("main ← active");
170
+ expect(text).toMatch(/^\s*experiment$/m);
171
+ });
172
+
173
+ it("4.15 branches with no active → main defaults active", async () => {
174
+ // When API returns no active flag, formatBranchList just passes through
175
+ // The client-side parseBranches defaults main to active
176
+ const t = setup();
177
+ fetchHelper.respondWith(200, {
178
+ branches: [
179
+ { name: "main", active: false },
180
+ { name: "dev", active: false },
181
+ ],
182
+ });
183
+ const result = await t.callTool("memory_branches", {}) as any;
184
+ const text = result.content[0].text;
185
+ // formatBranchList respects the active flag from API
186
+ expect(text).toContain("Branches:");
187
+ expect(text).toContain("main");
188
+ });
189
+
190
+ it("4.16 empty branches → default main active", async () => {
191
+ const t = setup();
192
+ fetchHelper.respondWith(200, { branches: [] });
193
+ const result = await t.callTool("memory_branches", {}) as any;
194
+ expect(result.content[0].text).toContain("main ← active");
195
+ });
196
+ });
197
+ });
@@ -6,6 +6,7 @@ import type {
6
6
  MemoriaTrustTier,
7
7
  } from "./config.js";
8
8
  import { MEMORIA_MEMORY_TYPES } from "./config.js";
9
+ import { MemoriaHttpTransport } from "./http-client.js";
9
10
 
10
11
  export type MemoriaMemoryRecord = {
11
12
  memory_id: string;
@@ -85,7 +86,7 @@ type McpContentBlock = {
85
86
  };
86
87
 
87
88
  const MCP_PROTOCOL_VERSION = "2024-11-05";
88
- const PLUGIN_VERSION = "0.3.0";
89
+ const PLUGIN_VERSION = "0.4.0";
89
90
  const MEMORY_LINE_RE = /^\[([^\]]+)\] \(([^)]+)\) ?([\s\S]*)$/;
90
91
 
91
92
  function asRecord(value: unknown): Record<string, unknown> | null {
@@ -425,13 +426,8 @@ class MemoriaMcpSession {
425
426
 
426
427
  private buildArgs(): string[] {
427
428
  const args = ["mcp"];
428
- if (this.config.backend === "http") {
429
- args.push("--api-url", this.config.apiUrl!);
430
- args.push("--token", this.config.apiKey!);
431
- args.push("--user", this.userId);
432
- return args;
433
- }
434
-
429
+ // MemoriaMcpSession is only used for embedded mode now.
430
+ // API mode uses MemoriaHttpTransport directly.
435
431
  args.push("--db-url", this.config.dbUrl);
436
432
  args.push("--user", this.userId);
437
433
  return args;
@@ -543,7 +539,7 @@ class MemoriaMcpSession {
543
539
  }
544
540
 
545
541
  export class MemoriaClient {
546
- private readonly sessions = new Map<string, MemoriaMcpSession>();
542
+ private readonly sessions = new Map<string, MemoriaMcpSession | MemoriaHttpTransport>();
547
543
  private readonly memoryCache = new Map<string, MemoriaMemoryRecord>();
548
544
 
549
545
  constructor(private readonly config: MemoriaPluginConfig) {}
@@ -556,6 +552,17 @@ export class MemoriaClient {
556
552
  }
557
553
 
558
554
  async health(userId: string) {
555
+ if (this.config.backend === "api") {
556
+ const transport = this.getSession(userId) as MemoriaHttpTransport;
557
+ const result = await transport.healthCheck();
558
+ return {
559
+ status: result.status,
560
+ mode: this.config.backend,
561
+ instance_id: result.instance_id,
562
+ db: result.db,
563
+ warnings: [],
564
+ };
565
+ }
559
566
  await this.callToolText(userId, "memory_list", { limit: 1 });
560
567
  return {
561
568
  status: "ok",
@@ -865,6 +872,9 @@ export class MemoriaClient {
865
872
  }
866
873
 
867
874
  async rebuildIndex(table: string) {
875
+ if (this.config.backend === "api") {
876
+ return { message: "rebuild_index is managed by the cloud service and not available via API." };
877
+ }
868
878
  return parseGenericResult(
869
879
  await this.callToolText(this.config.defaultUserId, "memory_rebuild_index", {
870
880
  table,
@@ -1016,14 +1026,20 @@ export class MemoriaClient {
1016
1026
  throw new Error(`Memoria tool '${name}' returned no text content.`);
1017
1027
  }
1018
1028
 
1019
- private getSession(userId: string): MemoriaMcpSession {
1029
+ private getSession(userId: string): MemoriaMcpSession | MemoriaHttpTransport {
1020
1030
  const key = `${this.config.backend}:${userId}`;
1021
1031
  const existing = this.sessions.get(key);
1022
1032
  if (existing?.isAlive()) {
1023
1033
  return existing;
1024
1034
  }
1025
1035
  existing?.close();
1026
- const created = new MemoriaMcpSession(this.config, userId);
1036
+
1037
+ let created: MemoriaMcpSession | MemoriaHttpTransport;
1038
+ if (this.config.backend === "api") {
1039
+ created = new MemoriaHttpTransport(this.config, userId);
1040
+ } else {
1041
+ created = new MemoriaMcpSession(this.config, userId);
1042
+ }
1027
1043
  this.sessions.set(key, created);
1028
1044
  return created;
1029
1045
  }
@@ -9,7 +9,7 @@ export const MEMORIA_MEMORY_TYPES = [
9
9
  ] as const;
10
10
 
11
11
  export const MEMORIA_TRUST_TIERS = ["T1", "T2", "T3", "T4"] as const;
12
- export const MEMORIA_BACKENDS = ["embedded", "http"] as const;
12
+ export const MEMORIA_BACKENDS = ["embedded", "api"] as const;
13
13
  export const MEMORIA_USER_ID_STRATEGIES = ["config", "agentId", "sessionKey"] as const;
14
14
 
15
15
  export type MemoriaMemoryType = (typeof MEMORIA_MEMORY_TYPES)[number];
@@ -84,7 +84,7 @@ const UI_HINTS: Record<
84
84
  > = {
85
85
  backend: {
86
86
  label: "Backend Mode",
87
- help: "embedded runs the Rust memoria CLI locally against MatrixOne; http connects to an existing Memoria API.",
87
+ help: "embedded runs the Rust memoria CLI locally against MatrixOne; api connects directly to the Memoria REST API over HTTP.",
88
88
  placeholder: DEFAULTS.backend,
89
89
  },
90
90
  dbUrl: {
@@ -94,14 +94,14 @@ const UI_HINTS: Record<
94
94
  },
95
95
  apiUrl: {
96
96
  label: "Memoria API URL",
97
- help: "Only used when backend=http.",
97
+ help: "Only used when backend=api.",
98
98
  placeholder: DEFAULTS.apiUrl,
99
99
  },
100
100
  apiKey: {
101
101
  label: "Memoria API Token",
102
- help: "Bearer token for backend=http.",
102
+ help: "Bearer token for backend=api.",
103
103
  sensitive: true,
104
- placeholder: "mem-...",
104
+ placeholder: "sk-...",
105
105
  },
106
106
  memoriaExecutable: {
107
107
  label: "Memoria Executable",
@@ -511,18 +511,26 @@ export function parseMemoriaPluginConfig(value: unknown): MemoriaPluginConfig {
511
511
  const input = asObject(value ?? {});
512
512
  assertNoUnknownKeys(input);
513
513
 
514
+ // Reject legacy "http" backend — it was removed in favor of "api"
515
+ if (typeof input.backend === "string" && input.backend.trim().toLowerCase() === "http") {
516
+ fail(
517
+ "backend 'http' is no longer supported. Use 'api' for cloud mode (direct HTTP to Memoria REST API, no binary needed).",
518
+ ["backend"],
519
+ );
520
+ }
521
+
514
522
  const backend = readEnum(input, "backend", MEMORIA_BACKENDS, DEFAULTS.backend);
515
523
  const apiUrl = optional(readString(input, "apiUrl", { defaultValue: DEFAULTS.apiUrl }))?.replace(
516
524
  /\/+$/,
517
525
  "",
518
526
  );
519
527
  const apiKey = optional(readString(input, "apiKey"));
520
- if (backend === "http") {
528
+ if (backend === "api") {
521
529
  if (!apiUrl) {
522
- fail("apiUrl required when backend=http", ["apiUrl"]);
530
+ fail("apiUrl required when backend=api", ["apiUrl"]);
523
531
  }
524
532
  if (!apiKey) {
525
- fail("apiKey required when backend=http", ["apiKey"]);
533
+ fail("apiKey required when backend=api", ["apiKey"]);
526
534
  }
527
535
  }
528
536