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.
- package/README.md +12 -0
- package/dist/{chunk-D4EWPGBL.js → chunk-C5LYVVEI.js} +1 -1
- package/dist/{chunk-X5WEODUD.js → chunk-CYQU33FY.js} +1 -0
- package/dist/{chunk-QNLL3ZDF.js → chunk-G6CEEMAR.js} +3 -3
- package/dist/{chunk-4DB4XTSL.js → chunk-KTX4MGMR.js} +15 -1
- package/dist/{chunk-KJUGTLPQ.js → chunk-YO6JKGR3.js} +38 -2
- package/dist/{config-AIZJJ5D2.js → config-LZFXXOI4.js} +276 -14
- package/dist/{dev-WJ5QQ35B.js → dev-R3IYWZ3M.js} +2 -2
- package/dist/{disable-JDVOQNZG.js → disable-R6K5YJN4.js} +2 -2
- package/dist/{enable-JBJ4Q2S7.js → enable-HF4PYVJN.js} +2 -2
- package/dist/{host-NA7LZ4HX.js → host-SYZH3FVC.js} +4 -4
- package/dist/index.js +78 -18
- package/dist/{init-FZ3GG53E.js → init-YK6YRTOT.js} +102 -6
- package/dist/{install-I3GOS56Q.js → install-Q4PWEU43.js} +4 -4
- package/dist/{mcp-DLS3J6QJ.js → mcp-TBEDYELW.js} +4 -4
- package/dist/memory-7RM67ZLS.js +668 -0
- package/dist/postinstall.js +1 -1
- package/dist/{session-E3CZJJZQ.js → session-H4XW2WXH.js} +1 -1
- package/dist/{statusline-6HQCDWBD.js → statusline-6Y2EBAFQ.js} +1 -1
- package/dist/{uninstall-JN5YIKKM.js → uninstall-3PSUDGI4.js} +3 -3
- package/dist/{update-OWS4IJTG.js → update-WL6SFGGO.js} +4 -4
- package/lib/cf-memory/CHANGELOG.md +15 -0
- package/lib/cf-memory/README.md +284 -0
- package/lib/cf-memory/package-lock.json +2790 -0
- package/lib/cf-memory/package.json +31 -0
- package/lib/cf-memory/scripts/migrate-frontmatter.ts +134 -0
- package/lib/cf-memory/src/__tests__/daemon-e2e.test.ts +223 -0
- package/lib/cf-memory/src/__tests__/daemon.test.ts +407 -0
- package/lib/cf-memory/src/__tests__/dedup.test.ts +103 -0
- package/lib/cf-memory/src/__tests__/embeddings.test.ts +292 -0
- package/lib/cf-memory/src/__tests__/lazy-install.test.ts +210 -0
- package/lib/cf-memory/src/__tests__/markdown-backend.test.ts +410 -0
- package/lib/cf-memory/src/__tests__/migration.test.ts +255 -0
- package/lib/cf-memory/src/__tests__/migrations.test.ts +288 -0
- package/lib/cf-memory/src/__tests__/minisearch-backend.test.ts +262 -0
- package/lib/cf-memory/src/__tests__/ollama.test.ts +48 -0
- package/lib/cf-memory/src/__tests__/schema.test.ts +128 -0
- package/lib/cf-memory/src/__tests__/search.test.ts +115 -0
- package/lib/cf-memory/src/__tests__/temporal-decay.test.ts +54 -0
- package/lib/cf-memory/src/__tests__/tier.test.ts +293 -0
- package/lib/cf-memory/src/__tests__/tools.test.ts +83 -0
- package/lib/cf-memory/src/backends/markdown.ts +318 -0
- package/lib/cf-memory/src/backends/minisearch.ts +203 -0
- package/lib/cf-memory/src/backends/sqlite/embeddings.ts +286 -0
- package/lib/cf-memory/src/backends/sqlite/index.ts +549 -0
- package/lib/cf-memory/src/backends/sqlite/migrations.ts +188 -0
- package/lib/cf-memory/src/backends/sqlite/schema.ts +120 -0
- package/lib/cf-memory/src/backends/sqlite/search.ts +296 -0
- package/lib/cf-memory/src/bin/cf-memory.ts +2 -0
- package/lib/cf-memory/src/daemon/entry.ts +99 -0
- package/lib/cf-memory/src/daemon/process.ts +220 -0
- package/lib/cf-memory/src/daemon/server.ts +166 -0
- package/lib/cf-memory/src/daemon/watcher.ts +90 -0
- package/lib/cf-memory/src/index.ts +45 -0
- package/lib/cf-memory/src/lib/backend.ts +23 -0
- package/lib/cf-memory/src/lib/daemon-client.ts +163 -0
- package/lib/cf-memory/src/lib/dedup.ts +80 -0
- package/lib/cf-memory/src/lib/lazy-install.ts +274 -0
- package/lib/cf-memory/src/lib/ollama.ts +76 -0
- package/lib/cf-memory/src/lib/temporal-decay.ts +19 -0
- package/lib/cf-memory/src/lib/tier.ts +107 -0
- package/lib/cf-memory/src/lib/types.ts +109 -0
- package/lib/cf-memory/src/resources/index.ts +62 -0
- package/lib/cf-memory/src/server.ts +20 -0
- package/lib/cf-memory/src/tools/delete.ts +38 -0
- package/lib/cf-memory/src/tools/list.ts +38 -0
- package/lib/cf-memory/src/tools/retrieve.ts +52 -0
- package/lib/cf-memory/src/tools/search.ts +47 -0
- package/lib/cf-memory/src/tools/store.ts +70 -0
- package/lib/cf-memory/src/tools/update.ts +62 -0
- package/lib/cf-memory/tsconfig.json +15 -0
- package/lib/cf-memory/vitest.config.ts +7 -0
- package/lib/learn-host/CHANGELOG.md +4 -0
- package/lib/learn-host/package.json +1 -1
- 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
|
+
}
|