coding-friend-cli 1.16.0 → 1.17.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.
Files changed (75) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
  3. package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
  4. package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
  5. package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
  6. package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
  7. package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
  8. package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
  9. package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
  10. package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
  11. package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
  12. package/dist/index.js +78 -18
  13. package/dist/{init-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
  14. package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
  15. package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
  16. package/dist/memory-7RM67ZLS.js +668 -0
  17. package/dist/postinstall.js +1 -1
  18. package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
  19. package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
  20. package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
  21. package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
  22. package/lib/cf-memory/CHANGELOG.md +15 -0
  23. package/lib/cf-memory/README.md +284 -0
  24. package/lib/cf-memory/package-lock.json +2790 -0
  25. package/lib/cf-memory/package.json +31 -0
  26. package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
  27. package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
  28. package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
  29. package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
  30. package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
  31. package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
  32. package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
  33. package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
  34. package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
  35. package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
  36. package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
  37. package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
  38. package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
  39. package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
  40. package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
  41. package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
  42. package/lib/cf-memory/src/backends/markdown.ts +318 -0
  43. package/lib/cf-memory/src/backends/minisearch.ts +203 -0
  44. package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
  45. package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
  46. package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
  47. package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
  48. package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
  49. package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
  50. package/lib/cf-memory/src/daemon/entry.ts +99 -0
  51. package/lib/cf-memory/src/daemon/process.ts +220 -0
  52. package/lib/cf-memory/src/daemon/server.ts +166 -0
  53. package/lib/cf-memory/src/daemon/watcher.ts +90 -0
  54. package/lib/cf-memory/src/index.ts +45 -0
  55. package/lib/cf-memory/src/lib/backend.ts +23 -0
  56. package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
  57. package/lib/cf-memory/src/lib/dedup.ts +80 -0
  58. package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
  59. package/lib/cf-memory/src/lib/ollama.ts +76 -0
  60. package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
  61. package/lib/cf-memory/src/lib/tier.ts +107 -0
  62. package/lib/cf-memory/src/lib/types.ts +109 -0
  63. package/lib/cf-memory/src/resources/index.ts +62 -0
  64. package/lib/cf-memory/src/server.ts +20 -0
  65. package/lib/cf-memory/src/tools/delete.ts +38 -0
  66. package/lib/cf-memory/src/tools/list.ts +38 -0
  67. package/lib/cf-memory/src/tools/retrieve.ts +52 -0
  68. package/lib/cf-memory/src/tools/search.ts +47 -0
  69. package/lib/cf-memory/src/tools/store.ts +70 -0
  70. package/lib/cf-memory/src/tools/update.ts +62 -0
  71. package/lib/cf-memory/tsconfig.json +15 -0
  72. package/lib/cf-memory/vitest.config.ts +7 -0
  73. package/lib/learn-host/CHANGELOG.md +4 -0
  74. package/lib/learn-host/package.json +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import net from "node:net";
4
+ import http from "node:http";
5
+ import { getRequestListener } from "@hono/node-server";
6
+ import { createDaemonApp } from "./server.js";
7
+ import type { MemoryBackend } from "../lib/backend.js";
8
+
9
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
10
+
11
+ export interface DaemonPaths {
12
+ socketPath: string;
13
+ pidFile: string;
14
+ logFile: string;
15
+ }
16
+
17
+ export function getDaemonPaths(): DaemonPaths {
18
+ const baseDir = path.join(
19
+ process.env.HOME ?? process.env.USERPROFILE ?? "/tmp",
20
+ ".coding-friend",
21
+ "memory",
22
+ );
23
+ fs.mkdirSync(baseDir, { recursive: true });
24
+ return {
25
+ socketPath: path.join(baseDir, "daemon.sock"),
26
+ pidFile: path.join(baseDir, "daemon.pid"),
27
+ logFile: path.join(baseDir, "daemon.log"),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Check if the daemon is running by trying to connect to the socket.
33
+ */
34
+ export async function isDaemonRunning(paths?: DaemonPaths): Promise<boolean> {
35
+ const { socketPath, pidFile } = paths ?? getDaemonPaths();
36
+
37
+ // Check PID file first
38
+ if (!fs.existsSync(pidFile)) return false;
39
+
40
+ const pid = parseInt(
41
+ fs.readFileSync(pidFile, "utf-8").trim().split("\n")[0],
42
+ 10,
43
+ );
44
+ if (isNaN(pid)) return false;
45
+
46
+ // Check if process is alive
47
+ try {
48
+ process.kill(pid, 0);
49
+ } catch {
50
+ // Process not running, clean up stale files
51
+ cleanupDaemonFiles(paths);
52
+ return false;
53
+ }
54
+
55
+ // Try to connect to socket
56
+ return new Promise((resolve) => {
57
+ const client = net.createConnection({ path: socketPath }, () => {
58
+ client.end();
59
+ resolve(true);
60
+ });
61
+ client.on("error", () => {
62
+ resolve(false);
63
+ });
64
+ client.setTimeout(1000, () => {
65
+ client.destroy();
66
+ resolve(false);
67
+ });
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Get daemon info from PID file.
73
+ */
74
+ export function getDaemonInfo(
75
+ paths?: DaemonPaths,
76
+ ): { pid: number; startedAt: number } | null {
77
+ const { pidFile } = paths ?? getDaemonPaths();
78
+ if (!fs.existsSync(pidFile)) return null;
79
+
80
+ const content = fs.readFileSync(pidFile, "utf-8").trim();
81
+ const lines = content.split("\n");
82
+ const pid = parseInt(lines[0], 10);
83
+ if (isNaN(pid)) return null;
84
+
85
+ const startedAt = lines[1] ? parseInt(lines[1], 10) : Date.now();
86
+ return { pid, startedAt };
87
+ }
88
+
89
+ function cleanupDaemonFiles(paths?: DaemonPaths): void {
90
+ const { socketPath, pidFile } = paths ?? getDaemonPaths();
91
+ try {
92
+ fs.unlinkSync(socketPath);
93
+ } catch {
94
+ // Ignore
95
+ }
96
+ try {
97
+ fs.unlinkSync(pidFile);
98
+ } catch {
99
+ // Ignore
100
+ }
101
+ }
102
+
103
+ export interface DaemonHandle {
104
+ close: () => void;
105
+ server: http.Server;
106
+ }
107
+
108
+ /**
109
+ * Start the daemon HTTP server on a Unix Domain Socket.
110
+ *
111
+ * Signal handling is NOT registered here — the caller owns that.
112
+ * Use the returned `close()` to trigger graceful shutdown.
113
+ */
114
+ export function startDaemonServer(
115
+ backend: MemoryBackend,
116
+ opts?: {
117
+ idleTimeoutMs?: number;
118
+ paths?: DaemonPaths;
119
+ onShutdown?: () => void;
120
+ },
121
+ ): DaemonHandle {
122
+ const paths = opts?.paths ?? getDaemonPaths();
123
+ const idleTimeoutMs = opts?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
124
+ const { socketPath, pidFile } = paths;
125
+
126
+ // Clean up stale socket (catch ENOENT instead of TOCTOU check-then-act)
127
+ try {
128
+ fs.unlinkSync(socketPath);
129
+ } catch {
130
+ // No stale socket — fine
131
+ }
132
+
133
+ const app = createDaemonApp(backend);
134
+ const listener = getRequestListener(app.fetch);
135
+
136
+ const server = http.createServer(listener);
137
+
138
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
139
+ let shuttingDown = false;
140
+
141
+ function resetIdleTimer() {
142
+ if (idleTimer) clearTimeout(idleTimer);
143
+ if (idleTimeoutMs > 0) {
144
+ idleTimer = setTimeout(() => {
145
+ shutdown();
146
+ }, idleTimeoutMs);
147
+ }
148
+ }
149
+
150
+ // Reset idle timer on each request
151
+ server.on("request", () => {
152
+ resetIdleTimer();
153
+ });
154
+
155
+ server.listen(socketPath, () => {
156
+ // Write PID file
157
+ fs.writeFileSync(pidFile, `${process.pid}\n${Date.now()}`, "utf-8");
158
+ resetIdleTimer();
159
+ });
160
+
161
+ function shutdown() {
162
+ if (shuttingDown) return;
163
+ shuttingDown = true;
164
+
165
+ if (idleTimer) clearTimeout(idleTimer);
166
+ opts?.onShutdown?.();
167
+ backend.close().catch(() => {});
168
+ server.close(() => {
169
+ cleanupDaemonFiles(paths);
170
+ process.exit(0);
171
+ });
172
+ // Force exit after 5 seconds
173
+ setTimeout(() => process.exit(1), 5000).unref();
174
+ }
175
+
176
+ return { close: shutdown, server };
177
+ }
178
+
179
+ /**
180
+ * Stop the daemon by sending SIGTERM.
181
+ */
182
+ export async function stopDaemon(paths?: DaemonPaths): Promise<boolean> {
183
+ const { pidFile } = paths ?? getDaemonPaths();
184
+ if (!fs.existsSync(pidFile)) return false;
185
+
186
+ const pid = parseInt(
187
+ fs.readFileSync(pidFile, "utf-8").trim().split("\n")[0],
188
+ 10,
189
+ );
190
+ if (isNaN(pid)) return false;
191
+
192
+ try {
193
+ process.kill(pid, "SIGTERM");
194
+ } catch {
195
+ // Already dead
196
+ cleanupDaemonFiles(paths);
197
+ return false;
198
+ }
199
+
200
+ // Wait for process to exit (max 5 seconds)
201
+ for (let i = 0; i < 50; i++) {
202
+ await new Promise((r) => setTimeout(r, 100));
203
+ try {
204
+ process.kill(pid, 0);
205
+ } catch {
206
+ // Process exited
207
+ cleanupDaemonFiles(paths);
208
+ return true;
209
+ }
210
+ }
211
+
212
+ // Force kill
213
+ try {
214
+ process.kill(pid, "SIGKILL");
215
+ } catch {
216
+ // Already dead
217
+ }
218
+ cleanupDaemonFiles(paths);
219
+ return true;
220
+ }
@@ -0,0 +1,166 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import type { MemoryBackend } from "../lib/backend.js";
4
+ import {
5
+ MEMORY_TYPES,
6
+ type MemoryType,
7
+ type SearchInput,
8
+ } from "../lib/types.js";
9
+
10
+ const memoryTypeSchema = z.enum(MEMORY_TYPES);
11
+
12
+ const storeSchema = z.object({
13
+ title: z.string().min(1),
14
+ description: z.string(),
15
+ type: memoryTypeSchema,
16
+ tags: z.array(z.string()),
17
+ content: z.string(),
18
+ importance: z.number().min(1).max(5).optional(),
19
+ source: z.string().optional(),
20
+ });
21
+
22
+ const updateSchema = z.object({
23
+ title: z.string().min(1).optional(),
24
+ description: z.string().optional(),
25
+ tags: z.array(z.string()).optional(),
26
+ content: z.string().optional(),
27
+ importance: z.number().min(1).max(5).optional(),
28
+ });
29
+
30
+ function parseType(raw: string | undefined): MemoryType | undefined {
31
+ if (!raw) return undefined;
32
+ const result = memoryTypeSchema.safeParse(raw);
33
+ return result.success ? result.data : undefined;
34
+ }
35
+
36
+ export function createDaemonApp(backend: MemoryBackend): Hono {
37
+ const app = new Hono();
38
+
39
+ // Global error handler
40
+ app.onError((err, c) => {
41
+ return c.json(
42
+ { error: err instanceof Error ? err.message : "Internal server error" },
43
+ 500,
44
+ );
45
+ });
46
+
47
+ // Health check
48
+ app.get("/health", (c) => {
49
+ return c.json({
50
+ status: "ok",
51
+ uptime: process.uptime(),
52
+ pid: process.pid,
53
+ });
54
+ });
55
+
56
+ // Stats
57
+ app.get("/stats", async (c) => {
58
+ const stats = await backend.stats();
59
+ return c.json(stats);
60
+ });
61
+
62
+ // Store a new memory
63
+ app.post("/memory", async (c) => {
64
+ const raw = await c.req.json();
65
+ const parsed = storeSchema.safeParse(raw);
66
+ if (!parsed.success) {
67
+ return c.json(
68
+ { error: "Validation failed", details: parsed.error.issues },
69
+ 400,
70
+ );
71
+ }
72
+ const memory = await backend.store(parsed.data);
73
+ return c.json(
74
+ { id: memory.id, title: memory.frontmatter.title, stored: true },
75
+ 201,
76
+ );
77
+ });
78
+
79
+ // Search memories
80
+ app.get("/memory/search", async (c) => {
81
+ const query = c.req.query("query") ?? "";
82
+ const type = parseType(c.req.query("type"));
83
+ const tagsRaw = c.req.query("tags");
84
+ const limitRaw = c.req.query("limit");
85
+
86
+ const input: SearchInput = {
87
+ query,
88
+ type,
89
+ tags: tagsRaw ? tagsRaw.split(",") : undefined,
90
+ limit: limitRaw ? parseInt(limitRaw, 10) : undefined,
91
+ };
92
+
93
+ const results = await backend.search(input);
94
+ return c.json(results);
95
+ });
96
+
97
+ // Retrieve a memory by ID (category/slug)
98
+ app.get("/memory/:category/:slug", async (c) => {
99
+ const id = `${c.req.param("category")}/${c.req.param("slug")}`;
100
+ const memory = await backend.retrieve(id);
101
+ if (!memory) {
102
+ return c.json({ error: "Not found" }, 404);
103
+ }
104
+ return c.json(memory);
105
+ });
106
+
107
+ // List memories
108
+ app.get("/memory", async (c) => {
109
+ const type = parseType(c.req.query("type"));
110
+ const category = c.req.query("category");
111
+ const limitRaw = c.req.query("limit");
112
+
113
+ const metas = await backend.list({
114
+ type,
115
+ category: category || undefined,
116
+ limit: limitRaw ? parseInt(limitRaw, 10) : undefined,
117
+ });
118
+ return c.json(metas);
119
+ });
120
+
121
+ // Update a memory
122
+ app.patch("/memory/:category/:slug", async (c) => {
123
+ const id = `${c.req.param("category")}/${c.req.param("slug")}`;
124
+ const raw = await c.req.json();
125
+ const parsed = updateSchema.safeParse(raw);
126
+ if (!parsed.success) {
127
+ return c.json(
128
+ { error: "Validation failed", details: parsed.error.issues },
129
+ 400,
130
+ );
131
+ }
132
+ const memory = await backend.update({ id, ...parsed.data });
133
+ if (!memory) {
134
+ return c.json({ error: "Not found" }, 404);
135
+ }
136
+ return c.json({
137
+ id: memory.id,
138
+ title: memory.frontmatter.title,
139
+ updated: true,
140
+ });
141
+ });
142
+
143
+ // Delete a memory
144
+ app.delete("/memory/:category/:slug", async (c) => {
145
+ const id = `${c.req.param("category")}/${c.req.param("slug")}`;
146
+ const deleted = await backend.delete(id);
147
+ if (!deleted) {
148
+ return c.json({ error: "Not found" }, 404);
149
+ }
150
+ return c.json({ id, deleted: true });
151
+ });
152
+
153
+ // Rebuild index (for MiniSearchBackend)
154
+ app.post("/rebuild", async (c) => {
155
+ if ("rebuild" in backend && typeof backend.rebuild === "function") {
156
+ await (backend as MemoryBackend & { rebuild(): Promise<void> }).rebuild();
157
+ return c.json({ rebuilt: true });
158
+ }
159
+ return c.json({
160
+ rebuilt: false,
161
+ reason: "Backend does not support rebuild",
162
+ });
163
+ });
164
+
165
+ return app;
166
+ }
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { MemoryBackend } from "../lib/backend.js";
4
+
5
+ export interface WatcherHandle {
6
+ close: () => void;
7
+ }
8
+
9
+ /**
10
+ * Watch docs/memory/ for changes and trigger index rebuild (debounced).
11
+ */
12
+ export function setupWatcher(
13
+ docsDir: string,
14
+ backend: Required<Pick<MemoryBackend, "rebuild">>,
15
+ debounceMs = 500,
16
+ ): WatcherHandle {
17
+ let timer: ReturnType<typeof setTimeout> | null = null;
18
+ const watchers: fs.FSWatcher[] = [];
19
+ const watchedDirs = new Set<string>();
20
+
21
+ function scheduleRebuild() {
22
+ if (timer) clearTimeout(timer);
23
+ timer = setTimeout(() => {
24
+ backend.rebuild().catch(() => {});
25
+ }, debounceMs);
26
+ }
27
+
28
+ function watchSubdir(dirPath: string) {
29
+ if (watchedDirs.has(dirPath)) return;
30
+ watchedDirs.add(dirPath);
31
+ try {
32
+ const w = fs.watch(dirPath, { persistent: false }, (_, filename) => {
33
+ if (filename && filename.endsWith(".md")) {
34
+ scheduleRebuild();
35
+ }
36
+ });
37
+ watchers.push(w);
38
+ } catch {
39
+ watchedDirs.delete(dirPath);
40
+ }
41
+ }
42
+
43
+ // Watch the root docs dir
44
+ if (fs.existsSync(docsDir)) {
45
+ try {
46
+ const rootWatcher = fs.watch(
47
+ docsDir,
48
+ { persistent: false },
49
+ (_, filename) => {
50
+ if (filename && !filename.startsWith(".")) {
51
+ scheduleRebuild();
52
+ // If a new directory appeared, watch it too
53
+ const subPath = path.join(docsDir, filename);
54
+ try {
55
+ if (fs.statSync(subPath).isDirectory()) {
56
+ watchSubdir(subPath);
57
+ }
58
+ } catch {
59
+ // File may have been deleted between event and stat
60
+ }
61
+ }
62
+ },
63
+ );
64
+ watchers.push(rootWatcher);
65
+ } catch {
66
+ // Ignore watch errors (e.g., directory doesn't exist)
67
+ }
68
+
69
+ // Watch existing subdirectories
70
+ try {
71
+ const entries = fs.readdirSync(docsDir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
74
+ watchSubdir(path.join(docsDir, entry.name));
75
+ }
76
+ }
77
+ } catch {
78
+ // Ignore
79
+ }
80
+ }
81
+
82
+ return {
83
+ close() {
84
+ if (timer) clearTimeout(timer);
85
+ for (const w of watchers) {
86
+ w.close();
87
+ }
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerAllTools } from "./server.js";
5
+ import { registerAllResources } from "./resources/index.js";
6
+ import { createBackendForTier } from "./lib/tier.js";
7
+ import type { TierConfig } from "./lib/tier.js";
8
+ import type { EmbeddingConfig } from "./backends/sqlite/embeddings.js";
9
+
10
+ const rawDir =
11
+ process.argv[2] ?? process.env.MEMORY_DOCS_DIR ?? "./docs/memory";
12
+ const docsDir = path.resolve(rawDir);
13
+ const tierConfig = (process.env.MEMORY_TIER ?? "auto") as TierConfig;
14
+
15
+ // Embedding config from environment variables
16
+ const embeddingConfig: Partial<EmbeddingConfig> | undefined = (() => {
17
+ const provider = process.env.MEMORY_EMBEDDING_PROVIDER as
18
+ | EmbeddingConfig["provider"]
19
+ | undefined;
20
+ const model = process.env.MEMORY_EMBEDDING_MODEL;
21
+ const ollamaUrl = process.env.MEMORY_EMBEDDING_OLLAMA_URL;
22
+ if (!provider && !model && !ollamaUrl) return undefined;
23
+ return {
24
+ ...(provider && { provider }),
25
+ ...(model && { model }),
26
+ ...(ollamaUrl && { ollamaUrl }),
27
+ };
28
+ })();
29
+
30
+ const { backend, tier } = await createBackendForTier(
31
+ docsDir,
32
+ tierConfig,
33
+ embeddingConfig,
34
+ );
35
+
36
+ const server = new McpServer({
37
+ name: "coding-friend-memory",
38
+ version: "0.0.1",
39
+ });
40
+
41
+ registerAllTools(server, backend);
42
+ registerAllResources(server, backend);
43
+
44
+ const transport = new StdioServerTransport();
45
+ await server.connect(transport);
@@ -0,0 +1,23 @@
1
+ import type {
2
+ ListInput,
3
+ Memory,
4
+ MemoryMeta,
5
+ MemoryStats,
6
+ SearchInput,
7
+ SearchResult,
8
+ StoreInput,
9
+ UpdateInput,
10
+ } from "./types.js";
11
+
12
+ export interface MemoryBackend {
13
+ store(input: StoreInput): Promise<Memory>;
14
+ search(input: SearchInput): Promise<SearchResult[]>;
15
+ retrieve(id: string): Promise<Memory | null>;
16
+ list(input: ListInput): Promise<MemoryMeta[]>;
17
+ update(input: UpdateInput): Promise<Memory | null>;
18
+ delete(id: string): Promise<boolean>;
19
+ stats(): Promise<MemoryStats>;
20
+ close(): Promise<void>;
21
+ /** Rebuild the search index from source files. Optional — not all backends support it. */
22
+ rebuild?(): Promise<void>;
23
+ }
@@ -0,0 +1,163 @@
1
+ import http from "node:http";
2
+ import type { MemoryBackend } from "./backend.js";
3
+ import type {
4
+ ListInput,
5
+ Memory,
6
+ MemoryMeta,
7
+ MemoryStats,
8
+ SearchInput,
9
+ SearchResult,
10
+ StoreInput,
11
+ UpdateInput,
12
+ } from "./types.js";
13
+
14
+ /**
15
+ * MemoryBackend implementation that talks to the daemon over UDS.
16
+ */
17
+ export class DaemonClient implements MemoryBackend {
18
+ constructor(private socketPath: string) {}
19
+
20
+ private request<T>(method: string, path: string, body?: unknown): Promise<T> {
21
+ return new Promise((resolve, reject) => {
22
+ const options: http.RequestOptions = {
23
+ socketPath: this.socketPath,
24
+ path,
25
+ method,
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ },
29
+ };
30
+
31
+ const req = http.request(options, (res) => {
32
+ let data = "";
33
+ res.on("data", (chunk: Buffer) => {
34
+ data += chunk.toString();
35
+ });
36
+ res.on("end", () => {
37
+ try {
38
+ const parsed = JSON.parse(data) as T;
39
+ if (res.statusCode && res.statusCode >= 400) {
40
+ reject(
41
+ new Error(
42
+ (parsed as Record<string, string>).error ??
43
+ `HTTP ${res.statusCode}`,
44
+ ),
45
+ );
46
+ } else {
47
+ resolve(parsed);
48
+ }
49
+ } catch {
50
+ reject(new Error(`Invalid JSON response: ${data}`));
51
+ }
52
+ });
53
+ });
54
+
55
+ req.on("error", (err) => {
56
+ reject(err);
57
+ });
58
+
59
+ req.setTimeout(10000, () => {
60
+ req.destroy();
61
+ reject(new Error("Request timeout"));
62
+ });
63
+
64
+ if (body) {
65
+ req.write(JSON.stringify(body));
66
+ }
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ async store(input: StoreInput): Promise<Memory> {
72
+ const result = await this.request<{
73
+ id: string;
74
+ title: string;
75
+ stored: boolean;
76
+ }>("POST", "/memory", input);
77
+ // Retrieve full memory after store
78
+ const memory = await this.retrieve(result.id);
79
+ if (!memory) throw new Error("Store succeeded but retrieve failed");
80
+ return memory;
81
+ }
82
+
83
+ async search(input: SearchInput): Promise<SearchResult[]> {
84
+ const params = new URLSearchParams();
85
+ params.set("query", input.query);
86
+ if (input.type) params.set("type", input.type);
87
+ if (input.tags) params.set("tags", input.tags.join(","));
88
+ if (input.limit) params.set("limit", String(input.limit));
89
+
90
+ return this.request<SearchResult[]>(
91
+ "GET",
92
+ `/memory/search?${params.toString()}`,
93
+ );
94
+ }
95
+
96
+ async retrieve(id: string): Promise<Memory | null> {
97
+ try {
98
+ return await this.request<Memory>("GET", `/memory/${id}`);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ async list(input: ListInput): Promise<MemoryMeta[]> {
105
+ const params = new URLSearchParams();
106
+ if (input.type) params.set("type", input.type);
107
+ if (input.category) params.set("category", input.category);
108
+ if (input.limit) params.set("limit", String(input.limit));
109
+
110
+ const qs = params.toString();
111
+ return this.request<MemoryMeta[]>("GET", qs ? `/memory?${qs}` : "/memory");
112
+ }
113
+
114
+ async update(input: UpdateInput): Promise<Memory | null> {
115
+ const { id, ...body } = input;
116
+ try {
117
+ await this.request("PATCH", `/memory/${id}`, body);
118
+ return this.retrieve(id);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ async delete(id: string): Promise<boolean> {
125
+ try {
126
+ await this.request("DELETE", `/memory/${id}`);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ async stats(): Promise<MemoryStats> {
134
+ return this.request<MemoryStats>("GET", "/stats");
135
+ }
136
+
137
+ async close(): Promise<void> {
138
+ // No-op — daemon keeps running
139
+ }
140
+
141
+ /**
142
+ * Check if the daemon is reachable.
143
+ */
144
+ async ping(): Promise<boolean> {
145
+ try {
146
+ const result = await this.request<{ status: string }>("GET", "/health");
147
+ return result.status === "ok";
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Trigger index rebuild on the daemon.
155
+ */
156
+ async rebuild(): Promise<void> {
157
+ try {
158
+ await this.request<{ rebuilt: boolean }>("POST", "/rebuild");
159
+ } catch {
160
+ // Rebuild failed or not supported
161
+ }
162
+ }
163
+ }