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,80 @@
1
+ import type { MemoryBackend } from "./backend.js";
2
+ import type { StoreInput } from "./types.js";
3
+
4
+ export interface DedupResult {
5
+ isDuplicate: boolean;
6
+ similarId?: string;
7
+ similarity: number;
8
+ }
9
+
10
+ interface DedupOptions {
11
+ threshold?: number;
12
+ }
13
+
14
+ export function textSimilarity(a: string, b: string): number {
15
+ const tokenize = (s: string): Set<string> =>
16
+ new Set(
17
+ s
18
+ .toLowerCase()
19
+ .split(/\s+/)
20
+ .filter((t) => t.length > 0),
21
+ );
22
+
23
+ const setA = tokenize(a);
24
+ const setB = tokenize(b);
25
+
26
+ if (setA.size === 0 && setB.size === 0) return 1;
27
+ if (setA.size === 0 || setB.size === 0) return 0;
28
+
29
+ let intersection = 0;
30
+ for (const token of setA) {
31
+ if (setB.has(token)) intersection++;
32
+ }
33
+
34
+ const union = new Set([...setA, ...setB]).size;
35
+ return intersection / union;
36
+ }
37
+
38
+ export async function checkDuplicate(
39
+ backend: MemoryBackend,
40
+ input: StoreInput,
41
+ options?: DedupOptions,
42
+ ): Promise<DedupResult> {
43
+ const searchScoreThreshold = options?.threshold ?? 0.8;
44
+ const titleThreshold = 0.85;
45
+
46
+ // Search by title only — more reliable across all backend tiers
47
+ // (MarkdownBackend uses substring matching, composite queries miss)
48
+ const results = await backend.search({ query: input.title, limit: 5 });
49
+
50
+ let maxSimilarity = 0;
51
+ const inputText = input.title + " " + input.description;
52
+
53
+ for (const result of results) {
54
+ const resultText =
55
+ result.memory.frontmatter.title +
56
+ " " +
57
+ result.memory.frontmatter.description;
58
+
59
+ const titleSim = textSimilarity(
60
+ input.title,
61
+ result.memory.frontmatter.title,
62
+ );
63
+ const overallSim = textSimilarity(inputText, resultText);
64
+
65
+ const combinedSim = Math.max(titleSim, overallSim);
66
+ if (combinedSim > maxSimilarity) {
67
+ maxSimilarity = combinedSim;
68
+ }
69
+
70
+ if (result.score > searchScoreThreshold && titleSim > titleThreshold) {
71
+ return {
72
+ isDuplicate: true,
73
+ similarId: result.memory.id,
74
+ similarity: combinedSim,
75
+ };
76
+ }
77
+ }
78
+
79
+ return { isDuplicate: false, similarity: maxSimilarity };
80
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Lazy dependency installer for Phase 3 (SQLite + Hybrid Search).
3
+ *
4
+ * Heavy deps (better-sqlite3, sqlite-vec, @huggingface/transformers) are installed
5
+ * into ~/.coding-friend/memory/node_modules/ — NOT bundled with the CLI package.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { execFileSync } from "node:child_process";
10
+ import { createRequire } from "node:module";
11
+
12
+ /** Dependencies required for Tier 1 (SQLite + Hybrid Search) */
13
+ export const SQLITE_DEPS: Record<string, string> = {
14
+ "better-sqlite3": "^11.0.0",
15
+ "sqlite-vec": "^0.1.6",
16
+ };
17
+
18
+ export const EMBEDDING_DEPS: Record<string, string> = {
19
+ "@huggingface/transformers": "^3.4.0",
20
+ };
21
+
22
+ export const ALL_DEPS = { ...SQLITE_DEPS, ...EMBEDDING_DEPS };
23
+
24
+ export interface DepsManifest {
25
+ version: number;
26
+ installed: Record<string, string>;
27
+ installedAt: string;
28
+ }
29
+
30
+ const MANIFEST_VERSION = 1;
31
+
32
+ /**
33
+ * Get the base directory for lazy-installed deps.
34
+ */
35
+ export function getDepsDir(): string {
36
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
37
+ return path.join(home, ".coding-friend", "memory");
38
+ }
39
+
40
+ /**
41
+ * Get the path to the deps manifest file.
42
+ */
43
+ export function getManifestPath(depsDir?: string): string {
44
+ return path.join(depsDir ?? getDepsDir(), "deps.json");
45
+ }
46
+
47
+ /**
48
+ * Read the current deps manifest.
49
+ */
50
+ export function readManifest(depsDir?: string): DepsManifest | null {
51
+ const manifestPath = getManifestPath(depsDir);
52
+ if (!fs.existsSync(manifestPath)) return null;
53
+ try {
54
+ return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Write the deps manifest.
62
+ */
63
+ function writeManifest(manifest: DepsManifest, depsDir?: string): void {
64
+ const dir = depsDir ?? getDepsDir();
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ fs.writeFileSync(getManifestPath(dir), JSON.stringify(manifest, null, 2));
67
+ }
68
+
69
+ /**
70
+ * Check if a specific set of deps are installed and up-to-date.
71
+ */
72
+ export function areDepsInstalled(
73
+ deps: Record<string, string>,
74
+ depsDir?: string,
75
+ ): boolean {
76
+ const dir = depsDir ?? getDepsDir();
77
+ const manifest = readManifest(dir);
78
+ if (!manifest || manifest.version !== MANIFEST_VERSION) return false;
79
+
80
+ for (const [pkg, version] of Object.entries(deps)) {
81
+ const installedVersion = manifest.installed[pkg];
82
+ if (!installedVersion) return false;
83
+
84
+ // Semver caret range check:
85
+ // ^x.y.z (x>0): same major → ^11.0.0 accepts 11.x.x
86
+ // ^0.y.z (y>0): same major+minor → ^0.1.6 accepts 0.1.x
87
+ // ^0.0.z: exact → ^0.0.3 accepts only 0.0.3
88
+ const requiredParts = version.replace(/^\^/, "").split(".");
89
+ const installedParts = installedVersion.split(".");
90
+
91
+ if (requiredParts[0] !== "0") {
92
+ // Major > 0: check major version matches
93
+ if (requiredParts[0] !== installedParts[0]) return false;
94
+ } else if (requiredParts[1] !== "0") {
95
+ // ^0.y.z: check major + minor match
96
+ if (requiredParts[0] !== installedParts[0]) return false;
97
+ if (requiredParts[1] !== installedParts[1]) return false;
98
+ } else {
99
+ // ^0.0.z: exact match required
100
+ if (installedVersion !== version.replace(/^\^/, "")) return false;
101
+ }
102
+
103
+ // Also check that the module actually exists on disk
104
+ const modulePath = path.join(dir, "node_modules", pkg);
105
+ if (!fs.existsSync(modulePath)) return false;
106
+ }
107
+
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Check if SQLite deps are available (quick check for tier detection).
113
+ */
114
+ export function areSqliteDepsAvailable(depsDir?: string): boolean {
115
+ return areDepsInstalled(SQLITE_DEPS, depsDir);
116
+ }
117
+
118
+ /**
119
+ * Check if embedding deps are available.
120
+ */
121
+ export function areEmbeddingDepsAvailable(depsDir?: string): boolean {
122
+ return areDepsInstalled(EMBEDDING_DEPS, depsDir);
123
+ }
124
+
125
+ export interface InstallOptions {
126
+ onProgress?: (message: string) => void;
127
+ depsDir?: string;
128
+ sqliteOnly?: boolean;
129
+ }
130
+
131
+ /**
132
+ * Ensure all required deps are installed. Installs missing ones.
133
+ *
134
+ * Returns true if all deps are available after this call.
135
+ */
136
+ export async function ensureDeps(opts?: InstallOptions): Promise<boolean> {
137
+ const dir = opts?.depsDir ?? getDepsDir();
138
+ const deps = opts?.sqliteOnly ? SQLITE_DEPS : ALL_DEPS;
139
+ const progress = opts?.onProgress ?? (() => {});
140
+
141
+ if (areDepsInstalled(deps, dir)) {
142
+ return true;
143
+ }
144
+
145
+ progress("Checking build tools...");
146
+
147
+ // Verify npm is available
148
+ try {
149
+ execFileSync("npm", ["--version"], { stdio: "ignore" });
150
+ } catch {
151
+ progress("Error: npm not found. Please install Node.js >= 18.");
152
+ return false;
153
+ }
154
+
155
+ // Create directory structure
156
+ fs.mkdirSync(dir, { recursive: true });
157
+
158
+ // Create a minimal package.json if it doesn't exist
159
+ const pkgPath = path.join(dir, "package.json");
160
+ if (!fs.existsSync(pkgPath)) {
161
+ fs.writeFileSync(
162
+ pkgPath,
163
+ JSON.stringify(
164
+ {
165
+ name: "cf-memory-deps",
166
+ version: "1.0.0",
167
+ private: true,
168
+ description: "Lazy-installed dependencies for cf-memory Tier 1",
169
+ },
170
+ null,
171
+ 2,
172
+ ),
173
+ );
174
+ }
175
+
176
+ // Build npm install args: ["install", "--save", "pkg1@ver1", "pkg2@ver2"]
177
+ const installArgs = ["install", "--save"];
178
+ for (const [name, version] of Object.entries(deps)) {
179
+ installArgs.push(`${name}@${version}`);
180
+ }
181
+
182
+ progress(`Installing dependencies: ${Object.keys(deps).join(", ")}...`);
183
+
184
+ try {
185
+ execFileSync("npm", installArgs, {
186
+ cwd: dir,
187
+ stdio: "pipe",
188
+ timeout: 300_000, // 5 minutes
189
+ });
190
+ } catch (err) {
191
+ const message =
192
+ err instanceof Error ? err.message : "Unknown installation error";
193
+ progress(`Installation failed: ${message}`);
194
+ return false;
195
+ }
196
+
197
+ // Read actual installed versions from node_modules
198
+ const installed: Record<string, string> = {};
199
+ for (const pkg of Object.keys(deps)) {
200
+ try {
201
+ const pkgJsonPath = path.join(dir, "node_modules", pkg, "package.json");
202
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
203
+ installed[pkg] = pkgJson.version;
204
+ } catch {
205
+ progress(`Warning: Could not read installed version for ${pkg}`);
206
+ }
207
+ }
208
+
209
+ // Write manifest
210
+ const manifest: DepsManifest = {
211
+ version: MANIFEST_VERSION,
212
+ installed,
213
+ installedAt: new Date().toISOString(),
214
+ };
215
+ writeManifest(manifest, dir);
216
+
217
+ progress("Dependencies installed successfully.");
218
+ return true;
219
+ }
220
+
221
+ /**
222
+ * Synchronously load a lazily-installed CJS module (e.g., better-sqlite3).
223
+ */
224
+ export function loadDepSync<T = unknown>(
225
+ moduleName: string,
226
+ depsDir?: string,
227
+ ): T {
228
+ const dir = depsDir ?? getDepsDir();
229
+ const modulePath = path.join(dir, "node_modules", moduleName);
230
+
231
+ if (!fs.existsSync(modulePath)) {
232
+ throw new Error(
233
+ `Dependency "${moduleName}" not installed. Run "cf memory init" first.`,
234
+ );
235
+ }
236
+
237
+ const require = createRequire(path.join(dir, "package.json"));
238
+ return require(moduleName) as T;
239
+ }
240
+
241
+ /**
242
+ * Dynamically import a lazily-installed ESM module (e.g., @huggingface/transformers).
243
+ */
244
+ export async function loadDepAsync<T = unknown>(
245
+ moduleName: string,
246
+ depsDir?: string,
247
+ ): Promise<T> {
248
+ const dir = depsDir ?? getDepsDir();
249
+ const modulePath = path.join(dir, "node_modules", moduleName);
250
+
251
+ if (!fs.existsSync(modulePath)) {
252
+ throw new Error(
253
+ `Dependency "${moduleName}" not installed. Run "cf memory init" first.`,
254
+ );
255
+ }
256
+
257
+ // Find the main entry point from package.json
258
+ const pkgJsonPath = path.join(modulePath, "package.json");
259
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
260
+
261
+ // Try: exports → module → main
262
+ const entry =
263
+ typeof pkgJson.exports === "string"
264
+ ? pkgJson.exports
265
+ : (pkgJson.exports?.["."]?.import ??
266
+ pkgJson.exports?.["."]?.default ??
267
+ pkgJson.exports?.["."] ??
268
+ pkgJson.module ??
269
+ pkgJson.main ??
270
+ "index.js");
271
+
272
+ const entryPath = path.join(modulePath, entry);
273
+ return (await import(entryPath)) as T;
274
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Ollama integration for embedding generation.
3
+ *
4
+ * Provides auto-detection of Ollama availability and model listing.
5
+ */
6
+
7
+ const DEFAULT_OLLAMA_URL = process.env.OLLAMA_HOST ?? "http://localhost:11434";
8
+
9
+ /**
10
+ * Check if Ollama is running at the given URL.
11
+ */
12
+ export async function isOllamaRunning(
13
+ url: string = DEFAULT_OLLAMA_URL,
14
+ ): Promise<boolean> {
15
+ try {
16
+ const response = await fetch(`${url}/api/tags`, {
17
+ signal: AbortSignal.timeout(2000),
18
+ });
19
+ return response.ok;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * List available Ollama models.
27
+ */
28
+ export async function listOllamaModels(
29
+ url: string = DEFAULT_OLLAMA_URL,
30
+ ): Promise<string[]> {
31
+ try {
32
+ const response = await fetch(`${url}/api/tags`, {
33
+ signal: AbortSignal.timeout(5000),
34
+ });
35
+ if (!response.ok) return [];
36
+
37
+ const data = (await response.json()) as {
38
+ models?: Array<{ name: string }>;
39
+ };
40
+ return data.models?.map((m) => m.name) ?? [];
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Check if a specific embedding model is available in Ollama.
48
+ */
49
+ export async function hasOllamaEmbeddingModel(
50
+ model: string = "all-minilm:l6-v2",
51
+ url: string = DEFAULT_OLLAMA_URL,
52
+ ): Promise<boolean> {
53
+ const models = await listOllamaModels(url);
54
+ return models.some((m) => m.startsWith(model));
55
+ }
56
+
57
+ /**
58
+ * Auto-detect the best embedding provider.
59
+ *
60
+ * Prefers Ollama if available and has an embedding model,
61
+ * otherwise falls back to Transformers.js.
62
+ */
63
+ export async function detectEmbeddingProvider(
64
+ ollamaUrl?: string,
65
+ ): Promise<{ provider: "ollama" | "transformers"; model?: string }> {
66
+ const url = ollamaUrl ?? DEFAULT_OLLAMA_URL;
67
+
68
+ if (await isOllamaRunning(url)) {
69
+ if (await hasOllamaEmbeddingModel("all-minilm:l6-v2", url)) {
70
+ return { provider: "ollama", model: "all-minilm:l6-v2" };
71
+ }
72
+ // Ollama running but no embedding model — use transformers
73
+ }
74
+
75
+ return { provider: "transformers" };
76
+ }
@@ -0,0 +1,19 @@
1
+ export function applyTemporalDecay(
2
+ score: number,
3
+ updatedDate: string,
4
+ accessCount?: number,
5
+ ): number {
6
+ const updated = new Date(updatedDate);
7
+ if (isNaN(updated.getTime())) return score;
8
+
9
+ const now = new Date();
10
+ const ageDays = (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24);
11
+
12
+ score *= 0.7 + 0.3 * Math.exp(-ageDays / 90);
13
+
14
+ if (accessCount !== undefined) {
15
+ score *= 1 + 0.05 * Math.min(accessCount, 10);
16
+ }
17
+
18
+ return score;
19
+ }
@@ -0,0 +1,107 @@
1
+ import type { MemoryBackend } from "./backend.js";
2
+ import { DaemonClient } from "./daemon-client.js";
3
+ import { MarkdownBackend } from "../backends/markdown.js";
4
+ import { getDaemonPaths, isDaemonRunning } from "../daemon/process.js";
5
+ import { areSqliteDepsAvailable } from "./lazy-install.js";
6
+ import type { EmbeddingConfig } from "../backends/sqlite/embeddings.js";
7
+ import type { SqliteBackendOptions } from "../backends/sqlite/index.js";
8
+
9
+ export type TierName = "full" | "lite" | "markdown";
10
+ export type TierConfig = "auto" | TierName;
11
+
12
+ export interface TierInfo {
13
+ name: TierName;
14
+ label: string;
15
+ number: 1 | 2 | 3;
16
+ }
17
+
18
+ export const TIERS: Record<TierName, TierInfo> = {
19
+ full: { name: "full", label: "Tier 1 (SQLite + Hybrid)", number: 1 },
20
+ lite: { name: "lite", label: "Tier 2 (MiniSearch + Daemon)", number: 2 },
21
+ markdown: { name: "markdown", label: "Tier 3 (Markdown)", number: 3 },
22
+ };
23
+
24
+ /**
25
+ * Detect the best available tier.
26
+ *
27
+ * Priority: SQLite (Tier 1) → Daemon running (Tier 2) → Markdown (Tier 3)
28
+ */
29
+ export async function detectTier(configTier?: TierConfig): Promise<TierInfo> {
30
+ // Explicit config override
31
+ if (configTier && configTier !== "auto") {
32
+ return TIERS[configTier];
33
+ }
34
+
35
+ // Check if SQLite deps are available → Tier 1
36
+ if (areSqliteDepsAvailable()) {
37
+ return TIERS.full;
38
+ }
39
+
40
+ // Check if daemon is running → Tier 2
41
+ if (await isDaemonRunning()) {
42
+ return TIERS.lite;
43
+ }
44
+
45
+ // Default: Tier 3
46
+ return TIERS.markdown;
47
+ }
48
+
49
+ /**
50
+ * Create the appropriate backend for the detected tier.
51
+ */
52
+ export async function createBackendForTier(
53
+ docsDir: string,
54
+ configTier?: TierConfig,
55
+ embeddingConfig?: Partial<EmbeddingConfig>,
56
+ sqliteOptions?: Pick<SqliteBackendOptions, "dbPath">,
57
+ ): Promise<{ backend: MemoryBackend; tier: TierInfo }> {
58
+ const tier = await detectTier(configTier);
59
+
60
+ switch (tier.name) {
61
+ case "full": {
62
+ // Try to create SqliteBackend, fall back if it fails
63
+ try {
64
+ const { SqliteBackend } = await import("../backends/sqlite/index.js");
65
+ const backend = new SqliteBackend(docsDir, {
66
+ ...(embeddingConfig ? { embedding: embeddingConfig } : {}),
67
+ ...sqliteOptions,
68
+ });
69
+ return { backend, tier };
70
+ } catch {
71
+ // SQLite backend failed — fall through to daemon or markdown
72
+ if (await isDaemonRunning()) {
73
+ const paths = getDaemonPaths();
74
+ const client = new DaemonClient(paths.socketPath);
75
+ const alive = await client.ping();
76
+ if (alive) {
77
+ return { backend: client, tier: TIERS.lite };
78
+ }
79
+ }
80
+ return {
81
+ backend: new MarkdownBackend(docsDir),
82
+ tier: TIERS.markdown,
83
+ };
84
+ }
85
+ }
86
+ case "lite": {
87
+ // Use daemon client
88
+ const paths = getDaemonPaths();
89
+ const client = new DaemonClient(paths.socketPath);
90
+ const alive = await client.ping();
91
+ if (alive) {
92
+ return { backend: client, tier };
93
+ }
94
+ // Daemon not reachable, fall back to Tier 3
95
+ return {
96
+ backend: new MarkdownBackend(docsDir),
97
+ tier: TIERS.markdown,
98
+ };
99
+ }
100
+ case "markdown":
101
+ default:
102
+ return {
103
+ backend: new MarkdownBackend(docsDir),
104
+ tier: TIERS.markdown,
105
+ };
106
+ }
107
+ }
@@ -0,0 +1,109 @@
1
+ export const MEMORY_TYPES = [
2
+ "fact",
3
+ "preference",
4
+ "context",
5
+ "episode",
6
+ "procedure",
7
+ ] as const;
8
+
9
+ export type MemoryType = (typeof MEMORY_TYPES)[number];
10
+
11
+ export const MEMORY_CATEGORIES: Record<MemoryType, string> = {
12
+ fact: "features",
13
+ preference: "conventions",
14
+ context: "decisions",
15
+ episode: "bugs",
16
+ procedure: "infrastructure",
17
+ };
18
+
19
+ export const CATEGORY_TO_TYPE: Record<string, MemoryType> = {
20
+ features: "fact",
21
+ conventions: "preference",
22
+ decisions: "context",
23
+ bugs: "episode",
24
+ infrastructure: "procedure",
25
+ };
26
+
27
+ export interface MemoryFrontmatter {
28
+ title: string;
29
+ description: string;
30
+ type: MemoryType;
31
+ tags: string[];
32
+ importance: number;
33
+ created: string;
34
+ updated: string;
35
+ source: string;
36
+ }
37
+
38
+ export interface Memory {
39
+ id: string;
40
+ slug: string;
41
+ category: string;
42
+ frontmatter: MemoryFrontmatter;
43
+ content: string;
44
+ }
45
+
46
+ export interface MemoryMeta {
47
+ id: string;
48
+ slug: string;
49
+ category: string;
50
+ frontmatter: MemoryFrontmatter;
51
+ excerpt: string;
52
+ }
53
+
54
+ export interface SearchResult {
55
+ memory: MemoryMeta;
56
+ score: number;
57
+ matchedOn: string[];
58
+ }
59
+
60
+ export interface MemoryStats {
61
+ total: number;
62
+ byCategory: Record<string, number>;
63
+ byType: Record<string, number>;
64
+ }
65
+
66
+ export interface StoreInput {
67
+ title: string;
68
+ description: string;
69
+ type: MemoryType;
70
+ tags: string[];
71
+ content: string;
72
+ importance?: number;
73
+ source?: string;
74
+ }
75
+
76
+ export interface SearchInput {
77
+ query: string;
78
+ type?: MemoryType;
79
+ tags?: string[];
80
+ limit?: number;
81
+ }
82
+
83
+ export interface ListInput {
84
+ type?: MemoryType;
85
+ category?: string;
86
+ limit?: number;
87
+ }
88
+
89
+ export interface UpdateInput {
90
+ id: string;
91
+ title?: string;
92
+ description?: string;
93
+ tags?: string[];
94
+ content?: string;
95
+ importance?: number;
96
+ }
97
+
98
+ /**
99
+ * Create a short excerpt from markdown content.
100
+ */
101
+ export function makeExcerpt(content: string, maxLen = 160): string {
102
+ const text = content
103
+ .replace(/^#+\s.*/gm, "")
104
+ .replace(/```[\s\S]*?```/g, "")
105
+ .replace(/\n{2,}/g, " ")
106
+ .replace(/\n/g, " ")
107
+ .trim();
108
+ return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
109
+ }