pi-vault-mind 0.8.5 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,8 +84,8 @@ obsidian plugin:install id=obsidian-git enable
84
84
  obsidian plugin:install id=obsidian-breadcrumbs enable
85
85
  obsidian plugin:install id=graph-analysis enable
86
86
  obsidian plugin:install id=actions-uri enable
87
- obsidian plugin:install id=shellcommands enable
88
- ```
87
+ obsidian plugin:install id=obsidian42-brat enable
88
+ obsidian plugin:install id=obsidian-pi-vault-mind enable
89
89
 
90
90
  Full walkthrough: see [`docs/integrations/OBSIDIAN_SETUP.md`](docs/integrations/OBSIDIAN_SETUP.md).
91
91
 
@@ -97,7 +97,7 @@ Full walkthrough: see [`docs/integrations/OBSIDIAN_SETUP.md`](docs/integrations/
97
97
  | **Breadcrumbs** | `obsidian-breadcrumbs` | Parses typed edges (`agent:related-to`, `agent:derived-from`) in YAML frontmatter. The recommended way to display the agent-extracted knowledge graph in Obsidian's UI. | Highly recommended |
98
98
  | **Graph Analysis** | `graph-analysis` | Co-citation discovery, Jaccard similarity on the native Obsidian graph. Surfaces "always cited together but not yet linked" notes — a key agent discovery signal. | Highly recommended |
99
99
  | **Actions URI** | `actions-uri` | `x-callback-url` endpoints so the Manager agent can trigger Obsidian UI commands (open notes, run commands) from pi. | Recommended |
100
- | **obsidian-shellcommands** | `shellcommands` | Bridge from Obsidian events (e.g., file save) to pi agent workflows. Configure a `curl` to `http://127.0.0.1:11435/vault-mind/scan` on file save. | Recommended |
100
+ | **Vault Mind plugin** | `obsidian-pi-vault-mind` | Native setup/status/chat UI and HTTP bridge integration. | Recommended |
101
101
 
102
102
  ### Recommended: `notesmd-cli` (headless alternative)
103
103
 
@@ -52,10 +52,11 @@ const VM_USAGE = [
52
52
  " /vm watcher cancel <id> Cancel a pending agent job",
53
53
  " /vm server status Show HTTP server status + port",
54
54
  " /vm personalize Analyze vault and suggest personality tuning for system.md + AGENTS.md",
55
- " /vm setup Interactive global config wizard",
55
+ " /vm setup Onboarding flow: config + personalize (auto-uses cwd when it's a vault)",
56
56
  " /vm setup --vault <path> CLI: set vault path",
57
57
  " /vm setup --remoteUrl <url> CLI: set remote embedding URL",
58
58
  " /vm setup --localUrl <url> CLI: set local embedding URL",
59
+ " /vm setup --workspace <slug> CLI: derive Modal URL + set workspace",
59
60
  " /vm token Show API token source and masked value",
60
61
  " /vm token show Show API token source and masked value",
61
62
  " /vm token rotate Generate + persist a new API token (old token invalidated)",
@@ -237,9 +238,6 @@ const handleInit = async (_args, ctx, pi) => {
237
238
  ],
238
239
  vaultMind: {
239
240
  dataDir: ".lancedb",
240
- embedding: {
241
- model: "embeddinggemma",
242
- },
243
241
  ftsEnabled: true,
244
242
  graph: { enabled: true, canvasSync: false },
245
243
  },
@@ -289,22 +287,21 @@ const handleInit = async (_args, ctx, pi) => {
289
287
  msg.push("", '💡 Tip: Tell the agent "Remember: [fact]" to auto-append to the main collection!');
290
288
  if (ctx.hasUI) {
291
289
  ctx.ui.notify(msg.join("\n"), "info");
292
- if (hasPiContextTools(pi)) {
293
- const enable = await ctx.ui.confirm("Enable pi-context integration?", "pi-context tools were detected. Would you like to enable the integration now to capture session context?");
294
- if (enable) {
295
- const existing = JSON.parse(fs.readFileSync(cfgDest, "utf-8"));
296
- existing.extensionCompatibility = existing.extensionCompatibility || {};
297
- existing.extensionCompatibility["pi-context"] = {
298
- tagPatterns: [],
299
- enhanceInjectors: false,
300
- autoEnableAcm: true,
301
- indexContextEvents: true,
302
- ...existing.extensionCompatibility["pi-context"],
303
- enabled: true,
304
- };
305
- fs.writeFileSync(cfgDest, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
306
- ctx.ui.notify("✅ pi-context integration ENABLED in config", "info");
307
- }
290
+ }
291
+ if (hasPiContextTools(pi)) {
292
+ const existing = JSON.parse(fs.readFileSync(cfgDest, "utf-8"));
293
+ existing.extensionCompatibility = existing.extensionCompatibility || {};
294
+ existing.extensionCompatibility["pi-context"] = {
295
+ tagPatterns: [],
296
+ enhanceInjectors: false,
297
+ autoEnableAcm: true,
298
+ indexContextEvents: true,
299
+ ...existing.extensionCompatibility["pi-context"],
300
+ enabled: true,
301
+ };
302
+ fs.writeFileSync(cfgDest, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
303
+ if (ctx.hasUI) {
304
+ ctx.ui.notify("✅ pi-context automation enabled (auto-ACM + event indexing)", "info");
308
305
  }
309
306
  }
310
307
  };
@@ -1360,7 +1357,7 @@ const handleWatcher = async (args, ctx, pi) => {
1360
1357
  }
1361
1358
  };
1362
1359
  // ── Setup ────────────────────────────────────────────────────────────────────
1363
- const handleSetup = async (args, ctx) => {
1360
+ const handleSetup = async (args, ctx, pi) => {
1364
1361
  // Parse optional CLI-style args: --vault <path> --remoteUrl <url> --localUrl <url> --model <name> --workspace <name>
1365
1362
  const cliArgs = {};
1366
1363
  const parts = args.trim().split(/\s+--/);
@@ -1382,7 +1379,13 @@ const handleSetup = async (args, ctx) => {
1382
1379
  }
1383
1380
  const hasCliArgs = cliArgs.vault || cliArgs.remoteUrl || cliArgs.localUrl || cliArgs.model || cliArgs.workspace;
1384
1381
  await setupWizard(ctx, hasCliArgs ? cliArgs : undefined);
1385
- ctx.ui.notify("Vault is configured. Run /vm personalize to tune the agent personality for this vault.", "info");
1382
+ if (ctx.hasUI) {
1383
+ ctx.ui.notify("Vault configured. Running personalization as part of onboarding...", "info");
1384
+ await runPersonalize(ctx, pi);
1385
+ }
1386
+ else {
1387
+ ctx.ui.notify("Vault is configured. Run /vm personalize in a TUI session to tune agent personality.", "info");
1388
+ }
1386
1389
  };
1387
1390
  // ── /vm intake ─────────────────────────────────────────────────────────────
1388
1391
  const handleIntake = async (args, ctx) => {
@@ -1671,7 +1674,7 @@ export const registerCommands = (pi) => {
1671
1674
  case "personalize":
1672
1675
  return runPersonalize(ctx, pi);
1673
1676
  case "setup":
1674
- return handleSetup(rest, ctx);
1677
+ return handleSetup(rest, ctx, pi);
1675
1678
  case "token":
1676
1679
  return handleToken(rest, ctx);
1677
1680
  default:
@@ -51,12 +51,8 @@ export const handleBeforeAgentStart = async (pi, event, ctx) => {
51
51
  }
52
52
  // 2. pi-context integration: auto-ACM and tag-based triggers
53
53
  const piContextCfg = getPiContextConfig(cfg);
54
- if (!piContextCfg.enabled) {
55
- // pi-context explicitly disabled — nothing to do
56
- }
57
- else if (!hasPiContextTools(pi)) {
58
- // pi-context enabled in config but tools not available — log once per turn_start
59
- console.warn("[pi-vault-mind] pi-context enabled but tools not found. Install pi-context extension.");
54
+ if (!piContextCfg.enabled || !hasPiContextTools(pi)) {
55
+ // pi-context is disabled or unavailable skip integration silently.
60
56
  }
61
57
  else {
62
58
  if (piContextCfg.autoEnableAcm) {
@@ -11,8 +11,8 @@
11
11
  * Binds 127.0.0.1 only — no network exposure. Port defaults to 11435,
12
12
  * configurable via vaultMind.httpPort in pi-vault-mind.config.json.
13
13
  *
14
- * Used by the obsidian-shellcommands plugin to notify pi of file saves,
15
- * replacing fs.watch reliance with explicit push notifications.
14
+ * Used by the Vault Mind Obsidian plugin and local automation scripts to notify
15
+ * pi of file saves via explicit push notifications.
16
16
  *
17
17
  * New Stage-1 routes (auth-gated by `PVM_API_TOKEN`):
18
18
  * GET /vm/status, POST /vm/init, POST /vm/setup, POST /server/start,
@@ -11,8 +11,8 @@
11
11
  * Binds 127.0.0.1 only — no network exposure. Port defaults to 11435,
12
12
  * configurable via vaultMind.httpPort in pi-vault-mind.config.json.
13
13
  *
14
- * Used by the obsidian-shellcommands plugin to notify pi of file saves,
15
- * replacing fs.watch reliance with explicit push notifications.
14
+ * Used by the Vault Mind Obsidian plugin and local automation scripts to notify
15
+ * pi of file saves via explicit push notifications.
16
16
  *
17
17
  * New Stage-1 routes (auth-gated by `PVM_API_TOKEN`):
18
18
  * GET /vm/status, POST /vm/init, POST /vm/setup, POST /server/start,
@@ -1,4 +1,13 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export interface ObsidianPluginRequirement {
3
+ id: string;
4
+ label: string;
5
+ required: boolean;
6
+ reason: string;
7
+ }
8
+ export declare const getMissingOnboardingPlugins: (vaultPath: string) => ObsidianPluginRequirement[];
9
+ export declare const buildPluginInstallGuidance: (plugins: ObsidianPluginRequirement[]) => string;
10
+ export declare const detectVaultFromCwd: (cwd: string) => string | null;
2
11
  export declare const createCollectionWizard: (ctx: ExtensionContext) => Promise<void>;
3
12
  export declare const createInjectorWizard: (ctx: ExtensionContext) => Promise<void>;
4
13
  export declare const setupWizard: (ctx: ExtensionContext, cliArgs?: {
@@ -1,7 +1,149 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import { MODAL_TOKEN_ENV, createModalClient, modalUrl, resolveModalToken } from "./modal-config.js";
4
5
  import { collectionNames, findConfig, getGlobalConfigPath, loadConfig, shrinkHome, } from "./utils.js";
6
+ const ONBOARDING_PLUGIN_REQUIREMENTS = [
7
+ {
8
+ id: "obsidian-pi-vault-mind",
9
+ label: "Vault Mind plugin",
10
+ required: true,
11
+ reason: "native setup/status/chat UI and local bridge controls",
12
+ },
13
+ {
14
+ id: "actions-uri",
15
+ label: "Actions URI",
16
+ required: false,
17
+ reason: "deep-link automation and capture shortcuts",
18
+ },
19
+ {
20
+ id: "obsidian-git",
21
+ label: "Obsidian Git",
22
+ required: false,
23
+ reason: "vault backup and change history safety net",
24
+ },
25
+ ];
26
+ const readEnabledCommunityPlugins = (vaultPath) => {
27
+ const communityPluginsPath = path.join(vaultPath, ".obsidian", "community-plugins.json");
28
+ try {
29
+ const parsed = JSON.parse(fs.readFileSync(communityPluginsPath, "utf-8"));
30
+ if (!Array.isArray(parsed))
31
+ return new Set();
32
+ return new Set(parsed.filter((value) => typeof value === "string"));
33
+ }
34
+ catch {
35
+ return new Set();
36
+ }
37
+ };
38
+ export const getMissingOnboardingPlugins = (vaultPath) => {
39
+ const enabled = readEnabledCommunityPlugins(vaultPath);
40
+ return ONBOARDING_PLUGIN_REQUIREMENTS.filter((plugin) => !enabled.has(plugin.id));
41
+ };
42
+ const hasObsidianCli = () => {
43
+ try {
44
+ execFileSync("obsidian", ["help"], {
45
+ encoding: "utf-8",
46
+ stdio: ["ignore", "pipe", "pipe"],
47
+ });
48
+ return true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ };
54
+ const formatExecError = (err) => {
55
+ if (err && typeof err === "object" && "stderr" in err) {
56
+ const stderr = err.stderr;
57
+ if (typeof stderr === "string" && stderr.trim())
58
+ return stderr.trim();
59
+ if (stderr instanceof Buffer && stderr.length > 0)
60
+ return stderr.toString("utf-8").trim();
61
+ }
62
+ return err instanceof Error ? err.message : String(err);
63
+ };
64
+ const installObsidianPlugin = (pluginId) => {
65
+ try {
66
+ execFileSync("obsidian", ["plugin:install", `id=${pluginId}`, "enable"], {
67
+ encoding: "utf-8",
68
+ stdio: ["ignore", "pipe", "pipe"],
69
+ });
70
+ return { ok: true };
71
+ }
72
+ catch (err) {
73
+ return { ok: false, error: formatExecError(err) };
74
+ }
75
+ };
76
+ 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
+ ];
88
+ return lines.join("\n");
89
+ };
90
+ const runPluginOnboarding = async (ctx, vaultPath) => {
91
+ const missingPlugins = getMissingOnboardingPlugins(vaultPath);
92
+ if (missingPlugins.length === 0) {
93
+ ctx.ui.notify("Obsidian plugin check: required integration plugins already enabled.", "info");
94
+ return;
95
+ }
96
+ ctx.ui.notify([
97
+ "Missing Obsidian plugins detected:",
98
+ ...missingPlugins.map((plugin) => `- ${plugin.label} (${plugin.id})${plugin.required ? " [required]" : " [recommended]"} — ${plugin.reason}`),
99
+ ].join("\n"), "warning");
100
+ const cliAvailable = hasObsidianCli();
101
+ const options = cliAvailable
102
+ ? [
103
+ "Install missing plugins now (Obsidian CLI)",
104
+ "I'll install manually (show commands + URIs)",
105
+ "Skip plugin setup for now",
106
+ ]
107
+ : ["I'll install manually (show commands + URIs)", "Skip plugin setup for now"];
108
+ const choice = await ctx.ui.select("Obsidian plugin onboarding", options);
109
+ if (!choice || choice === "Skip plugin setup for now")
110
+ return;
111
+ if (choice === "Install missing plugins now (Obsidian CLI)") {
112
+ const installed = [];
113
+ const failed = [];
114
+ for (const plugin of missingPlugins) {
115
+ const result = installObsidianPlugin(plugin.id);
116
+ if (result.ok) {
117
+ installed.push(plugin.id);
118
+ }
119
+ else {
120
+ failed.push({ plugin, error: result.error || "Unknown error" });
121
+ }
122
+ }
123
+ if (installed.length > 0) {
124
+ ctx.ui.notify([`Installed plugins (${installed.length}):`, ...installed.map((id) => `- ${id}`)].join("\n"), "info");
125
+ }
126
+ if (failed.length > 0) {
127
+ ctx.ui.notify([
128
+ "Some plugin installs failed:",
129
+ ...failed.map(({ plugin, error }) => `- ${plugin.id}: ${error}`),
130
+ "",
131
+ buildPluginInstallGuidance(failed.map(({ plugin }) => plugin)),
132
+ ].join("\n"), "warning");
133
+ }
134
+ return;
135
+ }
136
+ ctx.ui.notify(buildPluginInstallGuidance(missingPlugins), "info");
137
+ };
138
+ export const detectVaultFromCwd = (cwd) => {
139
+ const obsidianDir = path.join(cwd, ".obsidian");
140
+ try {
141
+ return fs.statSync(obsidianDir).isDirectory() ? cwd : null;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ };
5
147
  export const createCollectionWizard = async (ctx) => {
6
148
  const { project: cfgPath } = findConfig(ctx.cwd);
7
149
  if (!cfgPath) {
@@ -84,6 +226,7 @@ export const createInjectorWizard = async (ctx) => {
84
226
  };
85
227
  export const setupWizard = async (ctx, cliArgs) => {
86
228
  const existingGlobal = fs.existsSync(getGlobalConfigPath());
229
+ const detectedVaultPath = detectVaultFromCwd(ctx.cwd);
87
230
  // ── Non-interactive / CLI mode ──────────────────────────────────────────
88
231
  if (cliArgs && (cliArgs.vault || cliArgs.remoteUrl || cliArgs.localUrl || cliArgs.workspace)) {
89
232
  const config = existingGlobal
@@ -92,8 +235,9 @@ export const setupWizard = async (ctx, cliArgs) => {
92
235
  config.vaultMind = config.vaultMind || {};
93
236
  config.vaultMind.embedding = config.vaultMind.embedding || {};
94
237
  config.vaultMind.vaults = config.vaultMind.vaults || {};
95
- if (cliArgs.vault) {
96
- config.vaultMind.vaults.default = { path: shrinkHome(cliArgs.vault), autoSync: true };
238
+ const effectiveVaultPath = cliArgs.vault || detectedVaultPath || undefined;
239
+ if (effectiveVaultPath) {
240
+ config.vaultMind.vaults.default = { path: shrinkHome(effectiveVaultPath), autoSync: true };
97
241
  }
98
242
  if (cliArgs.remoteUrl) {
99
243
  config.vaultMind.embedding.remoteUrl = cliArgs.remoteUrl;
@@ -110,16 +254,19 @@ export const setupWizard = async (ctx, cliArgs) => {
110
254
  if (cliArgs.workspace) {
111
255
  config.vaultMind.embedding.modal = config.vaultMind.embedding.modal || {};
112
256
  config.vaultMind.embedding.modal.workspace = cliArgs.workspace;
257
+ if (!cliArgs.remoteUrl) {
258
+ config.vaultMind.embedding.remoteUrl = modalUrl(cliArgs.workspace);
259
+ }
113
260
  }
114
261
  const dir = path.dirname(getGlobalConfigPath());
115
262
  if (!fs.existsSync(dir))
116
263
  fs.mkdirSync(dir, { recursive: true });
117
264
  fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
118
265
  const lines = ["✅ Global config written:", ` ${getGlobalConfigPath()}`, ""];
119
- if (cliArgs.vault)
120
- lines.push(` Vault: ${cliArgs.vault}`);
121
- if (cliArgs.remoteUrl)
122
- lines.push(` Remote URL: ${cliArgs.remoteUrl}`);
266
+ if (effectiveVaultPath)
267
+ lines.push(` Vault: ${effectiveVaultPath}`);
268
+ if (config.vaultMind.embedding.remoteUrl)
269
+ lines.push(` Remote URL: ${config.vaultMind.embedding.remoteUrl}`);
123
270
  if (cliArgs.localUrl)
124
271
  lines.push(` Local URL: ${cliArgs.localUrl}`);
125
272
  if (cliArgs.model)
@@ -128,6 +275,21 @@ export const setupWizard = async (ctx, cliArgs) => {
128
275
  lines.push(` Dim: ${cliArgs.dim}`);
129
276
  if (cliArgs.workspace)
130
277
  lines.push(` Modal workspace: ${cliArgs.workspace}`);
278
+ if (effectiveVaultPath) {
279
+ const obsidianDir = path.join(effectiveVaultPath, ".obsidian");
280
+ if (fs.existsSync(obsidianDir)) {
281
+ const missingPlugins = getMissingOnboardingPlugins(effectiveVaultPath);
282
+ if (missingPlugins.length === 0) {
283
+ lines.push("", "Obsidian plugin check: required integration plugins already enabled.");
284
+ }
285
+ else {
286
+ lines.push("", "Missing Obsidian plugins:", ...missingPlugins.map((plugin) => `- ${plugin.label} (${plugin.id})`), "", buildPluginInstallGuidance(missingPlugins));
287
+ }
288
+ }
289
+ else {
290
+ lines.push("", "Obsidian plugin check skipped: .obsidian/ not found.", "Open the vault in Obsidian once, then re-run /vm setup for plugin guidance.");
291
+ }
292
+ }
131
293
  ctx.ui.notify(lines.join("\n"), "info");
132
294
  return;
133
295
  }
@@ -152,7 +314,12 @@ export const setupWizard = async (ctx, cliArgs) => {
152
314
  }
153
315
  }
154
316
  // ── Step 1: Vault path ─────────────────────────────────────────────────
155
- const vaultPath = await ctx.ui.input("Obsidian vault path (absolute):", `/home/${process.env.USER || "you"}/Obsidian/`);
317
+ const autoVaultPath = cliArgs?.vault?.trim() || detectedVaultPath || "";
318
+ if (autoVaultPath) {
319
+ ctx.ui.notify(`Using current vault path: ${autoVaultPath}`, "info");
320
+ }
321
+ const vaultPath = autoVaultPath ||
322
+ (await ctx.ui.input("Obsidian vault path (absolute):", `/home/${process.env.USER || "you"}/Obsidian/`));
156
323
  if (!vaultPath) {
157
324
  ctx.ui.notify("Setup cancelled — vault path is required.", "warning");
158
325
  return;
@@ -167,29 +334,72 @@ export const setupWizard = async (ctx, cliArgs) => {
167
334
  return;
168
335
  }
169
336
  }
337
+ // ── Step 1.5: Obsidian plugin readiness ────────────────────────────────
338
+ await runPluginOnboarding(ctx, vaultPath);
170
339
  // ── Step 2: Embedding config ──────────────────────────────────────────
171
- const remoteUrl = await ctx.ui.input("Remote Embedding URL (e.g. https://.../v1, optional):", "");
340
+ let remoteUrl = await ctx.ui.input("Remote Embedding URL (e.g. https://.../v1, optional):", "");
172
341
  const localUrl = await ctx.ui.input("Local Embedding URL (e.g. http://127.0.0.1:11434/v1, optional):", "");
173
342
  let useTransformers = false;
343
+ let guidedModalWorkspace;
174
344
  if (!remoteUrl && !localUrl) {
175
- const useTF = await ctx.ui.confirm("Use Transformers", "No embedding URLs configured. Use local transformers (all-MiniLM-L6-v2, 384 dims, offline)?");
176
- if (!useTF) {
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") {
177
351
  ctx.ui.notify("Setup cancelled — at least one URL or transformers is required.", "warning");
178
352
  return;
179
353
  }
180
- useTransformers = true;
354
+ if (route === "Use local transformers (offline)") {
355
+ useTransformers = true;
356
+ }
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;
362
+ }
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
+ }
181
376
  }
182
- const model = await ctx.ui.input("Model name:", "embeddinggemma");
183
- if (!model) {
184
- ctx.ui.notify("Setup cancelled — model name is required.", "warning");
377
+ const model = (await ctx.ui.input("Embedding model key (optional):", ""))?.trim();
378
+ if (model === undefined) {
379
+ ctx.ui.notify("Setup cancelled.", "warning");
185
380
  return;
186
381
  }
187
382
  const dimStr = await ctx.ui.input("Dimensions (optional):", "");
188
383
  const dim = dimStr ? Number.parseInt(dimStr, 10) : undefined;
189
- let workspace;
190
- if (remoteUrl?.includes("modal.run")) {
384
+ let workspace = guidedModalWorkspace;
385
+ if (!workspace && remoteUrl?.includes("modal.run")) {
191
386
  workspace = await ctx.ui.input("Modal workspace (optional):", "");
192
387
  }
388
+ // ── Step 3: Deterministic runtime settings ──────────────────────────────
389
+ const enableContextAutomation = await ctx.ui.confirm("Context automation", "Enable pi-context integration with auto-/acm trigger and event indexing?");
390
+ const enableAutoStart = await ctx.ui.confirm("Vault auto-start", "Auto-start watcher/server for this vault when pi session starts?");
391
+ const apply = await ctx.ui.confirm("Apply setup", [
392
+ `Vault: ${vaultPath}`,
393
+ `Remote URL: ${remoteUrl || "none"}`,
394
+ `Local URL: ${localUrl || "none"}`,
395
+ `Model: ${model || "(auto/server default)"}`,
396
+ `Context automation: ${enableContextAutomation ? "enabled" : "disabled"}`,
397
+ `Vault auto-start: ${enableAutoStart ? "enabled" : "disabled"}`,
398
+ ].join("\n"));
399
+ if (!apply) {
400
+ ctx.ui.notify("Setup cancelled — no changes written.", "warning");
401
+ return;
402
+ }
193
403
  // ── Write config ────────────────────────────────────────────────────────
194
404
  const config = existingGlobal
195
405
  ? JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"))
@@ -201,7 +411,8 @@ export const setupWizard = async (ctx, cliArgs) => {
201
411
  config.vaultMind.embedding.remoteUrl = remoteUrl;
202
412
  if (localUrl)
203
413
  config.vaultMind.embedding.localUrl = localUrl;
204
- config.vaultMind.embedding.model = model;
414
+ if (model)
415
+ config.vaultMind.embedding.model = model;
205
416
  if (useTransformers)
206
417
  config.vaultMind.embedding.useTransformers = true;
207
418
  if (dim)
@@ -210,9 +421,22 @@ export const setupWizard = async (ctx, cliArgs) => {
210
421
  config.vaultMind.embedding.modal = config.vaultMind.embedding.modal || {};
211
422
  config.vaultMind.embedding.modal.workspace = workspace;
212
423
  }
213
- config.vaultMind.vaults.default = { path: shrinkHome(vaultPath), autoSync: true };
424
+ config.vaultMind.vaults.default = {
425
+ path: shrinkHome(vaultPath),
426
+ autoSync: true,
427
+ autoStart: enableAutoStart,
428
+ };
214
429
  config.vaultMind.graph = config.vaultMind.graph || { enabled: true, canvasSync: true };
215
430
  config.vaultMind.ftsEnabled = config.vaultMind.ftsEnabled !== false;
431
+ config.extensionCompatibility = config.extensionCompatibility || {};
432
+ config.extensionCompatibility["pi-context"] = {
433
+ tagPatterns: [],
434
+ enhanceInjectors: false,
435
+ autoEnableAcm: true,
436
+ indexContextEvents: true,
437
+ ...(config.extensionCompatibility["pi-context"] || {}),
438
+ enabled: enableContextAutomation,
439
+ };
216
440
  const dir = path.dirname(getGlobalConfigPath());
217
441
  if (!fs.existsSync(dir))
218
442
  fs.mkdirSync(dir, { recursive: true });
@@ -224,10 +448,19 @@ export const setupWizard = async (ctx, cliArgs) => {
224
448
  ` Vault: ${vaultPath}`,
225
449
  ` Remote: ${remoteUrl || "none"}`,
226
450
  ` Local: ${localUrl || "none"}`,
227
- ` Model: ${model}`,
451
+ ` Model: ${model || "(auto/server default)"}`,
452
+ ` Context automation: ${enableContextAutomation ? "enabled" : "disabled"}`,
453
+ ` Vault auto-start: ${enableAutoStart ? "enabled" : "disabled"}`,
228
454
  "",
229
455
  "Next: /vm watcher start (or restart pi for auto-start)",
230
456
  ].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
+ }
231
464
  };
232
465
  /**
233
466
  * Interactive Modal embedding configuration + "Test connection" action.
@@ -347,30 +580,68 @@ export const openSettingsDashboard = async (ctx) => {
347
580
  let running = true;
348
581
  while (running) {
349
582
  const choice = await ctx.ui.select("pi-vault-mind Settings", [
350
- "Manage Collections",
351
- "Manage Injectors",
352
- "Modal Embedding",
353
- "Extension Integrations",
583
+ "Collections (storage + search targets)",
584
+ "Injectors (regex capture rules)",
585
+ "Embedding + remote provider",
586
+ "Context automation (pi-context)",
587
+ "Config (view active values)",
588
+ "Obsidian plugin settings (what to configure)",
354
589
  "Exit",
355
590
  ]);
356
591
  if (!choice || choice === "Exit") {
357
592
  running = false;
358
593
  break;
359
594
  }
360
- if (choice === "Manage Collections") {
595
+ if (choice === "Collections (storage + search targets)") {
361
596
  await manageCollections(ctx, cfgPath);
362
597
  }
363
- else if (choice === "Manage Injectors") {
598
+ else if (choice === "Injectors (regex capture rules)") {
364
599
  await manageInjectors(ctx, cfgPath);
365
600
  }
366
- else if (choice === "Modal Embedding") {
601
+ else if (choice === "Embedding + remote provider") {
367
602
  await configureModalWizard(ctx);
368
603
  }
369
- else if (choice === "Extension Integrations") {
604
+ else if (choice === "Context automation (pi-context)") {
370
605
  await manageExtensions(ctx, cfgPath);
371
606
  }
607
+ else if (choice === "Config (view active values)") {
608
+ await showConfigSettings(ctx, cfgPath);
609
+ }
610
+ else if (choice === "Obsidian plugin settings (what to configure)") {
611
+ showObsidianSettingsHelp(ctx);
612
+ }
372
613
  }
373
614
  };
615
+ const showConfigSettings = async (ctx, cfgPath) => {
616
+ const merged = loadConfig(ctx.cwd);
617
+ const paths = findConfig(ctx.cwd);
618
+ const lines = [
619
+ "Config sources:",
620
+ `- Global: ${paths.global || "(none)"}`,
621
+ `- Project: ${paths.project || "(none)"}`,
622
+ "",
623
+ "Merged runtime config:",
624
+ JSON.stringify(merged.vaultMind, null, 2),
625
+ ];
626
+ ctx.ui.notify(lines.join("\n"), "info");
627
+ const projectRaw = fs.readFileSync(cfgPath, "utf-8");
628
+ ctx.ui.notify(["Project config JSON:", projectRaw].join("\n"), "info");
629
+ };
630
+ const showObsidianSettingsHelp = (ctx) => {
631
+ ctx.ui.notify([
632
+ "Obsidian plugin settings (Vault Mind):",
633
+ "- Host / Port",
634
+ "- Pi binary path",
635
+ "- Check extension on startup",
636
+ "- Include editor context",
637
+ "- Include file picker",
638
+ "- Include slash commands",
639
+ "- Folder layout: Inbox / Library / Presentations / Journal",
640
+ "- API token (Import from dotenv)",
641
+ "",
642
+ "Location: Obsidian → Settings → Community plugins → Vault Mind",
643
+ ].join("\n"), "info");
644
+ };
374
645
  const manageCollections = async (ctx, cfgPath) => {
375
646
  const existing = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
376
647
  const names = Object.keys(existing.collections || {});
@@ -422,22 +693,27 @@ const manageInjectors = async (ctx, cfgPath) => {
422
693
  const manageExtensions = async (ctx, cfgPath) => {
423
694
  const existing = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
424
695
  existing.extensionCompatibility = existing.extensionCompatibility || {};
425
- const piCtx = existing.extensionCompatibility["pi-context"] || {};
426
- const choice = await ctx.ui.select("pi-context Integration", [
427
- `Toggle Enabled (currently ${piCtx.enabled ? "ON" : "OFF"})`,
428
- `Toggle Auto-ACM (currently ${piCtx.autoEnableAcm !== false ? "ON" : "OFF"})`,
429
- `Toggle Event Indexing (currently ${piCtx.indexContextEvents !== false ? "ON" : "OFF"})`,
696
+ const piCtx = {
697
+ enabled: true,
698
+ autoEnableAcm: true,
699
+ indexContextEvents: true,
700
+ ...(existing.extensionCompatibility["pi-context"] || {}),
701
+ };
702
+ const choice = await ctx.ui.select("Context automation (pi-context)", [
703
+ `Toggle integration (currently ${piCtx.enabled ? "ON" : "OFF"})`,
704
+ `Toggle auto-/acm trigger (currently ${piCtx.autoEnableAcm !== false ? "ON" : "OFF"})`,
705
+ `Toggle context event indexing (currently ${piCtx.indexContextEvents !== false ? "ON" : "OFF"})`,
430
706
  "Back",
431
707
  ]);
432
708
  if (!choice || choice === "Back")
433
709
  return;
434
- if (choice.startsWith("Toggle Enabled")) {
710
+ if (choice.startsWith("Toggle integration")) {
435
711
  piCtx.enabled = !piCtx.enabled;
436
712
  }
437
- else if (choice.startsWith("Toggle Auto-ACM")) {
713
+ else if (choice.startsWith("Toggle auto-/acm trigger")) {
438
714
  piCtx.autoEnableAcm = piCtx.autoEnableAcm === false;
439
715
  }
440
- else if (choice.startsWith("Toggle Event Indexing")) {
716
+ else if (choice.startsWith("Toggle context event indexing")) {
441
717
  piCtx.indexContextEvents = piCtx.indexContextEvents === false;
442
718
  }
443
719
  existing.extensionCompatibility["pi-context"] = piCtx;
package/dist/src/types.js CHANGED
@@ -32,7 +32,6 @@ export const DEFAULT_CONFIG = {
32
32
  },
33
33
  embedding: {
34
34
  useTransformers: true,
35
- model: "embeddinggemma",
36
35
  localUrl: "http://127.0.0.1:11434",
37
36
  },
38
37
  graph: {
@@ -45,7 +44,7 @@ export const DEFAULT_CONFIG = {
45
44
  },
46
45
  extensionCompatibility: {
47
46
  "pi-context": {
48
- enabled: false,
47
+ enabled: true,
49
48
  tagPatterns: [],
50
49
  enhanceInjectors: false,
51
50
  autoEnableAcm: true,
package/dist/src/utils.js CHANGED
@@ -7,7 +7,7 @@ export const CONFIG_FILES = ["pi-vault-mind.config.json", ".pi/vault-mind.config
7
7
  export const getGlobalConfigDir = () => path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent");
8
8
  /** Lazy — recomputes HOME each call so tests can override it. */
9
9
  export const getGlobalConfigPath = () => path.join(getGlobalConfigDir(), "vault-mind.config.json");
10
- export const EXT_ROOT = path.join(import.meta.dirname, "..");
10
+ export const EXT_ROOT = path.join(import.meta.dirname, "..", "..");
11
11
  /** Expand a leading `~` to the user's home directory. */
12
12
  export const expandHome = (p) => {
13
13
  if (p.startsWith("~/") || p === "~") {
@@ -123,7 +123,7 @@ export const hasPiContextTools = (pi) => {
123
123
  };
124
124
  export const isPiContextEnabled = (cfg) => cfg.extensionCompatibility?.["pi-context"]?.enabled === true;
125
125
  export const getPiContextConfig = (cfg) => ({
126
- enabled: false,
126
+ enabled: true,
127
127
  tagPatterns: [],
128
128
  enhanceInjectors: false,
129
129
  autoEnableAcm: true,
@@ -99,7 +99,7 @@ describe("pi-vault-mind", () => {
99
99
  assert.equal(types.DEFAULT_CONFIG.version, 2);
100
100
  assert.ok(types.DEFAULT_CONFIG.collections, "collections should exist");
101
101
  assert.ok(types.DEFAULT_CONFIG.collections.main, "should have main collection");
102
- assert.equal(types.DEFAULT_CONFIG.vaultMind.embedding.model, "embeddinggemma");
102
+ assert.equal(types.DEFAULT_CONFIG.vaultMind.embedding.model, undefined);
103
103
  }
104
104
  finally {
105
105
  process.chdir(cwd);
@@ -0,0 +1,91 @@
1
+ import { strict as assert } from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, describe, it } from "node:test";
6
+ import { buildPluginInstallGuidance, detectVaultFromCwd, getMissingOnboardingPlugins, setupWizard, } from "../src/settings-ui.js";
7
+ import { modalUrl } from "../src/modal-config.js";
8
+ import { getGlobalConfigPath } from "../src/utils.js";
9
+ const savedHome = process.env.HOME;
10
+ const mkTmpDir = (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix));
11
+ const makeCtx = (cwd) => ({
12
+ cwd,
13
+ hasUI: false,
14
+ ui: {
15
+ notify: () => undefined,
16
+ },
17
+ });
18
+ const expandMaybeHome = (p, home) => p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
19
+ afterEach(() => {
20
+ if (savedHome === undefined) {
21
+ Reflect.deleteProperty(process.env, "HOME");
22
+ }
23
+ else {
24
+ process.env.HOME = savedHome;
25
+ }
26
+ });
27
+ describe("settings-ui vault detection", () => {
28
+ it("detectVaultFromCwd returns cwd when .obsidian exists", () => {
29
+ const vault = mkTmpDir("pvm-vault-detect-");
30
+ fs.mkdirSync(path.join(vault, ".obsidian"), { recursive: true });
31
+ assert.equal(detectVaultFromCwd(vault), vault);
32
+ fs.rmSync(vault, { recursive: true, force: true });
33
+ });
34
+ it("detectVaultFromCwd returns null when .obsidian missing", () => {
35
+ const dir = mkTmpDir("pvm-nonvault-detect-");
36
+ assert.equal(detectVaultFromCwd(dir), null);
37
+ fs.rmSync(dir, { recursive: true, force: true });
38
+ });
39
+ });
40
+ describe("setupWizard plugin onboarding helpers", () => {
41
+ it("reports only missing community plugins", () => {
42
+ const vault = mkTmpDir("pvm-plugin-check-");
43
+ const obsidianDir = path.join(vault, ".obsidian");
44
+ fs.mkdirSync(obsidianDir, { recursive: true });
45
+ fs.writeFileSync(path.join(obsidianDir, "community-plugins.json"), JSON.stringify(["obsidian-pi-vault-mind"], null, 2), "utf-8");
46
+ const missing = getMissingOnboardingPlugins(vault).map((plugin) => plugin.id);
47
+ assert.equal(missing.includes("obsidian-pi-vault-mind"), false);
48
+ assert.equal(missing.includes("actions-uri"), true);
49
+ assert.equal(missing.includes("obsidian-git"), true);
50
+ fs.rmSync(vault, { recursive: true, force: true });
51
+ });
52
+ it("builds manual guidance with CLI commands and Obsidian URIs", () => {
53
+ const vault = mkTmpDir("pvm-plugin-guide-");
54
+ const guidance = buildPluginInstallGuidance(getMissingOnboardingPlugins(vault));
55
+ assert.match(guidance, /obsidian plugin:install id=actions-uri enable/);
56
+ assert.match(guidance, /obsidian:\/\/show-plugin\?id=obsidian-pi-vault-mind/);
57
+ fs.rmSync(vault, { recursive: true, force: true });
58
+ });
59
+ });
60
+ describe("setupWizard CLI defaults", () => {
61
+ it("auto-uses cwd as vault when cwd is an Obsidian vault", async () => {
62
+ const home = mkTmpDir("pvm-home-");
63
+ const vault = path.join(home, "Vault");
64
+ fs.mkdirSync(path.join(vault, ".obsidian"), { recursive: true });
65
+ process.env.HOME = home;
66
+ await setupWizard(makeCtx(vault), {
67
+ remoteUrl: "https://example.com/v1",
68
+ model: "embeddinggemma",
69
+ });
70
+ const cfg = JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"));
71
+ const stored = cfg.vaultMind?.vaults?.default?.path;
72
+ assert.ok(stored, "default vault path should be written");
73
+ assert.equal(expandMaybeHome(stored, home), vault);
74
+ assert.equal(cfg.vaultMind?.embedding?.remoteUrl, "https://example.com/v1");
75
+ fs.rmSync(home, { recursive: true, force: true });
76
+ });
77
+ it("derives remoteUrl from workspace in CLI mode", async () => {
78
+ const home = mkTmpDir("pvm-home-");
79
+ const vault = path.join(home, "Vault");
80
+ fs.mkdirSync(path.join(vault, ".obsidian"), { recursive: true });
81
+ process.env.HOME = home;
82
+ await setupWizard(makeCtx(vault), {
83
+ workspace: "acme-workspace",
84
+ model: "embeddinggemma",
85
+ });
86
+ const cfg = JSON.parse(fs.readFileSync(getGlobalConfigPath(), "utf-8"));
87
+ assert.equal(cfg.vaultMind?.embedding?.modal?.workspace, "acme-workspace");
88
+ assert.equal(cfg.vaultMind?.embedding?.remoteUrl, modalUrl("acme-workspace"));
89
+ fs.rmSync(home, { recursive: true, force: true });
90
+ });
91
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vault-mind",
3
- "version": "0.8.5",
3
+ "version": "0.8.8",
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",
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # setup-vault-pi.sh — Bootstrap a vault-local pi agent config.
3
+ #
4
+ # Run from the vault root. Creates .pi/agent/ with pi-vault-mind and pi-context
5
+ # installed locally, isolated from your global ~/.pi/agent/.
6
+ #
7
+ # Usage:
8
+ # cd /path/to/vault
9
+ # bash scripts/setup-vault-pi.sh
10
+ #
11
+ # After setup, start pi with:
12
+ # PI_CODING_AGENT_DIR="$(pwd)/.pi/agent" pi
13
+
14
+ set -euo pipefail
15
+
16
+ VAULT_ROOT="${1:-$(pwd)}"
17
+ PI_DIR="${VAULT_ROOT}/.pi/agent"
18
+
19
+ echo "==> Setting up vault-local pi config at ${PI_DIR}"
20
+
21
+ mkdir -p "${PI_DIR}"
22
+
23
+ # Install pi-vault-mind and pi-context into the vault-local agent dir
24
+ PI_CODING_AGENT_DIR="${PI_DIR}" pi install npm:pi-vault-mind
25
+ PI_CODING_AGENT_DIR="${PI_DIR}" pi install npm:pi-context
26
+
27
+ echo ""
28
+ echo "==> Done. Start pi for this vault with:"
29
+ echo " PI_CODING_AGENT_DIR=\"${PI_DIR}\" pi"
30
+ echo ""
31
+ echo " Or from Obsidian Shell Commands:"
32
+ echo " PI_CODING_AGENT_DIR=${PI_DIR} pi"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # vault-pi.sh — Start pi for a vault, ensuring only one instance runs.
3
+ #
4
+ # Usage:
5
+ # bash scripts/vault-pi.sh /path/to/vault
6
+ #
7
+ # Obsidian Shell Commands integration:
8
+ # Command: bash /path/to/pi-vault-mind/scripts/vault-pi.sh {{vault_path}}
9
+ #
10
+ # The script:
11
+ # 1. Checks for an existing PID file at <vault>/.pi/agent/daemon.pid
12
+ # 2. If the PID is alive → exits silently (already running)
13
+ # 3. If stale/dead → cleans up and starts fresh
14
+ # 4. Opens a new terminal window with pi using the vault-local config dir
15
+
16
+ set -euo pipefail
17
+
18
+ VAULT="${1:?Usage: vault-pi.sh <vault-path>}"
19
+ PI_DIR="${VAULT}/.pi/agent"
20
+ PID_FILE="${PI_DIR}/daemon.pid"
21
+
22
+ # Ensure the vault-local agent dir exists
23
+ mkdir -p "${PI_DIR}"
24
+
25
+ # Check for existing process
26
+ if [ -f "${PID_FILE}" ]; then
27
+ PID=$(cat "${PID_FILE}")
28
+ if kill -0 "${PID}" 2>/dev/null; then
29
+ echo "[vault-pi] pi already running for ${VAULT} (PID ${PID})"
30
+ exit 0
31
+ fi
32
+ echo "[vault-pi] Stale PID file (${PID} is dead), cleaning up"
33
+ rm -f "${PID_FILE}"
34
+ fi
35
+
36
+ # Start pi in a new terminal window.
37
+ # macOS: open -a Terminal or open -a iTerm
38
+ # The trap writes the PID and cleans up on exit.
39
+ PI_CMD="PI_CODING_AGENT_DIR='${PI_DIR}' pi; rm -f '${PID_FILE}'"
40
+
41
+ if [ -d "/Applications/iTerm.app" ]; then
42
+ open -a iTerm --args --detach bash -c "echo \$\$ > '${PID_FILE}'; cd '${VAULT}'; ${PI_CMD}"
43
+ elif [ -d "/Applications/Utilities/Terminal.app" ]; then
44
+ osascript -e "tell application \"Terminal\" to do script \"echo \$\$ > '${PID_FILE}'; cd '${VAULT}'; ${PI_CMD}\""
45
+ else
46
+ echo "[vault-pi] No supported terminal found (iTerm or Terminal.app)"
47
+ exit 1
48
+ fi
49
+
50
+ echo "[vault-pi] Started pi for ${VAULT}"
@@ -83,15 +83,15 @@ Uses `all-MiniLM-L6-v2` (384 dimensions). Downloads ONNX model on first use.
83
83
  ## Obsidian Bridge
84
84
 
85
85
  pi-vault-mind runs an HTTP server on `http://127.0.0.1:11435` (configurable
86
- via `vaultMind.httpPort`). The `obsidian-shellcommands` plugin can POST file-save
87
- events to `/vault-mind/scan` for explicit, reliable file-watching instead of
88
- relying solely on `fs.watch`.
86
+ via `vaultMind.httpPort`). The Vault Mind plugin (or any local automation)
87
+ can POST file-save events to `/vault-mind/scan` for explicit, reliable
88
+ file-watching instead of relying solely on `fs.watch`.
89
89
 
90
90
  ```bash
91
- # Shell command config in Obsidian:
91
+ # Manual local-bridge trigger:
92
92
  curl -s -X POST http://127.0.0.1:11435/vault-mind/scan \
93
93
  -H "Content-Type: application/json" \
94
- -d '{"file":"{{file_path:absolute}}"}'
94
+ -d '{"file":"/absolute/path/to/note.md"}'
95
95
  ```
96
96
 
97
97
  Endpoints:
@@ -73,7 +73,7 @@ obsidian plugin:install id=obsidian-git enable
73
73
  obsidian plugin:install id=obsidian-breadcrumbs enable
74
74
  obsidian plugin:install id=graph-analysis enable
75
75
  obsidian plugin:install id=actions-uri enable
76
- obsidian plugin:install id=shellcommands enable
76
+ obsidian plugin:install id=obsidian-pi-vault-mind enable
77
77
  obsidian plugin:install id=obsidian42-brat enable # for beta plugins
78
78
 
79
79
  # Verify
@@ -103,4 +103,4 @@ Community plugins to install:
103
103
  - `Breadcrumbs` — typed-edge graph
104
104
  - `Graph Analysis` — co-citation discovery
105
105
  - `Actions URI` — cross-app automation
106
- - `obsidian-shellcommands` — bridge to pi
106
+ - `Vault Mind plugin` — native setup/status/chat + local bridge trigger