@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 CHANGED
@@ -198,7 +198,7 @@ For each item, use the memory_store tool to save it with an appropriate title an
198
198
  return;
199
199
  }
200
200
 
201
- await showMemorySettings(pi);
201
+ await showMemorySettings(ctx);
202
202
  },
203
203
  });
204
204
  }
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(pi: ExtensionAPI): Promise<number> {
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, pi);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Persistent cross-session memory with vector search for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/storage.ts CHANGED
@@ -44,7 +44,22 @@ interface MemoryFrontmatter {
44
44
  }
45
45
 
46
46
  const MEMORY_DB_NAME = "memory.db";
47
- const MEMORY_EMBEDDING_DIM = 384;
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[${MEMORY_EMBEDDING_DIM}])
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
- // Upsert into memories table
336
- const stmt = this.db.prepare(`
337
- INSERT OR REPLACE INTO memories (id, title, content, tags, project, type, created, updated, embedding)
338
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
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
- const tagsJson = JSON.stringify(record.tags);
342
- const embeddingBuf = record.embedding ? Buffer.from(record.embedding.buffer) : null;
343
-
344
- stmt.run(
345
- record.id,
346
- record.title,
347
- record.content,
348
- tagsJson,
349
- record.project,
350
- record.type,
351
- record.created,
352
- record.updated,
353
- embeddingBuf
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
- // Update vector table
357
- if (record.embedding) {
358
- try {
359
- // Delete old vector if exists
360
- this.db.prepare("DELETE FROM memories_vec WHERE rowid = ?").run(BigInt(this.idToRowid(record.id)));
361
- } catch {
362
- // Ignore if not found
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
- const vecStmt = this.db.prepare(
367
- "INSERT INTO memories_vec(rowid, embedding) VALUES (?, ?)"
368
- );
369
- vecStmt.run(
370
- BigInt(this.idToRowid(record.id)),
371
- Buffer.from(record.embedding.buffer)
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("[unipi/memory] Failed to insert vector:", err);
479
+ console.warn(`[unipi/memory] Failed to sync ${file}:`, err);
375
480
  }
376
481
  }
377
482
 
378
- // Write markdown file
379
- const mdPath = path.join(this.scopeDir, `${record.id}.md`);
380
- writeMemoryFile(mdPath, record);
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 if similar memory exists
75
+ // Step 1: Check for exact duplicate
76
76
  const existing = storage.getByTitle(params.title);
77
77
  if (existing) {
78
- // Update existing
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
- // Create new memory
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,
@@ -2,10 +2,10 @@
2
2
  * @unipi/memory — Settings TUI
3
3
  *
4
4
  * Interactive settings dialog for embedding configuration.
5
- * Uses pi's UI primitives (select, input, notify).
5
+ * Uses ctx.ui primitives (select, input, notify).
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
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(pi: ExtensionAPI): Promise<void> {
34
- // Cast to access pi.ui which exists at runtime but isn't typed
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 selected = await ui.select({
119
- title: "🧠 Memory Settings",
120
- message: statusLines.join("\n"),
121
- options,
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 (!selected || selected === "__exit__") {
119
+ if (!selectedValue || selectedValue === "__exit__") {
125
120
  running = false;
126
121
  continue;
127
122
  }
128
123
 
129
- switch (selected) {
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
- await ui.notify({
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, pi);
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
- await ui.notify({
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: PiUI): Promise<void> {
167
- const key = await ui.input({
168
- title: "OpenRouter API Key",
169
- message: "Enter your OpenRouter API key (sk-or-v1-...):",
170
- placeholder: "sk-or-v1-...",
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
- setApiKey(key.trim());
184
- await ui.notify({
185
- message: "API key saved. Vector search enabled.",
186
- level: "success",
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: PiUI): Promise<void> {
179
+ async function handleModelSelection(ui: ExtensionCommandContext["ui"]): Promise<void> {
195
180
  const config = loadEmbeddingConfig();
196
181
 
197
- const options = OPENROUTER_EMBEDDING_MODELS.map((m) => ({
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
- options.push({
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 selected = await ui.select({
211
- title: "Select Embedding Model",
212
- message: "Choose an embedding model. ⚠ Changing model invalidates existing embeddings.",
213
- options,
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
- let modelId = selected;
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 (selected === "__custom__") {
221
- const custom = await ui.input({
222
- title: "Custom Model ID",
223
- message: "Enter the OpenRouter model ID:",
224
- placeholder: "openai/text-embedding-3-small",
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
- await ui.notify({
239
- message: `Model set to ${modelId} (${dimensions}d).${hasModelChanged() ? " Re-embed existing memories to use new model." : ""}`,
240
- level: "success",
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: PiUI): Promise<void> {
235
+ async function handleDimensionsInput(ui: ExtensionCommandContext["ui"]): Promise<void> {
248
236
  const config = loadEmbeddingConfig();
249
237
 
250
- const dimStr = await ui.input({
251
- title: "Embedding Dimensions",
252
- message: `Enter dimensions (default: 384). Lower = faster, less storage.\nNote: openai/text-embedding-3 supports 256-3072.\nada-002 only supports 1536.`,
253
- placeholder: "384",
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
- await ui.notify({
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: PiUI, pi: ExtensionAPI): Promise<void> {
280
- const confirm = await ui.select({
281
- title: "Re-embed All Memories",
282
- message: " This will re-generate ALL embeddings using the current model.\nOld embeddings will be overwritten.\nThis may take a while and costs API calls.",
283
- options: [
284
- { label: "Yes, re-embed all", value: "yes", description: "Proceed with re-embedding" },
285
- { label: "Cancel", value: "no", description: "Abort" },
286
- ],
287
- });
288
-
289
- if (confirm !== "yes") return;
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(pi);
276
+ const count = await reembedAllMemories(ctx);
294
277
 
295
278
  markModelUsed();
296
279
 
297
- await ui.notify({
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
  }