@phren/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- package/starter/templates/python-project/tasks.md +10 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as crypto from "crypto";
|
|
4
|
+
import { debugLog, runtimeDir, } from "./shared.js";
|
|
5
|
+
import { withFileLock } from "./shared-governance.js";
|
|
6
|
+
import { errorMessage } from "./utils.js";
|
|
7
|
+
import { bootstrapSqlJs } from "./shared-sqljs.js";
|
|
8
|
+
let sqlJsLoader = bootstrapSqlJs;
|
|
9
|
+
const EMBED_CACHE_DB = "embed-cache.db";
|
|
10
|
+
function getCacheDbPath(phrenPath) {
|
|
11
|
+
const dir = runtimeDir(phrenPath);
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
return path.join(dir, EMBED_CACHE_DB);
|
|
14
|
+
}
|
|
15
|
+
function sha256(text) {
|
|
16
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
/** Encode a number[] embedding into a compact binary blob (Float32Array). */
|
|
19
|
+
function encodeEmbedding(embedding) {
|
|
20
|
+
const f32 = new Float32Array(embedding);
|
|
21
|
+
return Buffer.from(f32.buffer);
|
|
22
|
+
}
|
|
23
|
+
/** Decode a binary blob back to number[]. */
|
|
24
|
+
function decodeEmbedding(blob) {
|
|
25
|
+
const f32 = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
|
|
26
|
+
return Array.from(f32);
|
|
27
|
+
}
|
|
28
|
+
let sqlPromise = null;
|
|
29
|
+
// Q14: Synchronously-accessible resolved SQL static, set once sqlPromise settles.
|
|
30
|
+
let sqlResolved = null;
|
|
31
|
+
function getSql() {
|
|
32
|
+
if (!sqlPromise) {
|
|
33
|
+
sqlPromise = sqlJsLoader()
|
|
34
|
+
.then(sql => {
|
|
35
|
+
sqlResolved = sql;
|
|
36
|
+
return sql;
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
sqlPromise = null;
|
|
40
|
+
sqlResolved = null;
|
|
41
|
+
debugLog(`embedding: sql.js init failed: ${errorMessage(err)}`);
|
|
42
|
+
throw err;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return sqlPromise;
|
|
46
|
+
}
|
|
47
|
+
export function setSqlJsLoaderForTests(loader) {
|
|
48
|
+
sqlJsLoader = loader;
|
|
49
|
+
sqlPromise = null;
|
|
50
|
+
sqlResolved = null;
|
|
51
|
+
}
|
|
52
|
+
export function resetSqlJsStateForTests() {
|
|
53
|
+
sqlJsLoader = bootstrapSqlJs;
|
|
54
|
+
sqlPromise = null;
|
|
55
|
+
sqlResolved = null;
|
|
56
|
+
}
|
|
57
|
+
async function openCacheDb(phrenPath) {
|
|
58
|
+
const dbPath = getCacheDbPath(phrenPath);
|
|
59
|
+
const SQL = await getSql();
|
|
60
|
+
let db;
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(dbPath)) {
|
|
63
|
+
const data = fs.readFileSync(dbPath);
|
|
64
|
+
db = new SQL.Database(data);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
db = new SQL.Database();
|
|
68
|
+
}
|
|
69
|
+
db.run(`CREATE TABLE IF NOT EXISTS embeddings (
|
|
70
|
+
model TEXT NOT NULL,
|
|
71
|
+
hash TEXT NOT NULL,
|
|
72
|
+
embedding BLOB NOT NULL,
|
|
73
|
+
PRIMARY KEY (model, hash)
|
|
74
|
+
)`);
|
|
75
|
+
return db;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
try {
|
|
79
|
+
db?.close();
|
|
80
|
+
}
|
|
81
|
+
catch (e2) {
|
|
82
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
83
|
+
process.stderr.write(`[phren] embedding openCacheDb dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Q14: Persist the in-memory DB to disk under a file lock.
|
|
90
|
+
* Reads the current on-disk snapshot inside the lock, merges any entries that
|
|
91
|
+
* are in `db` but missing from disk, then writes atomically via temp-file rename.
|
|
92
|
+
* This prevents the "last writer wins" race where two concurrent processes each
|
|
93
|
+
* open the same on-disk snapshot, insert different entries, and overwrite each
|
|
94
|
+
* other's work.
|
|
95
|
+
*/
|
|
96
|
+
function persistDb(phrenPath, db) {
|
|
97
|
+
const dbPath = getCacheDbPath(phrenPath);
|
|
98
|
+
try {
|
|
99
|
+
withFileLock(dbPath, () => {
|
|
100
|
+
// Read the freshest on-disk snapshot (may have entries from another process)
|
|
101
|
+
let onDisk = null;
|
|
102
|
+
// We cannot call the async openCacheDb here; use a raw sync open instead.
|
|
103
|
+
// If that fails we fall back to writing `db` as-is (best-effort).
|
|
104
|
+
try {
|
|
105
|
+
if (fs.existsSync(dbPath)) {
|
|
106
|
+
// sql.js-fts5 is already initialised by the time we persist; reuse the
|
|
107
|
+
// cached promise result synchronously if available (it is a resolved
|
|
108
|
+
// Promise at this point because getCachedEmbedding awaited it first).
|
|
109
|
+
// We extract the resolved value by creating a fulfilled-only thenable.
|
|
110
|
+
if (sqlResolved) {
|
|
111
|
+
onDisk = new sqlResolved.Database(fs.readFileSync(dbPath));
|
|
112
|
+
onDisk.run(`CREATE TABLE IF NOT EXISTS embeddings (
|
|
113
|
+
model TEXT NOT NULL, hash TEXT NOT NULL, embedding BLOB NOT NULL,
|
|
114
|
+
PRIMARY KEY (model, hash)
|
|
115
|
+
)`);
|
|
116
|
+
// Merge entries from `db` into onDisk
|
|
117
|
+
const rows = db.exec("SELECT model, hash, embedding FROM embeddings")[0]?.values ?? [];
|
|
118
|
+
if (rows.length > 0) {
|
|
119
|
+
onDisk.run("BEGIN TRANSACTION");
|
|
120
|
+
for (const [model, hash, embedding] of rows) {
|
|
121
|
+
onDisk.run("INSERT OR IGNORE INTO embeddings (model, hash, embedding) VALUES (?, ?, ?)", [model, hash, embedding]);
|
|
122
|
+
}
|
|
123
|
+
onDisk.run("COMMIT");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
130
|
+
process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
131
|
+
try {
|
|
132
|
+
onDisk?.close();
|
|
133
|
+
}
|
|
134
|
+
catch (e2) {
|
|
135
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
136
|
+
process.stderr.write(`[phren] embedding persistDb onDiskClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
137
|
+
}
|
|
138
|
+
onDisk = null;
|
|
139
|
+
}
|
|
140
|
+
const target = onDisk ?? db;
|
|
141
|
+
const tmp = dbPath + `.tmp-${crypto.randomUUID()}`;
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(tmp, Buffer.from(target.export()));
|
|
144
|
+
fs.renameSync(tmp, dbPath);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
if (onDisk)
|
|
148
|
+
try {
|
|
149
|
+
onDisk.close();
|
|
150
|
+
}
|
|
151
|
+
catch (e2) {
|
|
152
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
153
|
+
process.stderr.write(`[phren] embedding persistDb onDiskCloseFinally: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
debugLog(`embedding: failed to persist cache db: ${errorMessage(err)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function lookupCache(db, model, hash) {
|
|
163
|
+
const results = db.exec("SELECT embedding FROM embeddings WHERE model = ? AND hash = ?", [model, hash]);
|
|
164
|
+
if (results.length > 0 && results[0].values.length > 0) {
|
|
165
|
+
const blob = results[0].values[0][0];
|
|
166
|
+
return decodeEmbedding(blob);
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function insertCache(db, model, hash, embedding) {
|
|
171
|
+
db.run("INSERT OR REPLACE INTO embeddings (model, hash, embedding) VALUES (?, ?, ?)", [model, hash, encodeEmbedding(embedding)]);
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// API embedding (unchanged)
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
/**
|
|
177
|
+
* Get embedding from OpenAI-compatible API.
|
|
178
|
+
* Calls POST https://api.openai.com/v1/embeddings (or compatible endpoint).
|
|
179
|
+
*/
|
|
180
|
+
async function getApiEmbedding(text, apiKey, model = "text-embedding-3-small") {
|
|
181
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: {
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
input: text,
|
|
189
|
+
model,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
const body = await response.text();
|
|
194
|
+
throw new Error(`Embedding API error ${response.status}: ${body}`);
|
|
195
|
+
}
|
|
196
|
+
const data = await response.json();
|
|
197
|
+
if (!data.data?.[0]?.embedding) {
|
|
198
|
+
throw new Error("Embedding API returned unexpected format");
|
|
199
|
+
}
|
|
200
|
+
return data.data[0].embedding;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get embeddings for multiple texts in a single API call.
|
|
204
|
+
* The OpenAI embeddings API supports array input natively.
|
|
205
|
+
*/
|
|
206
|
+
async function getApiEmbeddings(texts, apiKey, model = "text-embedding-3-small") {
|
|
207
|
+
if (texts.length === 0)
|
|
208
|
+
return [];
|
|
209
|
+
if (texts.length === 1)
|
|
210
|
+
return [await getApiEmbedding(texts[0], apiKey, model)];
|
|
211
|
+
const response = await fetch("https://api.openai.com/v1/embeddings", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
216
|
+
},
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
input: texts,
|
|
219
|
+
model,
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
const body = await response.text();
|
|
224
|
+
throw new Error(`Embedding API error ${response.status}: ${body}`);
|
|
225
|
+
}
|
|
226
|
+
const data = await response.json();
|
|
227
|
+
if (!data.data?.length) {
|
|
228
|
+
throw new Error("Embedding API returned unexpected format");
|
|
229
|
+
}
|
|
230
|
+
// Sort by index to ensure order matches input
|
|
231
|
+
const sorted = data.data.sort((a, b) => a.index - b.index);
|
|
232
|
+
return sorted.map(d => d.embedding);
|
|
233
|
+
}
|
|
234
|
+
export const embeddingOps = {
|
|
235
|
+
openCacheDb,
|
|
236
|
+
persistDb,
|
|
237
|
+
lookupCache,
|
|
238
|
+
insertCache,
|
|
239
|
+
getApiEmbedding,
|
|
240
|
+
getApiEmbeddings,
|
|
241
|
+
};
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Cached embedding (uses SQLite cache)
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
/**
|
|
246
|
+
* Get embedding with caching. Uses the configured provider.
|
|
247
|
+
*/
|
|
248
|
+
export async function getCachedEmbedding(phrenPath, text, apiKey, model) {
|
|
249
|
+
let db;
|
|
250
|
+
try {
|
|
251
|
+
const hash = sha256(`${model}:${text}`);
|
|
252
|
+
db = await embeddingOps.openCacheDb(phrenPath);
|
|
253
|
+
const cached = embeddingOps.lookupCache(db, model, hash);
|
|
254
|
+
if (cached)
|
|
255
|
+
return cached;
|
|
256
|
+
const embedding = await embeddingOps.getApiEmbedding(text, apiKey, model);
|
|
257
|
+
embeddingOps.insertCache(db, model, hash, embedding);
|
|
258
|
+
// Q14: persistDb now holds a file lock and merges with the on-disk snapshot
|
|
259
|
+
// before writing, so concurrent callers don't overwrite each other's entries.
|
|
260
|
+
embeddingOps.persistDb(phrenPath, db);
|
|
261
|
+
return embedding;
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
debugLog(`embedding: getCachedEmbedding failed: ${errorMessage(err)}`);
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
try {
|
|
269
|
+
db?.close();
|
|
270
|
+
}
|
|
271
|
+
catch (e2) {
|
|
272
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
273
|
+
process.stderr.write(`[phren] embedding getCachedEmbedding dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get embeddings for multiple texts with caching. Batches uncached texts into single API calls.
|
|
279
|
+
*/
|
|
280
|
+
export async function getCachedEmbeddings(phrenPath, texts, apiKey, model) {
|
|
281
|
+
if (texts.length === 0)
|
|
282
|
+
return [];
|
|
283
|
+
let db;
|
|
284
|
+
try {
|
|
285
|
+
db = await embeddingOps.openCacheDb(phrenPath);
|
|
286
|
+
const results = texts.map(text => {
|
|
287
|
+
const hash = sha256(`${model}:${text}`);
|
|
288
|
+
return embeddingOps.lookupCache(db, model, hash);
|
|
289
|
+
});
|
|
290
|
+
const uncachedIndices = [];
|
|
291
|
+
const uncachedTexts = [];
|
|
292
|
+
for (let i = 0; i < results.length; i++) {
|
|
293
|
+
if (results[i] === null) {
|
|
294
|
+
uncachedIndices.push(i);
|
|
295
|
+
uncachedTexts.push(texts[i]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (uncachedTexts.length > 0) {
|
|
299
|
+
const BATCH_SIZE = 20;
|
|
300
|
+
for (let start = 0; start < uncachedTexts.length; start += BATCH_SIZE) {
|
|
301
|
+
const batch = uncachedTexts.slice(start, start + BATCH_SIZE);
|
|
302
|
+
const batchEmbeddings = await embeddingOps.getApiEmbeddings(batch, apiKey, model);
|
|
303
|
+
for (let j = 0; j < batch.length; j++) {
|
|
304
|
+
const idx = uncachedIndices[start + j];
|
|
305
|
+
results[idx] = batchEmbeddings[j];
|
|
306
|
+
const hash = sha256(`${model}:${batch[j]}`);
|
|
307
|
+
embeddingOps.insertCache(db, model, hash, batchEmbeddings[j]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
embeddingOps.persistDb(phrenPath, db);
|
|
311
|
+
}
|
|
312
|
+
return results.map(result => result ?? []);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
debugLog(`embedding: getCachedEmbeddings failed: ${errorMessage(err)}`);
|
|
316
|
+
return texts.map(() => []);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
try {
|
|
320
|
+
db?.close();
|
|
321
|
+
}
|
|
322
|
+
catch (e2) {
|
|
323
|
+
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
324
|
+
process.stderr.write(`[phren] embedding getCachedEmbeddings dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Compute cosine similarity between two vectors.
|
|
330
|
+
*/
|
|
331
|
+
export function cosineSimilarity(a, b) {
|
|
332
|
+
if (a.length !== b.length || a.length === 0)
|
|
333
|
+
return 0;
|
|
334
|
+
let dot = 0, normA = 0, normB = 0;
|
|
335
|
+
for (let i = 0; i < a.length; i++) {
|
|
336
|
+
dot += a[i] * b[i];
|
|
337
|
+
normA += a[i] * a[i];
|
|
338
|
+
normB += b[i] * b[i];
|
|
339
|
+
}
|
|
340
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
341
|
+
return denom === 0 ? 0 : dot / denom;
|
|
342
|
+
}
|
|
343
|
+
// Export helpers for testing
|
|
344
|
+
export { encodeEmbedding, decodeEmbedding, openCacheDb };
|