pi-vault-mind 0.8.8 → 0.9.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.
@@ -5,7 +5,7 @@ import { Container, SelectList, Text } from "@earendil-works/pi-tui";
5
5
  import * as lancedb from "@lancedb/lancedb";
6
6
  import { getActiveCollection, setActiveCollection } from "./state.js";
7
7
  import { DEFAULT_CONFIG } from "./types.js";
8
- import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, } from "./utils.js";
8
+ import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, resolveInitCollectionName, } from "./utils.js";
9
9
  import { updateActiveCollectionWidget } from "./widget.js";
10
10
  import { cancelJobHelper, listJobs, retryJob, } from "./agent-queue.js";
11
11
  import { revokeToken, rotateToken } from "./auth.js";
@@ -23,8 +23,7 @@ import { createWatcherState, getWatcherStatus, startWatcher, stopWatcher } from
23
23
  // ── Shared helpers ───────────────────────────────────────────────────────────
24
24
  const VM_USAGE = [
25
25
  "**pi-vault-mind Commands**",
26
- "",
27
- " /vm init Scaffold config + collections",
26
+ " /vm init [--collection <name>] Scaffold config + collections (default collection path uses vault name)",
28
27
  " /vm validate Check LanceDB, config, and collection health",
29
28
  " /vm validate --audit Audit config for missing defaults",
30
29
  " /vm approve [collection] Batch-review pending entries",
@@ -34,7 +33,8 @@ const VM_USAGE = [
34
33
  " /vm collection select Select active collection",
35
34
  " /vm collection list List configured collections",
36
35
  " /vm collection create Create a new collection (wizard)",
37
- " /vm collection reindex Rebuild FTS + vector indexes (replaces old /qmd-index)",
36
+ " /vm collection reindex Rebuild FTS + vector indexes",
37
+ " /vm index [--all] [--reembed] [--remote] Index collections (alias: reindex)",
38
38
  " /vm discover-schema Infer schema from a JSONL file",
39
39
  " /vm injector create Create a new injector (wizard)",
40
40
  " /vm context enable|disable Enable/disable pi-context integration",
@@ -200,6 +200,15 @@ const handleInit = async (_args, ctx, pi) => {
200
200
  const created = [];
201
201
  const updated = [];
202
202
  const skipped = [];
203
+ const initTokens = _args.trim().split(/\s+/).filter(Boolean);
204
+ let requestedCollection;
205
+ for (let i = 0; i < initTokens.length; i++) {
206
+ if (initTokens[i] === "--collection") {
207
+ requestedCollection = initTokens[i + 1];
208
+ i++;
209
+ }
210
+ }
211
+ const initCollection = resolveInitCollectionName(ctx.cwd, requestedCollection);
203
212
  const ensureFile = (dest, tmpl) => {
204
213
  if (fs.existsSync(dest)) {
205
214
  skipped.push(dest);
@@ -216,7 +225,7 @@ const handleInit = async (_args, ctx, pi) => {
216
225
  version: 2,
217
226
  collections: {
218
227
  main: {
219
- path: "collections/main.jsonl",
228
+ path: `collections/${initCollection}.jsonl`,
220
229
  schema: ["id", "domain", "source", "fact", "tag", "artifact"],
221
230
  dedupField: "fact",
222
231
  },
@@ -284,7 +293,7 @@ const handleInit = async (_args, ctx, pi) => {
284
293
  if (skipped.length) {
285
294
  msg.push("", "Skipped (already exist):", ...skipped.map((s) => ` • ${path.relative(ctx.cwd, s)}`));
286
295
  }
287
- msg.push("", '💡 Tip: Tell the agent "Remember: [fact]" to auto-append to the main collection!');
296
+ msg.push("", `💡 Tip: default collection file is collections/${initCollection}.jsonl (override with /vm init --collection <name>).`);
288
297
  if (ctx.hasUI) {
289
298
  ctx.ui.notify(msg.join("\n"), "info");
290
299
  }
@@ -639,7 +648,7 @@ const handleReindex = async (args, ctx, pi) => {
639
648
  lines.unshift("**Reindex Report:**", "");
640
649
  lines.push("", rebuildEmbeddings
641
650
  ? "Entries will be re-embedded with current embedding model on next append."
642
- : "Use /vm reindex --reembed to regenerate embeddings after model switch.");
651
+ : "Use /vm index --reembed to regenerate embeddings after model switch.");
643
652
  ctx.ui.notify(lines.join("\n"), "info");
644
653
  };
645
654
  // ── /vm embedding ──────────────────────────────────────────────────────────
@@ -1553,7 +1562,7 @@ const handleToken = (args, ctx) => {
1553
1562
  // ── Main /vm command ───────────────────────────────────────────────────────
1554
1563
  export const registerCommands = (pi) => {
1555
1564
  pi.registerCommand("vm", {
1556
- description: "pi-vault-mind: manage collections, embedding, reindexing, and config.",
1565
+ description: "pi-vault-mind: manage collections, embedding, indexing, and config.",
1557
1566
  getArgumentCompletions: (_prefix) => {
1558
1567
  const words = _prefix.trim().split(/\s+/);
1559
1568
  const top = [
@@ -1574,6 +1583,8 @@ export const registerCommands = (pi) => {
1574
1583
  "personalize",
1575
1584
  "setup",
1576
1585
  "token",
1586
+ "index",
1587
+ "reindex",
1577
1588
  ];
1578
1589
  if (words.length === 1 || (words.length === 2 && words[1] === "")) {
1579
1590
  return top
@@ -1584,6 +1595,11 @@ export const registerCommands = (pi) => {
1584
1595
  const subcommand = words[0];
1585
1596
  if (words.length >= 2) {
1586
1597
  const prefix = words[1] || "";
1598
+ if (subcommand === "init") {
1599
+ return ["--collection"]
1600
+ .filter((c) => c.startsWith(prefix))
1601
+ .map((c) => ({ label: c, value: c, description: `init ${c}` }));
1602
+ }
1587
1603
  if (subcommand === "embedding") {
1588
1604
  return ["status", "use", "model", "models", "pull", "cloud"]
1589
1605
  .filter((c) => c.startsWith(prefix))
@@ -1677,6 +1693,9 @@ export const registerCommands = (pi) => {
1677
1693
  return handleSetup(rest, ctx, pi);
1678
1694
  case "token":
1679
1695
  return handleToken(rest, ctx);
1696
+ case "index":
1697
+ case "reindex":
1698
+ return handleReindex(rest, ctx, pi);
1680
1699
  default:
1681
1700
  ctx.ui.notify(VM_USAGE, "info");
1682
1701
  }
package/dist/src/index.js CHANGED
@@ -37,7 +37,12 @@ export default function (pi) {
37
37
  * during extension loading — the runtime must be initialized first. */
38
38
  pi.on("session_start", async (_event, ctx) => {
39
39
  updateActiveCollectionWidget(ctx);
40
- enableACM(pi);
40
+ /* Only enable ACM when this session has a vault configured */
41
+ const sessionCfg = loadConfig(ctx.cwd);
42
+ const piCtxCfg = sessionCfg.extensionCompatibility?.["pi-context"];
43
+ if (piCtxCfg?.enabled && piCtxCfg?.autoEnableAcm !== false) {
44
+ enableACM(pi);
45
+ }
41
46
  /* register shortcut */
42
47
  pi.registerShortcut("ctrl+alt+l", {
43
48
  description: "Select Active Collection",
@@ -30,7 +30,7 @@ import { authoriseRequest, resolveAuthToken } from "./auth.js";
30
30
  import { GITIGNORE_ENTRIES } from "./commands.js";
31
31
  import { searchHybrid, upsertEntry } from "./lance.js";
32
32
  import { DEFAULT_CONFIG } from "./types.js";
33
- import { EXT_ROOT, ensureDir, expandHome, findConfig, loadConfig, shrinkHome } from "./utils.js";
33
+ import { ensureDir, expandHome, findConfig, loadConfig, resolveInitCollectionName, shrinkHome, } from "./utils.js";
34
34
  import { processQueue, scanFile, startWatcher, stopWatcher } from "./watcher.js";
35
35
  export function createServerState(port = 11435) {
36
36
  return { server: null, wss: null, port, startTime: 0 };
@@ -493,22 +493,43 @@ function handleVmInit(req, res) {
493
493
  res.end(JSON.stringify({ error: `Vault path does not exist: ${vaultPath}` }));
494
494
  return;
495
495
  }
496
+ const initCollection = resolveInitCollectionName(vaultPath, parsed.collection);
496
497
  // Scaffold project config
497
498
  const cfgPath = path.join(vaultPath, "pi-vault-mind.config.json");
498
499
  if (!fs.existsSync(cfgPath)) {
499
- const tmpl = path.join(EXT_ROOT, "templates", "pi-vault-mind.config.json");
500
- if (fs.existsSync(tmpl)) {
501
- fs.copyFileSync(tmpl, cfgPath);
502
- }
503
- else {
504
- fs.writeFileSync(cfgPath, `${JSON.stringify({ version: 2, collections: DEFAULT_CONFIG.collections, injectors: DEFAULT_CONFIG.injectors, vaultMind: DEFAULT_CONFIG.vaultMind }, null, 2)}\n`, "utf-8");
505
- }
506
- }
507
- // Scaffold collections dir
508
- const collDir = path.join(vaultPath, "collections");
509
- if (!fs.existsSync(collDir))
510
- fs.mkdirSync(collDir, { recursive: true });
511
- for (const [name, def] of Object.entries(DEFAULT_CONFIG.collections)) {
500
+ const tmpl = {
501
+ version: 2,
502
+ collections: {
503
+ main: {
504
+ path: `collections/${initCollection}.jsonl`,
505
+ schema: ["id", "domain", "source", "fact", "tag", "artifact"],
506
+ dedupField: "fact",
507
+ },
508
+ pending: { path: "collections/pending.jsonl", schema: "main" },
509
+ context_events: {
510
+ path: "collections/context_events.jsonl",
511
+ schema: ["id", "type", "session_entry_id", "content", "timestamp", "tags"],
512
+ dedupField: "id",
513
+ },
514
+ },
515
+ injectors: [
516
+ {
517
+ name: "draft-context",
518
+ regex: "draft\\s+(\\S+)",
519
+ collection: "main",
520
+ filterField: "tag",
521
+ artifactPath: "collections/artifact.md",
522
+ },
523
+ ],
524
+ vaultMind: DEFAULT_CONFIG.vaultMind,
525
+ };
526
+ fs.writeFileSync(cfgPath, `${JSON.stringify(tmpl, null, 2)}\n`, "utf-8");
527
+ }
528
+ // Scaffold collections dir from config
529
+ const projectCfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
530
+ for (const def of Object.values(projectCfg.collections || {})) {
531
+ if (!def.path)
532
+ continue;
512
533
  const p = path.join(vaultPath, def.path);
513
534
  if (!fs.existsSync(p)) {
514
535
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -4,6 +4,12 @@ import * as path from "node:path";
4
4
  import { MODAL_TOKEN_ENV, createModalClient, modalUrl, resolveModalToken } from "./modal-config.js";
5
5
  import { collectionNames, findConfig, getGlobalConfigPath, loadConfig, shrinkHome, } from "./utils.js";
6
6
  const ONBOARDING_PLUGIN_REQUIREMENTS = [
7
+ {
8
+ id: "obsidian42-brat",
9
+ label: "BRAT (Beta Reviewer's Auto-update Tester)",
10
+ required: true,
11
+ reason: "required to install obsidian-pi-vault-mind (not yet in community registry)",
12
+ },
7
13
  {
8
14
  id: "obsidian-pi-vault-mind",
9
15
  label: "Vault Mind plugin",
@@ -61,7 +67,21 @@ const formatExecError = (err) => {
61
67
  }
62
68
  return err instanceof Error ? err.message : String(err);
63
69
  };
70
+ const BRAT_REPO = "kylebrodeur/obsidian-pi-vault-mind";
71
+ const installViaBrat = () => {
72
+ try {
73
+ execFileSync("obsidian", ["eval", `app.plugins.getPlugin("obsidian42-brat").addPlugin("${BRAT_REPO}")`], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
74
+ return { ok: true };
75
+ }
76
+ catch (err) {
77
+ return { ok: false, error: formatExecError(err) };
78
+ }
79
+ };
64
80
  const installObsidianPlugin = (pluginId) => {
81
+ // vault-mind plugin must be installed via BRAT (not in community registry)
82
+ if (pluginId === "obsidian-pi-vault-mind") {
83
+ return installViaBrat();
84
+ }
65
85
  try {
66
86
  execFileSync("obsidian", ["plugin:install", `id=${pluginId}`, "enable"], {
67
87
  encoding: "utf-8",
@@ -74,17 +94,15 @@ const installObsidianPlugin = (pluginId) => {
74
94
  }
75
95
  };
76
96
  export const buildPluginInstallGuidance = (plugins) => {
77
- const lines = [
78
- "Install missing Obsidian plugins:",
79
- "",
80
- "CLI (when Obsidian CLI is available):",
81
- ...plugins.map((plugin) => `obsidian plugin:install id=${plugin.id} enable`),
82
- "",
83
- "Deep links (open each URI):",
84
- ...plugins.map((plugin) => `obsidian://show-plugin?id=${encodeURIComponent(plugin.id)}`),
85
- "",
86
- "GUI fallback: Obsidian → Settings → Community plugins → Browse",
87
- ];
97
+ const bratPlugins = plugins.filter((p) => p.id === "obsidian-pi-vault-mind");
98
+ const registryPlugins = plugins.filter((p) => p.id !== "obsidian-pi-vault-mind");
99
+ const lines = ["Install missing Obsidian plugins:", ""];
100
+ if (registryPlugins.length > 0) {
101
+ lines.push("CLI (when Obsidian CLI is available):", ...registryPlugins.map((plugin) => ` obsidian plugin:install id=${plugin.id} enable`), "", "Deep links (open each URI):", ...registryPlugins.map((plugin) => ` obsidian://show-plugin?id=${encodeURIComponent(plugin.id)}`), "", "GUI fallback: Obsidian → Settings → Community plugins → Browse");
102
+ }
103
+ if (bratPlugins.length > 0) {
104
+ lines.push("", "Via BRAT (not yet in community registry):", ` Settings → BRAT → "Add Beta plugin" → ${BRAT_REPO}`, ` Or CLI: obsidian eval code='await app.plugins.getPlugin("obsidian42-brat").addPlugin("${BRAT_REPO}")'`);
105
+ }
88
106
  return lines.join("\n");
89
107
  };
90
108
  const runPluginOnboarding = async (ctx, vaultPath) => {
@@ -224,6 +242,60 @@ export const createInjectorWizard = async (ctx) => {
224
242
  fs.writeFileSync(cfgPath, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
225
243
  ctx.ui.notify(`✅ Injector "${name}" created and configured!`, "info");
226
244
  };
245
+ /** Probe Ollama at localhost for available embedding models. */
246
+ const probeOllamaModels = async () => {
247
+ try {
248
+ const res = await fetch("http://127.0.0.1:11434/api/tags", {
249
+ signal: AbortSignal.timeout(3000),
250
+ });
251
+ if (!res.ok)
252
+ return [];
253
+ const data = (await res.json());
254
+ if (!Array.isArray(data.models))
255
+ return [];
256
+ // Filter to embedding-capable models (heuristic: name contains "embed" or known embedding models)
257
+ const embeddingHints = ["embed", "minilm", "nomic", "bge", "gte", "e5", "mxbai"];
258
+ const all = data.models
259
+ .filter((m) => embeddingHints.some((h) => m.name.toLowerCase().includes(h)))
260
+ .map((m) => ({
261
+ name: m.name,
262
+ size: m.size > 1e9 ? `${(m.size / 1e9).toFixed(1)}GB` : `${(m.size / 1e6).toFixed(0)}MB`,
263
+ }));
264
+ // Also include all models as secondary options (user might know what they want)
265
+ const nonEmbedding = data.models
266
+ .filter((m) => !embeddingHints.some((h) => m.name.toLowerCase().includes(h)))
267
+ .map((m) => ({
268
+ name: m.name,
269
+ size: m.size > 1e9 ? `${(m.size / 1e9).toFixed(1)}GB` : `${(m.size / 1e6).toFixed(0)}MB`,
270
+ }));
271
+ return [...all, ...nonEmbedding];
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ };
277
+ /** Probe a Modal deployment for available embedding models via GET /models. */
278
+ const probeModalModels = async (baseUrl, cfg) => {
279
+ try {
280
+ const token = resolveModalToken(cfg);
281
+ const headers = {};
282
+ if (token)
283
+ headers.Authorization = `Bearer ${token}`;
284
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
285
+ headers,
286
+ signal: AbortSignal.timeout(8000),
287
+ });
288
+ if (!res.ok)
289
+ return [];
290
+ const data = (await res.json());
291
+ if (!Array.isArray(data.models))
292
+ return [];
293
+ return data.models.filter((m) => m.enabled);
294
+ }
295
+ catch {
296
+ return [];
297
+ }
298
+ };
227
299
  export const setupWizard = async (ctx, cliArgs) => {
228
300
  const existingGlobal = fs.existsSync(getGlobalConfigPath());
229
301
  const detectedVaultPath = detectVaultFromCwd(ctx.cwd);
@@ -336,55 +408,119 @@ export const setupWizard = async (ctx, cliArgs) => {
336
408
  }
337
409
  // ── Step 1.5: Obsidian plugin readiness ────────────────────────────────
338
410
  await runPluginOnboarding(ctx, vaultPath);
411
+ // ── Step 1.6: Pi skills ────────────────────────────────────────────────
412
+ const installSkills = await ctx.ui.confirm("Pi skills", "Install recommended Obsidian skills for agents? (obsidian-markdown, obsidian-bases, json-canvas, obsidian-cli, defuddle)");
413
+ if (installSkills) {
414
+ try {
415
+ execFileSync("npx", [
416
+ "-y",
417
+ "skills",
418
+ "add",
419
+ "https://github.com/kepano/obsidian-skills",
420
+ "--skill",
421
+ "obsidian-markdown",
422
+ "--skill",
423
+ "obsidian-bases",
424
+ "--skill",
425
+ "json-canvas",
426
+ "--skill",
427
+ "obsidian-cli",
428
+ "--skill",
429
+ "defuddle",
430
+ "-g",
431
+ "-a",
432
+ "pi",
433
+ "--copy",
434
+ "-y",
435
+ ], {
436
+ encoding: "utf-8",
437
+ stdio: ["ignore", "pipe", "pipe"],
438
+ env: { ...process.env, DISABLE_TELEMETRY: "1" },
439
+ });
440
+ ctx.ui.notify("✅ Pi skills installed.", "info");
441
+ }
442
+ catch (err) {
443
+ ctx.ui.notify(`⚠️ Skill install failed: ${formatExecError(err)}\nInstall manually: see INSTALL.md §Layer 2`, "warning");
444
+ }
445
+ }
339
446
  // ── Step 2: Embedding config ──────────────────────────────────────────
340
- let remoteUrl = await ctx.ui.input("Remote Embedding URL (e.g. https://.../v1, optional):", "");
341
- const localUrl = await ctx.ui.input("Local Embedding URL (e.g. http://127.0.0.1:11434/v1, optional):", "");
447
+ let remoteUrl = "";
448
+ let localUrl = "";
342
449
  let useTransformers = false;
343
450
  let guidedModalWorkspace;
344
- if (!remoteUrl && !localUrl) {
345
- const route = await ctx.ui.select("No embedding URL configured", [
346
- "Use local transformers (offline)",
347
- "Guide me through Modal setup",
348
- "Cancel setup",
349
- ]);
350
- if (!route || route === "Cancel setup") {
351
- ctx.ui.notify("Setup cancelled — at least one URL or transformers is required.", "warning");
451
+ let model = "";
452
+ let dim;
453
+ // Probe Ollama for local embedding models
454
+ const ollamaModels = await probeOllamaModels();
455
+ const providerOptions = [
456
+ ...(ollamaModels.length > 0 ? ["Local (Ollama — models detected)"] : []),
457
+ "Cloud (Modal workspace)",
458
+ "Custom URL (any /v1/embeddings endpoint)",
459
+ "Offline (transformers.js — no external service)",
460
+ ];
461
+ const provider = await ctx.ui.select("Embedding provider:", providerOptions);
462
+ if (!provider) {
463
+ ctx.ui.notify("Setup cancelled.", "warning");
464
+ return;
465
+ }
466
+ if (provider.startsWith("Local")) {
467
+ localUrl = "http://127.0.0.1:11434/v1";
468
+ // Let user pick from detected Ollama embedding models
469
+ const modelChoice = await ctx.ui.select("Select embedding model:", ollamaModels.map((m) => `${m.name} (${m.size})`));
470
+ if (!modelChoice) {
471
+ ctx.ui.notify("Setup cancelled.", "warning");
352
472
  return;
353
473
  }
354
- if (route === "Use local transformers (offline)") {
355
- useTransformers = true;
474
+ model = modelChoice.replace(/\s*\(.*\)$/, "");
475
+ }
476
+ else if (provider.startsWith("Cloud")) {
477
+ const workspaceSlug = await ctx.ui.input("Modal workspace slug:", "");
478
+ if (!workspaceSlug) {
479
+ ctx.ui.notify("Setup cancelled — workspace is required.", "warning");
480
+ return;
356
481
  }
357
- else {
358
- const workspaceSlug = await ctx.ui.input("Modal workspace slug:", "");
359
- if (!workspaceSlug) {
360
- ctx.ui.notify("Setup cancelled workspace is required for Modal guidance.", "warning");
361
- return;
482
+ guidedModalWorkspace = workspaceSlug.trim();
483
+ remoteUrl = modalUrl(guidedModalWorkspace);
484
+ // Probe Modal /models for available embedders
485
+ const modalModels = await probeModalModels(remoteUrl, loadConfig(ctx.cwd).vaultMind);
486
+ if (modalModels.length > 0) {
487
+ const modalChoice = await ctx.ui.select("Select cloud embedding model:", modalModels.map((m) => `${m.key} (dim ${m.native_dim}${m.gated ? ", gated" : ""})`));
488
+ if (modalChoice) {
489
+ model = modalChoice.replace(/\s*\(.*\)$/, "");
490
+ const matched = modalModels.find((m) => m.key === model);
491
+ if (matched)
492
+ dim = matched.native_dim;
362
493
  }
363
- guidedModalWorkspace = workspaceSlug.trim();
364
- remoteUrl = modalUrl(guidedModalWorkspace);
365
- ctx.ui.notify([
366
- "Modal endpoint discovery:",
367
- `- Derived remote URL: ${remoteUrl}`,
368
- "- Set token now with one of:",
369
- " /vm remote token",
370
- " ./scripts/fetch-modal-token.sh --write",
371
- "- Reference docs:",
372
- " docs/getting-started/NEW_VAULT_WALKTHROUGH.md",
373
- " docs/integrations/MODAL_EMBEDDING.md",
374
- ].join("\n"), "info");
375
- }
376
- }
377
- const model = (await ctx.ui.input("Embedding model key (optional):", ""))?.trim();
378
- if (model === undefined) {
379
- ctx.ui.notify("Setup cancelled.", "warning");
380
- return;
494
+ }
495
+ else {
496
+ ctx.ui.notify("Could not reach Modal /models — using default (embeddinggemma @ 768).", "warning");
497
+ model = "embeddinggemma";
498
+ dim = 768;
499
+ }
500
+ ctx.ui.notify([
501
+ "Modal endpoint discovery:",
502
+ `- Remote URL: ${remoteUrl}`,
503
+ "- Set token with: /vm remote token or ./scripts/fetch-modal-token.sh --write",
504
+ ].join("\n"), "info");
381
505
  }
382
- const dimStr = await ctx.ui.input("Dimensions (optional):", "");
383
- const dim = dimStr ? Number.parseInt(dimStr, 10) : undefined;
384
- let workspace = guidedModalWorkspace;
385
- if (!workspace && remoteUrl?.includes("modal.run")) {
386
- workspace = await ctx.ui.input("Modal workspace (optional):", "");
506
+ else if (provider.startsWith("Custom")) {
507
+ const url = await ctx.ui.input("Embedding URL (e.g. https://my-server.com/v1):", "");
508
+ if (!url) {
509
+ ctx.ui.notify("Setup cancelled — URL required.", "warning");
510
+ return;
511
+ }
512
+ remoteUrl = url;
513
+ model = (await ctx.ui.input("Model name (optional):", ""))?.trim() || "";
514
+ const dimStr = await ctx.ui.input("Dimensions (optional):", "");
515
+ dim = dimStr ? Number.parseInt(dimStr, 10) : undefined;
516
+ }
517
+ else {
518
+ // Offline transformers
519
+ useTransformers = true;
520
+ model = "Xenova/all-MiniLM-L6-v2";
521
+ dim = 384;
387
522
  }
523
+ const workspace = guidedModalWorkspace;
388
524
  // ── Step 3: Deterministic runtime settings ──────────────────────────────
389
525
  const enableContextAutomation = await ctx.ui.confirm("Context automation", "Enable pi-context integration with auto-/acm trigger and event indexing?");
390
526
  const enableAutoStart = await ctx.ui.confirm("Vault auto-start", "Auto-start watcher/server for this vault when pi session starts?");
@@ -428,22 +564,30 @@ export const setupWizard = async (ctx, cliArgs) => {
428
564
  };
429
565
  config.vaultMind.graph = config.vaultMind.graph || { enabled: true, canvasSync: true };
430
566
  config.vaultMind.ftsEnabled = config.vaultMind.ftsEnabled !== false;
431
- config.extensionCompatibility = config.extensionCompatibility || {};
432
- config.extensionCompatibility["pi-context"] = {
567
+ const dir = path.dirname(getGlobalConfigPath());
568
+ if (!fs.existsSync(dir))
569
+ fs.mkdirSync(dir, { recursive: true });
570
+ fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
571
+ // Write extension compatibility to the PROJECT config (not global)
572
+ // so pi-context settings are scoped to this vault, not every session.
573
+ const projectCfgPath = path.join(vaultPath, "pi-vault-mind.config.json");
574
+ const projectCfg = fs.existsSync(projectCfgPath)
575
+ ? JSON.parse(fs.readFileSync(projectCfgPath, "utf-8"))
576
+ : {};
577
+ projectCfg.extensionCompatibility = projectCfg.extensionCompatibility || {};
578
+ projectCfg.extensionCompatibility["pi-context"] = {
433
579
  tagPatterns: [],
434
580
  enhanceInjectors: false,
435
581
  autoEnableAcm: true,
436
582
  indexContextEvents: true,
437
- ...(config.extensionCompatibility["pi-context"] || {}),
583
+ ...(projectCfg.extensionCompatibility["pi-context"] || {}),
438
584
  enabled: enableContextAutomation,
439
585
  };
440
- const dir = path.dirname(getGlobalConfigPath());
441
- if (!fs.existsSync(dir))
442
- fs.mkdirSync(dir, { recursive: true });
443
- fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
586
+ fs.writeFileSync(projectCfgPath, `${JSON.stringify(projectCfg, null, 2)}\n`, "utf-8");
444
587
  ctx.ui.notify([
445
- "✅ Global config written!",
446
- ` ${getGlobalConfigPath()}`,
588
+ "✅ Config written!",
589
+ ` Global: ${getGlobalConfigPath()}`,
590
+ ` Project: ${projectCfgPath}`,
447
591
  "",
448
592
  ` Vault: ${vaultPath}`,
449
593
  ` Remote: ${remoteUrl || "none"}`,
@@ -454,13 +598,6 @@ export const setupWizard = async (ctx, cliArgs) => {
454
598
  "",
455
599
  "Next: /vm watcher start (or restart pi for auto-start)",
456
600
  ].join("\n"), "info");
457
- if (remoteUrl?.includes("modal.run")) {
458
- ctx.ui.notify([
459
- "Modal token reminder:",
460
- "- /vm remote token",
461
- "- ./scripts/fetch-modal-token.sh --write",
462
- ].join("\n"), "info");
463
- }
464
601
  };
465
602
  /**
466
603
  * Interactive Modal embedding configuration + "Test connection" action.
@@ -21,6 +21,9 @@ export declare const hasPiContextTools: (pi: ExtensionAPI) => boolean;
21
21
  export declare const isPiContextEnabled: (cfg: UniversalConfig) => boolean;
22
22
  export declare const getPiContextConfig: (cfg: UniversalConfig) => PiContextDef;
23
23
  export declare const collectionNames: (cfg: UniversalConfig) => string[];
24
+ export declare const normalizeCollectionName: (value: string) => string;
25
+ export declare const deriveCollectionNameFromVaultPath: (vaultPath: string) => string;
26
+ export declare const resolveInitCollectionName: (vaultPath: string, override?: string) => string;
24
27
  /** Resolve a configured vault folder, falling back to the default layout. */
25
28
  export declare function resolveVaultFolder(cfg: VaultMindConfig, key: "inbox" | "library" | "presentations" | "journal"): string;
26
29
  /** Resolve the configured default vault path, or null if none is configured. */
package/dist/src/utils.js CHANGED
@@ -132,6 +132,25 @@ export const getPiContextConfig = (cfg) => ({
132
132
  });
133
133
  // Backward compat alias
134
134
  export const collectionNames = (cfg) => Object.keys(cfg.collections);
135
+ export const normalizeCollectionName = (value) => value
136
+ .trim()
137
+ .toLowerCase()
138
+ .replace(/[^a-z0-9]+/g, "_")
139
+ .replace(/^_+|_+$/g, "")
140
+ .slice(0, 64);
141
+ export const deriveCollectionNameFromVaultPath = (vaultPath) => {
142
+ const base = path.basename(path.resolve(vaultPath));
143
+ const normalized = normalizeCollectionName(base);
144
+ return normalized || "main";
145
+ };
146
+ export const resolveInitCollectionName = (vaultPath, override) => {
147
+ if (override && override.trim().length > 0) {
148
+ const normalized = normalizeCollectionName(override);
149
+ if (normalized)
150
+ return normalized;
151
+ }
152
+ return deriveCollectionNameFromVaultPath(vaultPath);
153
+ };
135
154
  /** Default vault folder layout. Paths are relative to the vault root. */
136
155
  const DEFAULT_VAULT_FOLDERS = {
137
156
  inbox: "Agent/Inbox",
@@ -114,16 +114,30 @@ describe("REST setup routes", () => {
114
114
  const { status } = await fetchJson("127.0.0.1", port, "GET", "/vm/status");
115
115
  assert.equal(status, 401);
116
116
  });
117
- it("POST /vm/init scaffolds config and collections", async () => {
117
+ it("POST /vm/init scaffolds config and vault-named collection file", async () => {
118
118
  const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath }, "test-token");
119
119
  assert.equal(status, 200);
120
120
  const b = body;
121
121
  assert.equal(b.ok, true);
122
122
  assert.equal(b.path, vaultPath);
123
- assert.ok(fs.existsSync(path.join(vaultPath, "pi-vault-mind.config.json")));
124
- assert.ok(fs.existsSync(path.join(vaultPath, "collections", "main.jsonl")));
123
+ const configPath = path.join(vaultPath, "pi-vault-mind.config.json");
124
+ assert.ok(fs.existsSync(configPath));
125
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
126
+ const mainPath = String(cfg.collections?.main?.path || "");
127
+ assert.notEqual(mainPath, "collections/main.jsonl");
128
+ assert.match(mainPath, /^collections\/.+\.jsonl$/);
129
+ assert.ok(fs.existsSync(path.join(vaultPath, mainPath)));
125
130
  assert.ok(fs.existsSync(path.join(vaultPath, ".gitignore")));
126
131
  });
132
+ it("POST /vm/init accepts collection override", async () => {
133
+ const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath, collection: "returnvape" }, "test-token");
134
+ assert.equal(status, 200);
135
+ const b = body;
136
+ assert.equal(b.ok, true);
137
+ const cfg = JSON.parse(fs.readFileSync(path.join(vaultPath, "pi-vault-mind.config.json"), "utf-8"));
138
+ assert.equal(cfg.collections?.main?.path, "collections/returnvape.jsonl");
139
+ assert.ok(fs.existsSync(path.join(vaultPath, "collections", "returnvape.jsonl")));
140
+ });
127
141
  it("POST /vm/init is idempotent", async () => {
128
142
  await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath }, "test-token");
129
143
  const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath }, "test-token");
@@ -3,8 +3,8 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { afterEach, describe, it } from "node:test";
6
- import { buildPluginInstallGuidance, detectVaultFromCwd, getMissingOnboardingPlugins, setupWizard, } from "../src/settings-ui.js";
7
6
  import { modalUrl } from "../src/modal-config.js";
7
+ import { buildPluginInstallGuidance, detectVaultFromCwd, getMissingOnboardingPlugins, setupWizard, } from "../src/settings-ui.js";
8
8
  import { getGlobalConfigPath } from "../src/utils.js";
9
9
  const savedHome = process.env.HOME;
10
10
  const mkTmpDir = (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix));
@@ -52,8 +52,9 @@ describe("setupWizard plugin onboarding helpers", () => {
52
52
  it("builds manual guidance with CLI commands and Obsidian URIs", () => {
53
53
  const vault = mkTmpDir("pvm-plugin-guide-");
54
54
  const guidance = buildPluginInstallGuidance(getMissingOnboardingPlugins(vault));
55
+ assert.match(guidance, /obsidian plugin:install id=obsidian42-brat enable/);
55
56
  assert.match(guidance, /obsidian plugin:install id=actions-uri enable/);
56
- assert.match(guidance, /obsidian:\/\/show-plugin\?id=obsidian-pi-vault-mind/);
57
+ assert.match(guidance, /BRAT.*kylebrodeur\/obsidian-pi-vault-mind/);
57
58
  fs.rmSync(vault, { recursive: true, force: true });
58
59
  });
59
60
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vault-mind",
3
- "version": "0.8.8",
3
+ "version": "0.9.1",
4
4
  "description": "Passive Obsidian vault extension for pi. Watches @agent markers, dispatches forked subagents (Miner, Broadcaster, Heavy-Lifter), stores in LanceDB with vector + FTS + graph. Multi-agent 'Drop & Forget' workflow for the pi agent ecosystem.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,6 +10,7 @@
10
10
  "agents",
11
11
  "skills",
12
12
  "scripts",
13
+ "pi-vault-mind.config.example.json",
13
14
  "CHANGELOG.md",
14
15
  "README.md",
15
16
  "LICENSE"
@@ -0,0 +1,43 @@
1
+ {
2
+ "version": 2,
3
+ "collections": {
4
+ "main": {
5
+ "path": "collections/main.jsonl",
6
+ "schema": ["id", "domain", "source", "fact", "tag", "artifact"],
7
+ "dedupField": "fact"
8
+ },
9
+ "pending": {
10
+ "path": "collections/pending.jsonl",
11
+ "schema": "main"
12
+ },
13
+ "context_events": {
14
+ "path": "collections/context_events.jsonl",
15
+ "schema": ["id", "type", "session_entry_id", "content", "timestamp", "tags"],
16
+ "dedupField": "id"
17
+ }
18
+ },
19
+ "injectors": [],
20
+ "vaultMind": {
21
+ "dataDir": ".lancedb",
22
+ "embedding": {
23
+ "localUrl": "http://127.0.0.1:11434",
24
+ "useTransformers": true
25
+ },
26
+ "graph": {
27
+ "enabled": true,
28
+ "canvasSync": false
29
+ },
30
+ "ftsEnabled": true,
31
+ "httpPort": 11435,
32
+ "autoIndex": false
33
+ },
34
+ "extensionCompatibility": {
35
+ "pi-context": {
36
+ "enabled": true,
37
+ "tagPatterns": [],
38
+ "enhanceInjectors": false,
39
+ "autoEnableAcm": true,
40
+ "indexContextEvents": true
41
+ }
42
+ }
43
+ }