@pi-unipi/memory 0.1.6 → 0.1.9
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/commands.ts +1 -1
- package/embedding.ts +3 -3
- package/index.ts +6 -0
- package/package.json +1 -1
- package/storage.ts +185 -39
- package/tools.ts +64 -3
- package/tui/settings-tui.ts +80 -100
package/commands.ts
CHANGED
package/embedding.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* openai/text-embedding-3 supports custom dimensions via API param.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import {
|
|
13
13
|
loadEmbeddingConfig,
|
|
14
14
|
getApiKey,
|
|
@@ -174,7 +174,7 @@ export async function generateEmbeddingsBatch(
|
|
|
174
174
|
* Re-embed all memories across all projects.
|
|
175
175
|
* Returns count of successfully re-embedded memories.
|
|
176
176
|
*/
|
|
177
|
-
export async function reembedAllMemories(
|
|
177
|
+
export async function reembedAllMemories(ctx: ExtensionCommandContext): Promise<number> {
|
|
178
178
|
const { getAllProjectDirs, MemoryStorage } = await import("./storage.js");
|
|
179
179
|
const projectDirs = getAllProjectDirs();
|
|
180
180
|
let count = 0;
|
|
@@ -197,7 +197,7 @@ export async function reembedAllMemories(pi: ExtensionAPI): Promise<number> {
|
|
|
197
197
|
|
|
198
198
|
// Generate embeddings in batch
|
|
199
199
|
const texts = fullRecords.map((r) => `${r.title} ${r.content}`);
|
|
200
|
-
const embeddings = await generateEmbeddingsBatch(texts
|
|
200
|
+
const embeddings = await generateEmbeddingsBatch(texts);
|
|
201
201
|
|
|
202
202
|
// Update records
|
|
203
203
|
for (let i = 0; i < fullRecords.length; i++) {
|
package/index.ts
CHANGED
|
@@ -75,6 +75,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
75
|
projectStorage = new MemoryStorage(projectName);
|
|
76
76
|
try {
|
|
77
77
|
projectStorage.init();
|
|
78
|
+
|
|
79
|
+
// Sync any orphaned markdown files into the database
|
|
80
|
+
const synced = projectStorage.syncOrphanedFiles();
|
|
81
|
+
if (synced > 0) {
|
|
82
|
+
console.warn(`[unipi/memory] Synced ${synced} orphaned memory files into database`);
|
|
83
|
+
}
|
|
78
84
|
} catch (err) {
|
|
79
85
|
console.warn("[unipi/memory] Failed to initialize storage, running without memory:", (err as any)?.message ?? err);
|
|
80
86
|
projectStorage = null;
|
package/package.json
CHANGED
package/storage.ts
CHANGED
|
@@ -44,7 +44,22 @@ interface MemoryFrontmatter {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const MEMORY_DB_NAME = "memory.db";
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Get the configured embedding dimensions.
|
|
49
|
+
* Reads from config, falls back to 384.
|
|
50
|
+
*/
|
|
51
|
+
function getEmbeddingDims(): number {
|
|
52
|
+
try {
|
|
53
|
+
const configPath = path.join(os.homedir(), ".unipi", "memory", "config.json");
|
|
54
|
+
if (fs.existsSync(configPath)) {
|
|
55
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
56
|
+
if (typeof raw.dimensions === "number" && raw.dimensions >= 64) {
|
|
57
|
+
return raw.dimensions;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
return 384;
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
/**
|
|
50
65
|
* Get the base memory directory (~/.unipi/memory/)
|
|
@@ -232,10 +247,11 @@ export class MemoryStorage {
|
|
|
232
247
|
* Open database and set up schema. Called by init() with retry logic.
|
|
233
248
|
*/
|
|
234
249
|
private initDb(dbPath: string): void {
|
|
235
|
-
this.db = new Database(dbPath);
|
|
250
|
+
this.db = new Database(dbPath, { timeout: 5000 });
|
|
236
251
|
|
|
237
252
|
// Enable WAL mode for concurrent reads
|
|
238
253
|
this.db.pragma("journal_mode = WAL");
|
|
254
|
+
this.db.pragma("busy_timeout = 5000");
|
|
239
255
|
|
|
240
256
|
// Load sqlite-vec extension
|
|
241
257
|
try {
|
|
@@ -262,7 +278,7 @@ export class MemoryStorage {
|
|
|
262
278
|
// Create vector table if sqlite-vec loaded
|
|
263
279
|
try {
|
|
264
280
|
this.db.exec(`
|
|
265
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(embedding float[${
|
|
281
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(embedding float[${getEmbeddingDims()}])
|
|
266
282
|
`);
|
|
267
283
|
} catch {
|
|
268
284
|
// vec0 table may already exist or sqlite-vec not loaded
|
|
@@ -315,6 +331,7 @@ export class MemoryStorage {
|
|
|
315
331
|
|
|
316
332
|
/**
|
|
317
333
|
* Store or update a memory record.
|
|
334
|
+
* Uses transaction to ensure atomicity — either all writes succeed or none do.
|
|
318
335
|
*/
|
|
319
336
|
store(record: MemoryRecord): void {
|
|
320
337
|
if (!this.db) throw new Error("Storage not initialized");
|
|
@@ -332,52 +349,181 @@ export class MemoryStorage {
|
|
|
332
349
|
// Set project if not provided
|
|
333
350
|
if (!record.project) record.project = this.projectName;
|
|
334
351
|
|
|
335
|
-
//
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
// Prepare markdown content BEFORE transaction (fail fast)
|
|
353
|
+
const mdPath = path.join(this.scopeDir, `${record.id}.md`);
|
|
354
|
+
const frontmatter: MemoryFrontmatter = {
|
|
355
|
+
title: record.title,
|
|
356
|
+
tags: record.tags,
|
|
357
|
+
project: record.project,
|
|
358
|
+
created: record.created,
|
|
359
|
+
updated: record.updated,
|
|
360
|
+
type: record.type,
|
|
361
|
+
};
|
|
362
|
+
const mdContent = `---\n${yaml.dump(frontmatter, { lineWidth: -1 })}---\n\n${record.content}\n`;
|
|
363
|
+
|
|
364
|
+
// Use transaction for atomicity
|
|
365
|
+
const storeInTx = this.db.transaction(() => {
|
|
366
|
+
// Upsert into memories table
|
|
367
|
+
const stmt = this.db!.prepare(`
|
|
368
|
+
INSERT OR REPLACE INTO memories (id, title, content, tags, project, type, created, updated, embedding)
|
|
369
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
370
|
+
`);
|
|
340
371
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
372
|
+
const tagsJson = JSON.stringify(record.tags);
|
|
373
|
+
const embeddingBuf = record.embedding ? Buffer.from(record.embedding.buffer) : null;
|
|
374
|
+
|
|
375
|
+
stmt.run(
|
|
376
|
+
record.id,
|
|
377
|
+
record.title,
|
|
378
|
+
record.content,
|
|
379
|
+
tagsJson,
|
|
380
|
+
record.project,
|
|
381
|
+
record.type,
|
|
382
|
+
record.created,
|
|
383
|
+
record.updated,
|
|
384
|
+
embeddingBuf
|
|
385
|
+
);
|
|
355
386
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
387
|
+
// Update vector table
|
|
388
|
+
if (record.embedding) {
|
|
389
|
+
try {
|
|
390
|
+
// Delete old vector if exists
|
|
391
|
+
this.db!.prepare("DELETE FROM memories_vec WHERE rowid = ?").run(BigInt(this.idToRowid(record.id)));
|
|
392
|
+
} catch {
|
|
393
|
+
// Ignore if not found
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const vecStmt = this.db!.prepare(
|
|
398
|
+
"INSERT INTO memories_vec(rowid, embedding) VALUES (?, ?)"
|
|
399
|
+
);
|
|
400
|
+
vecStmt.run(
|
|
401
|
+
BigInt(this.idToRowid(record.id)),
|
|
402
|
+
Buffer.from(record.embedding.buffer)
|
|
403
|
+
);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
console.warn("[unipi/memory] Failed to insert vector:", err);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Execute transaction
|
|
411
|
+
storeInTx();
|
|
412
|
+
|
|
413
|
+
// Write markdown file AFTER successful DB write
|
|
414
|
+
try {
|
|
415
|
+
// Ensure directory exists
|
|
416
|
+
const dir = path.dirname(mdPath);
|
|
417
|
+
if (!fs.existsSync(dir)) {
|
|
418
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
363
419
|
}
|
|
420
|
+
fs.writeFileSync(mdPath, mdContent, "utf-8");
|
|
421
|
+
} catch (err) {
|
|
422
|
+
// DB write succeeded but file write failed — log but don't throw
|
|
423
|
+
// Memory is still in DB and searchable
|
|
424
|
+
console.warn("[unipi/memory] Failed to write markdown file:", err);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Sync orphaned markdown files into the database.
|
|
430
|
+
* Reads all .md files in the project dir, parses frontmatter,
|
|
431
|
+
* and inserts any that are missing from the DB.
|
|
432
|
+
* Returns count of synced files.
|
|
433
|
+
*/
|
|
434
|
+
syncOrphanedFiles(): number {
|
|
435
|
+
if (!this.db) throw new Error("Storage not initialized");
|
|
364
436
|
|
|
437
|
+
const files = fs.readdirSync(this.scopeDir)
|
|
438
|
+
.filter(f => f.endsWith(".md") && !f.startsWith("."));
|
|
439
|
+
|
|
440
|
+
// Get existing IDs from DB
|
|
441
|
+
const existingIds = new Set(
|
|
442
|
+
(this.db.prepare("SELECT id FROM memories").all() as any[])
|
|
443
|
+
.map(r => r.id)
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
let synced = 0;
|
|
447
|
+
for (const file of files) {
|
|
448
|
+
const filePath = path.join(this.scopeDir, file);
|
|
449
|
+
const record = parseMemoryFile(filePath);
|
|
450
|
+
if (!record) continue;
|
|
451
|
+
|
|
452
|
+
// Generate ID from title (same logic as store())
|
|
453
|
+
const id = record.title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
454
|
+
|
|
455
|
+
if (existingIds.has(id)) continue; // Already in DB
|
|
456
|
+
|
|
457
|
+
// Insert into DB
|
|
365
458
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
459
|
+
record.id = id;
|
|
460
|
+
const tagsJson = JSON.stringify(record.tags);
|
|
461
|
+
|
|
462
|
+
this.db.prepare(`
|
|
463
|
+
INSERT OR IGNORE INTO memories (id, title, content, tags, project, type, created, updated, embedding)
|
|
464
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
465
|
+
`).run(
|
|
466
|
+
id,
|
|
467
|
+
record.title,
|
|
468
|
+
record.content,
|
|
469
|
+
tagsJson,
|
|
470
|
+
record.project || this.projectName,
|
|
471
|
+
record.type,
|
|
472
|
+
record.created,
|
|
473
|
+
record.updated
|
|
372
474
|
);
|
|
475
|
+
|
|
476
|
+
synced++;
|
|
477
|
+
console.warn(`[unipi/memory] Synced orphaned file: ${file}`);
|
|
373
478
|
} catch (err) {
|
|
374
|
-
console.warn(
|
|
479
|
+
console.warn(`[unipi/memory] Failed to sync ${file}:`, err);
|
|
375
480
|
}
|
|
376
481
|
}
|
|
377
482
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
483
|
+
return synced;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Check if a memory with the given title already exists.
|
|
488
|
+
*/
|
|
489
|
+
hasByTitle(title: string): boolean {
|
|
490
|
+
if (!this.db) throw new Error("Storage not initialized");
|
|
491
|
+
const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
492
|
+
const row = this.db.prepare("SELECT 1 FROM memories WHERE id = ?").get(id);
|
|
493
|
+
return !!row;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Find memories with similar titles (fuzzy match).
|
|
498
|
+
* Returns array of { record, similarity } sorted by similarity desc.
|
|
499
|
+
*/
|
|
500
|
+
findSimilarByTitle(title: string, threshold = 0.6): Array<{ record: MemoryRecord; similarity: number }> {
|
|
501
|
+
if (!this.db) throw new Error("Storage not initialized");
|
|
502
|
+
|
|
503
|
+
const allRows = this.db.prepare("SELECT id, title FROM memories").all() as any[];
|
|
504
|
+
const results: Array<{ record: MemoryRecord; similarity: number }> = [];
|
|
505
|
+
|
|
506
|
+
const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
507
|
+
const titleWords = new Set(normalizedTitle.split(/\s+/).filter((w: string) => w.length > 2));
|
|
508
|
+
|
|
509
|
+
for (const row of allRows) {
|
|
510
|
+
const normalizedRowTitle = row.title.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
511
|
+
const rowWords = new Set(normalizedRowTitle.split(/\s+/).filter((w: string) => w.length > 2));
|
|
512
|
+
|
|
513
|
+
// Calculate Jaccard similarity
|
|
514
|
+
const intersection = new Set([...titleWords].filter((w: string) => rowWords.has(w)));
|
|
515
|
+
const union = new Set([...titleWords, ...rowWords]);
|
|
516
|
+
const similarity = union.size > 0 ? intersection.size / union.size : 0;
|
|
517
|
+
|
|
518
|
+
if (similarity >= threshold) {
|
|
519
|
+
const record = this.getById(row.id);
|
|
520
|
+
if (record) {
|
|
521
|
+
results.push({ record, similarity });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return results.sort((a, b) => b.similarity - a.similarity);
|
|
381
527
|
}
|
|
382
528
|
|
|
383
529
|
/**
|
package/tools.ts
CHANGED
|
@@ -72,10 +72,28 @@ export function registerMemoryTools(
|
|
|
72
72
|
const storage = getStorage();
|
|
73
73
|
onActivity?.(); // Mark store as done for lifecycle
|
|
74
74
|
|
|
75
|
-
// Check
|
|
75
|
+
// Step 1: Check for exact duplicate
|
|
76
76
|
const existing = storage.getByTitle(params.title);
|
|
77
77
|
if (existing) {
|
|
78
|
-
//
|
|
78
|
+
// Exact match found — check if content is also the same
|
|
79
|
+
const isSameContent = existing.content.trim() === params.content.trim();
|
|
80
|
+
|
|
81
|
+
if (isSameContent) {
|
|
82
|
+
// Duplicate with same content — gentle error asking to read first
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `⚠️ Memory already exists with this title and content: "${params.title}"\n\n` +
|
|
88
|
+
`Please read the existing memory first using memory_search before saving.\n` +
|
|
89
|
+
`If you want to update it, provide new or modified content.`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
details: { action: "duplicate_detected", id: existing.id },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Same title but different content — update existing
|
|
79
97
|
const updated: MemoryRecord = {
|
|
80
98
|
...existing,
|
|
81
99
|
content: params.content,
|
|
@@ -102,7 +120,50 @@ export function registerMemoryTools(
|
|
|
102
120
|
};
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
//
|
|
123
|
+
// Step 2: Check for similar memories
|
|
124
|
+
const similarMemories = storage.findSimilarByTitle(params.title, 0.6);
|
|
125
|
+
|
|
126
|
+
if (similarMemories.length > 0) {
|
|
127
|
+
// Found similar memories — save but notify
|
|
128
|
+
const similarList = similarMemories
|
|
129
|
+
.slice(0, 3)
|
|
130
|
+
.map(s => ` - "${s.record.title}" (${Math.round(s.similarity * 100)}% similar)`)
|
|
131
|
+
.join("\n");
|
|
132
|
+
|
|
133
|
+
// Create new memory
|
|
134
|
+
const record: MemoryRecord = {
|
|
135
|
+
id: "",
|
|
136
|
+
title: params.title,
|
|
137
|
+
content: params.content,
|
|
138
|
+
tags: params.tags || [],
|
|
139
|
+
project: getProjectName(ctx.cwd),
|
|
140
|
+
type: (params.type as MemoryRecord["type"]) || "summary",
|
|
141
|
+
created: "",
|
|
142
|
+
updated: "",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const embedding = await generateEmbedding(
|
|
146
|
+
params.title + " " + params.content,
|
|
147
|
+
pi
|
|
148
|
+
);
|
|
149
|
+
record.embedding = embedding;
|
|
150
|
+
|
|
151
|
+
storage.store(record);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: `Stored memory: ${params.title}\n\n` +
|
|
158
|
+
`⚠️ Similar memories found:\n${similarList}\n\n` +
|
|
159
|
+
`Consider reviewing these to avoid redundancy.`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
details: { action: "created_with_similar", id: record.id, similar: similarMemories.map(s => s.record.id) },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Step 3: No duplicates or similar — create new memory
|
|
106
167
|
const record: MemoryRecord = {
|
|
107
168
|
id: "",
|
|
108
169
|
title: params.title,
|
package/tui/settings-tui.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* @unipi/memory — Settings TUI
|
|
3
3
|
*
|
|
4
4
|
* Interactive settings dialog for embedding configuration.
|
|
5
|
-
* Uses
|
|
5
|
+
* Uses ctx.ui primitives (select, input, notify).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import {
|
|
10
10
|
loadEmbeddingConfig,
|
|
11
11
|
saveEmbeddingConfig,
|
|
@@ -19,20 +19,12 @@ import {
|
|
|
19
19
|
type EmbeddingConfig,
|
|
20
20
|
} from "../settings.js";
|
|
21
21
|
|
|
22
|
-
/** pi.ui type that's available when TUI is present */
|
|
23
|
-
type PiUI = {
|
|
24
|
-
select: (opts: { title: string; message: string; options: Array<{ label: string; value: string; description?: string }> }) => Promise<string | null | undefined>;
|
|
25
|
-
input: (opts: { title: string; message: string; placeholder?: string; validate?: (value: string) => Promise<string | null> }) => Promise<string | null | undefined>;
|
|
26
|
-
notify: (opts: { message: string; level: string }) => Promise<void>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
22
|
/**
|
|
30
23
|
* Show memory settings dialog.
|
|
31
24
|
* Main entry point for /unipi:memory-settings command.
|
|
32
25
|
*/
|
|
33
|
-
export async function showMemorySettings(
|
|
34
|
-
|
|
35
|
-
const ui = (pi as any).ui as PiUI;
|
|
26
|
+
export async function showMemorySettings(ctx: ExtensionCommandContext): Promise<void> {
|
|
27
|
+
const ui = ctx.ui;
|
|
36
28
|
let running = true;
|
|
37
29
|
|
|
38
30
|
while (running) {
|
|
@@ -115,28 +107,28 @@ export async function showMemorySettings(pi: ExtensionAPI): Promise<void> {
|
|
|
115
107
|
description: "Exit settings",
|
|
116
108
|
});
|
|
117
109
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
const labels = options.map(o => `${o.label} — ${o.description}`);
|
|
111
|
+
const selected = await ui.select(
|
|
112
|
+
"🧠 Memory Settings",
|
|
113
|
+
labels,
|
|
114
|
+
);
|
|
115
|
+
// Map selected label back to value
|
|
116
|
+
const selectedOpt = options.find(o => `${o.label} — ${o.description}` === selected);
|
|
117
|
+
const selectedValue = selectedOpt?.value;
|
|
123
118
|
|
|
124
|
-
if (!
|
|
119
|
+
if (!selectedValue || selectedValue === "__exit__") {
|
|
125
120
|
running = false;
|
|
126
121
|
continue;
|
|
127
122
|
}
|
|
128
123
|
|
|
129
|
-
switch (
|
|
124
|
+
switch (selectedValue) {
|
|
130
125
|
case "__add_key__":
|
|
131
126
|
case "__update_key__":
|
|
132
127
|
await handleApiKeyInput(ui);
|
|
133
128
|
break;
|
|
134
129
|
case "__remove_key__":
|
|
135
130
|
clearApiKey();
|
|
136
|
-
|
|
137
|
-
message: "API key removed. Vector search disabled.",
|
|
138
|
-
level: "info",
|
|
139
|
-
});
|
|
131
|
+
ui.notify("API key removed. Vector search disabled.", "info");
|
|
140
132
|
break;
|
|
141
133
|
case "__select_model__":
|
|
142
134
|
await handleModelSelection(ui);
|
|
@@ -145,16 +137,13 @@ export async function showMemorySettings(pi: ExtensionAPI): Promise<void> {
|
|
|
145
137
|
await handleDimensionsInput(ui);
|
|
146
138
|
break;
|
|
147
139
|
case "__reembed__":
|
|
148
|
-
await handleReembed(ui,
|
|
140
|
+
await handleReembed(ui, ctx);
|
|
149
141
|
break;
|
|
150
142
|
case "__suppress__":
|
|
151
143
|
const cfg = loadEmbeddingConfig();
|
|
152
144
|
cfg.suppressMigrationWarning = true;
|
|
153
145
|
saveEmbeddingConfig(cfg);
|
|
154
|
-
|
|
155
|
-
message: "Migration warning suppressed.",
|
|
156
|
-
level: "info",
|
|
157
|
-
});
|
|
146
|
+
ui.notify("Migration warning suppressed.", "info");
|
|
158
147
|
break;
|
|
159
148
|
}
|
|
160
149
|
}
|
|
@@ -163,66 +152,65 @@ export async function showMemorySettings(pi: ExtensionAPI): Promise<void> {
|
|
|
163
152
|
/**
|
|
164
153
|
* Handle API key input.
|
|
165
154
|
*/
|
|
166
|
-
async function handleApiKeyInput(ui:
|
|
167
|
-
const key = await ui.input(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
validate: async (value: string) => {
|
|
172
|
-
if (!value || value.trim().length === 0) {
|
|
173
|
-
return "API key cannot be empty";
|
|
174
|
-
}
|
|
175
|
-
if (!value.startsWith("sk-or-") && !value.startsWith("sk-")) {
|
|
176
|
-
return "Key should start with sk-or- or sk-";
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
179
|
-
},
|
|
180
|
-
});
|
|
155
|
+
async function handleApiKeyInput(ui: ExtensionCommandContext["ui"]): Promise<void> {
|
|
156
|
+
const key = await ui.input(
|
|
157
|
+
"Enter your OpenRouter API key (sk-or-v1-...):",
|
|
158
|
+
"sk-or-v1-...",
|
|
159
|
+
);
|
|
181
160
|
|
|
182
161
|
if (key) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
162
|
+
const trimmed = key.trim();
|
|
163
|
+
if (trimmed.length === 0) {
|
|
164
|
+
ui.notify("API key cannot be empty.", "warning");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!trimmed.startsWith("sk-or-") && !trimmed.startsWith("sk-")) {
|
|
168
|
+
ui.notify("Key should start with sk-or- or sk-.", "warning");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
setApiKey(trimmed);
|
|
172
|
+
ui.notify("API key saved. Vector search enabled.", "info");
|
|
188
173
|
}
|
|
189
174
|
}
|
|
190
175
|
|
|
191
176
|
/**
|
|
192
177
|
* Handle model selection.
|
|
193
178
|
*/
|
|
194
|
-
async function handleModelSelection(ui:
|
|
179
|
+
async function handleModelSelection(ui: ExtensionCommandContext["ui"]): Promise<void> {
|
|
195
180
|
const config = loadEmbeddingConfig();
|
|
196
181
|
|
|
197
|
-
const
|
|
182
|
+
const modelOptions = OPENROUTER_EMBEDDING_MODELS.map((m) => ({
|
|
198
183
|
label: `${m.name}${m.id === config.model ? " ✓" : ""}`,
|
|
199
184
|
value: m.id,
|
|
200
185
|
description: `${m.description} (${m.dimensions}d, ~${m.costPer1k}/1k tokens)`,
|
|
201
186
|
}));
|
|
202
187
|
|
|
203
188
|
// Add custom option
|
|
204
|
-
|
|
189
|
+
modelOptions.push({
|
|
205
190
|
label: "✏️ Custom Model ID",
|
|
206
191
|
value: "__custom__",
|
|
207
192
|
description: "Enter a custom OpenRouter model ID",
|
|
208
193
|
});
|
|
209
194
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
195
|
+
const labels = modelOptions.map(o => `${o.label} — ${o.description}`);
|
|
196
|
+
const selected = await ui.select(
|
|
197
|
+
"Select Embedding Model",
|
|
198
|
+
labels,
|
|
199
|
+
);
|
|
215
200
|
|
|
216
201
|
if (!selected) return;
|
|
217
202
|
|
|
218
|
-
|
|
203
|
+
// Map label back to value
|
|
204
|
+
const selectedOpt = modelOptions.find(o => `${o.label} — ${o.description}` === selected);
|
|
205
|
+
let modelId = selectedOpt?.value;
|
|
219
206
|
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
207
|
+
if (!modelId) return;
|
|
208
|
+
|
|
209
|
+
if (modelId === "__custom__") {
|
|
210
|
+
const custom = await ui.input(
|
|
211
|
+
"Enter the OpenRouter model ID:",
|
|
212
|
+
"openai/text-embedding-3-small",
|
|
213
|
+
);
|
|
226
214
|
if (!custom) return;
|
|
227
215
|
modelId = custom.trim();
|
|
228
216
|
}
|
|
@@ -235,40 +223,33 @@ async function handleModelSelection(ui: PiUI): Promise<void> {
|
|
|
235
223
|
config.dimensions = dimensions;
|
|
236
224
|
saveEmbeddingConfig(config);
|
|
237
225
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
226
|
+
ui.notify(
|
|
227
|
+
`Model set to ${modelId} (${dimensions}d).${hasModelChanged() ? " Re-embed existing memories to use new model." : ""}`,
|
|
228
|
+
"info",
|
|
229
|
+
);
|
|
242
230
|
}
|
|
243
231
|
|
|
244
232
|
/**
|
|
245
233
|
* Handle dimensions input.
|
|
246
234
|
*/
|
|
247
|
-
async function handleDimensionsInput(ui:
|
|
235
|
+
async function handleDimensionsInput(ui: ExtensionCommandContext["ui"]): Promise<void> {
|
|
248
236
|
const config = loadEmbeddingConfig();
|
|
249
237
|
|
|
250
|
-
const dimStr = await ui.input(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
validate: async (value: string) => {
|
|
255
|
-
const num = parseInt(value, 10);
|
|
256
|
-
if (isNaN(num) || num < 64 || num > 3072) {
|
|
257
|
-
return "Must be a number between 64 and 3072";
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
260
|
-
},
|
|
261
|
-
});
|
|
238
|
+
const dimStr = await ui.input(
|
|
239
|
+
`Enter dimensions (default: 384). Lower = faster, less storage.\nNote: openai/text-embedding-3 supports 256-3072.\nada-002 only supports 1536.`,
|
|
240
|
+
"384",
|
|
241
|
+
);
|
|
262
242
|
|
|
263
243
|
if (dimStr) {
|
|
264
244
|
const dims = parseInt(dimStr, 10);
|
|
245
|
+
if (isNaN(dims) || dims < 64 || dims > 3072) {
|
|
246
|
+
ui.notify("Must be a number between 64 and 3072.", "warning");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
265
249
|
config.dimensions = dims;
|
|
266
250
|
saveEmbeddingConfig(config);
|
|
267
251
|
|
|
268
|
-
|
|
269
|
-
message: `Dimensions set to ${dims}. Re-embed existing memories to apply.`,
|
|
270
|
-
level: "success",
|
|
271
|
-
});
|
|
252
|
+
ui.notify(`Dimensions set to ${dims}. Re-embed existing memories to apply.`, "info");
|
|
272
253
|
}
|
|
273
254
|
}
|
|
274
255
|
|
|
@@ -276,26 +257,25 @@ async function handleDimensionsInput(ui: PiUI): Promise<void> {
|
|
|
276
257
|
* Handle re-embedding all memories.
|
|
277
258
|
* This is a destructive operation — warns user first.
|
|
278
259
|
*/
|
|
279
|
-
async function handleReembed(ui:
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
260
|
+
async function handleReembed(ui: ExtensionCommandContext["ui"], ctx: ExtensionCommandContext): Promise<void> {
|
|
261
|
+
const confirmOptions = [
|
|
262
|
+
{ label: "Yes, re-embed all — Proceed with re-embedding", value: "yes" },
|
|
263
|
+
{ label: "Cancel — Abort", value: "no" },
|
|
264
|
+
];
|
|
265
|
+
const confirmLabels = confirmOptions.map(o => o.label);
|
|
266
|
+
const confirm = await ui.select(
|
|
267
|
+
"Re-embed All Memories",
|
|
268
|
+
confirmLabels,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const confirmOpt = confirmOptions.find(o => o.label === confirm);
|
|
272
|
+
if (confirmOpt?.value !== "yes") return;
|
|
290
273
|
|
|
291
274
|
// Import here to avoid circular deps
|
|
292
275
|
const { reembedAllMemories } = await import("../embedding.js");
|
|
293
|
-
const count = await reembedAllMemories(
|
|
276
|
+
const count = await reembedAllMemories(ctx);
|
|
294
277
|
|
|
295
278
|
markModelUsed();
|
|
296
279
|
|
|
297
|
-
|
|
298
|
-
message: `Re-embedded ${count} memories with current model.`,
|
|
299
|
-
level: "success",
|
|
300
|
-
});
|
|
280
|
+
ui.notify(`Re-embedded ${count} memories with current model.`, "info");
|
|
301
281
|
}
|