pi-vault-mind 0.16.4 → 0.16.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.6 / 0.6.8 — 2026-07-18
4
+
5
+ ### Fixed
6
+
7
+ - **First-run personalization handoff.** Configured vaults now remain on a dedicated personalization card until `/vm personalize` durably completes, including the valid all-suggestions-rejected path. The chat feed and composer are revealed only after the persisted completion status is observed.
8
+ - **Live Pi model selection.** The first-run card now loads the active model and catalog from typed Pi RPC state, preserves provider and model ID for selection, and starts Pi only when a configured panel refresh needs that catalog.
9
+
10
+ ### Tests
11
+
12
+ - Added regression coverage for first-run gating, model selection ordering, delayed personalization completion, and demand-driven Pi model loading.
13
+
14
+ ## 0.16.5 / 0.6.7 — 2026-07-18
15
+
16
+ ### Fixed
17
+
18
+ - **Vault-scoped runtime ownership.** The plugin serializes startup per vault, adopts only a healthy bridge for the same canonical vault, and uses the configured `vaultMind.files.serverState` discovery path. This prevents duplicate Pi runtime launches across plugin instances and reloads.
19
+ - **Post-setup chat handoff.** A successful setup refreshes the shared panel controller before revealing an existing Vault Mind leaf. The panel remains available with its retained status if that refresh transiently fails.
20
+ - **Setup REST identity.** `GET /vm/status` now returns the configured vault identity so the plugin can verify runtime ownership.
21
+
22
+ ### Tests
23
+
24
+ - Added focused coverage for same-vault startup serialization, custom runtime discovery paths, setup-to-chat refresh ordering, and mount-on-refresh-failure behavior.
25
+
3
26
  ## 0.16.4 / 0.6.6 — 2026-07-17
4
27
 
5
28
  ### Fixed
@@ -1760,7 +1760,8 @@ export const registerCommands = (pi) => {
1760
1760
  case "remote":
1761
1761
  return handleModal(rest, ctx, pi);
1762
1762
  case "personalize":
1763
- return runPersonalize(ctx, pi);
1763
+ await runPersonalize(ctx, pi);
1764
+ return;
1764
1765
  case "setup":
1765
1766
  return handleSetup(rest, ctx, pi);
1766
1767
  case "token":
@@ -44,4 +44,6 @@ export declare const presentAndApplyDiff: (suggestions: SuggestionResult, ctx: E
44
44
  rejected: string[];
45
45
  }>;
46
46
  /** Orchestrate /vm personalize: analyze, suggest, confirm, apply. */
47
- export declare const runPersonalize: (ctx: ExtensionContext, pi: ExtensionAPI) => Promise<void>;
47
+ export declare const runPersonalize: (ctx: ExtensionContext, pi: ExtensionAPI) => Promise<{
48
+ completed: boolean;
49
+ }>;
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
4
4
  import { getStatus } from "./lance.js";
5
- import { expandHome, loadConfig, resolveAgentDir } from "./utils.js";
5
+ import { expandHome, getPersonalizedMarkerPath, loadConfig, resolveAgentDir } from "./utils.js";
6
6
  /** Resolve the vault path: configured default vault wins, otherwise ctx.cwd. */
7
7
  export const resolveVaultPath = (ctx) => {
8
8
  const cfg = loadConfig(ctx.cwd);
@@ -359,8 +359,14 @@ export const runPersonalize = async (ctx, pi) => {
359
359
  try {
360
360
  const suggestions = await generateSuggestions(pi, profile, currentConfig);
361
361
  await presentAndApplyDiff(suggestions, ctx, currentConfig);
362
+ const markerPath = getPersonalizedMarkerPath(vaultPath);
363
+ await fs.promises.mkdir(path.dirname(markerPath), { recursive: true });
364
+ await fs.promises.writeFile(markerPath, JSON.stringify({ completed: true, completedAt: new Date().toISOString() }, null, 2));
365
+ return { completed: true };
362
366
  }
363
367
  catch (err) {
364
- ctx.ui.notify(`Personalization failed: ${err.message}`, "error");
368
+ const message = err instanceof Error ? err.message : String(err);
369
+ ctx.ui.notify(`Personalization failed: ${message}`, "error");
370
+ return { completed: false };
365
371
  }
366
372
  };
@@ -45,7 +45,7 @@ import { readModelProviders } from "./models.js";
45
45
  import { scaffoldVaultConfig } from "./scaffold.js";
46
46
  import { archiveSession, deleteSession, exportSession, getSession, listSessions, renameSession, resolveSessionsDir, searchSessions, } from "./session-search.js";
47
47
  import { listAllTools } from "./tool-catalog.js";
48
- import { ensureDir, expandHome, findConfig, getConfigPath, loadConfig, resolveVaultFile, resolveVaultMindPaths, shrinkHome, } from "./utils.js";
48
+ import { ensureDir, expandHome, findConfig, getConfigPath, isPersonalized, loadConfig, resolveVaultFile, resolveVaultMindPaths, shrinkHome, } from "./utils.js";
49
49
  import { classifySearchMode, normalizeGraphResult, searchVaultNative } from "./vault-tools.js";
50
50
  import { createManualDispatch, processQueue, scanFile, startWatcher, stopWatcher, } from "./watcher.js";
51
51
  export function createServerState(port = 11435) {
@@ -98,8 +98,13 @@ export function startServer(pi, serverState, watcherState, readinessCallback) {
98
98
  // Resolve the vault path from the loaded config's default vault, else cwd.
99
99
  const startupCfg = loadConfig(process.cwd());
100
100
  const defaultVault = startupCfg.vaultMind.vaults?.default?.path;
101
- const vaultPath = defaultVault && fs.existsSync(defaultVault) ? defaultVault : process.cwd();
102
- serverState.vaultPath = vaultPath;
101
+ const configuredVaultPath = defaultVault && fs.existsSync(defaultVault) ? defaultVault : process.cwd();
102
+ try {
103
+ serverState.vaultPath = fs.realpathSync.native(configuredVaultPath);
104
+ }
105
+ catch {
106
+ serverState.vaultPath = path.resolve(configuredVaultPath);
107
+ }
103
108
  serverState.server = http.createServer(async (req, res) => {
104
109
  applyCorsPolicy(req.headers.origin, serverState.port, res);
105
110
  res.setHeader("Content-Type", "application/json");
@@ -750,15 +755,25 @@ export function startServer(pi, serverState, watcherState, readinessCallback) {
750
755
  res.end(JSON.stringify({ error: message }));
751
756
  }
752
757
  });
753
- let retriedEphemeral = false;
754
758
  serverState.server.on("error", (err) => {
755
- if (err.code === "EADDRINUSE" && !retriedEphemeral) {
756
- retriedEphemeral = true;
757
- console.warn(`[pi-vault-mind] Port ${serverState.port} in use; retrying on ephemeral port.`);
758
- serverState.server?.listen(0, "127.0.0.1");
759
- }
760
- else if (err.code === "EADDRINUSE") {
761
- console.warn(`[pi-vault-mind] Port ${serverState.port} in use. HTTP server not started.`);
759
+ if (err.code === "EADDRINUSE") {
760
+ console.warn(`[pi-vault-mind] Port ${serverState.port} in use; HTTP server not started.`);
761
+ if (serverState.unsubQueue) {
762
+ serverState.unsubQueue();
763
+ serverState.unsubQueue = undefined;
764
+ }
765
+ if (serverState.unsubToolBus) {
766
+ serverState.unsubToolBus();
767
+ serverState.unsubToolBus = undefined;
768
+ }
769
+ if (serverState.wss) {
770
+ serverState.wss.close();
771
+ serverState.wss = null;
772
+ }
773
+ if (serverState.server) {
774
+ serverState.server.close();
775
+ serverState.server = null;
776
+ }
762
777
  }
763
778
  else {
764
779
  console.error("[pi-vault-mind] HTTP server error:", err);
@@ -1077,14 +1092,23 @@ function handleVaultMindContextGet(res, serverState) {
1077
1092
  // ── Stage 1 setup routes (auth-gated) ───────────────────────────────────────
1078
1093
  function handleVmStatus(res, serverState) {
1079
1094
  const ws = serverState.watcherState;
1080
- const cfg = loadConfig(process.cwd());
1081
- const configured = !!findConfig(process.cwd()).project;
1095
+ const vaultPath = serverState.vaultPath;
1096
+ if (!vaultPath) {
1097
+ res.writeHead(500);
1098
+ res.end(JSON.stringify({ error: "Vault path not set" }));
1099
+ return;
1100
+ }
1101
+ const cfg = loadConfig(vaultPath);
1102
+ const configured = !!findConfig(vaultPath).project;
1103
+ const personalized = isPersonalized(vaultPath);
1082
1104
  const model = cfg.vaultMind.embedding[EMBEDDING_FLAT_KEYS[2]] || "embeddinggemma";
1083
1105
  const dim = cfg.vaultMind.embedding[EMBEDDING_FLAT_KEYS[4]] ?? 768;
1084
1106
  res.writeHead(200);
1085
1107
  res.end(JSON.stringify({
1086
1108
  ok: true,
1087
1109
  configured,
1110
+ personalized,
1111
+ vaultPath,
1088
1112
  watcher: ws?.running ?? false,
1089
1113
  embedding: {
1090
1114
  model,
@@ -13,6 +13,9 @@ export declare const resolveAgentDir: (cwd: string) => string;
13
13
  export declare const resolveVaultMindDir: (cwd: string) => string;
14
14
  /** Active pi-vault-mind config: `<vault>/.vault-mind/vault-mind.config.json`. */
15
15
  export declare const getConfigPath: (cwd: string) => string;
16
+ export declare const getPersonalizedMarkerPath: (cwd: string) => string;
17
+ /** Check if a vault has a valid personalization completion marker. */
18
+ export declare function isPersonalized(cwd: string): boolean;
16
19
  /** Token env file: `<vault>/.vault-mind/vault-mind.env`. */
17
20
  export declare const getTokenEnvPath: (cwd: string) => string;
18
21
  export declare const EXT_ROOT: string;
package/dist/src/utils.js CHANGED
@@ -18,6 +18,19 @@ export const resolveAgentDir = (cwd) => {
18
18
  export const resolveVaultMindDir = (cwd) => path.join(cwd, VAULT_MIND_DIR);
19
19
  /** Active pi-vault-mind config: `<vault>/.vault-mind/vault-mind.config.json`. */
20
20
  export const getConfigPath = (cwd) => path.join(resolveVaultMindDir(cwd), "vault-mind.config.json");
21
+ export const getPersonalizedMarkerPath = (cwd) => path.join(resolveVaultMindDir(cwd), "personalized.json");
22
+ /** Check if a vault has a valid personalization completion marker. */
23
+ export function isPersonalized(cwd) {
24
+ const p = getPersonalizedMarkerPath(cwd);
25
+ try {
26
+ const content = fs.readFileSync(p, "utf8");
27
+ const marker = JSON.parse(content);
28
+ return marker && typeof marker === "object" && marker.completed === true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
21
34
  /** Token env file: `<vault>/.vault-mind/vault-mind.env`. */
22
35
  export const getTokenEnvPath = (cwd) => path.join(resolveVaultMindDir(cwd), "vault-mind.env");
23
36
  export const EXT_ROOT = path.join(import.meta.dirname, "..", "..");
@@ -192,6 +192,69 @@ describe("personalize", () => {
192
192
  // AGENTS.md was never created, so rejection means it still does not exist.
193
193
  assert.equal(fs.existsSync(path.join(testDir, "AGENTS.md")), false);
194
194
  });
195
+ it("persists a completion marker even when all suggestions are rejected or skipped", async () => {
196
+ const { runPersonalize } = await import("../src/personalize.js");
197
+ const pi = makePi(JSON.stringify({
198
+ commentary: "No changes needed.",
199
+ files: [
200
+ { path: ".pi/agent/system.md", action: "skip" },
201
+ { path: "AGENTS.md", action: "skip" },
202
+ ],
203
+ }));
204
+ const ctx = makeCtx(testDir, []);
205
+ await runPersonalize(ctx, pi);
206
+ const markerPath = path.join(testDir, ".vault-mind", "personalized.json");
207
+ assert.ok(fs.existsSync(markerPath), "runPersonalize must write a durable completion marker after the flow finishes, even when every diff is rejected");
208
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
209
+ assert.strictEqual(marker.completed, true);
210
+ assert.ok(typeof marker.completedAt === "string");
211
+ });
212
+ it("persists a completion marker after approved suggestions", async () => {
213
+ const { runPersonalize } = await import("../src/personalize.js");
214
+ const pi = makePi(JSON.stringify({
215
+ commentary: "Updated tuning.",
216
+ files: [{ path: ".pi/agent/system.md", action: "create", content: "# New system" }],
217
+ }));
218
+ const ctx = makeCtx(testDir, [true]);
219
+ await runPersonalize(ctx, pi);
220
+ const markerPath = path.join(testDir, ".vault-mind", "personalized.json");
221
+ assert.ok(fs.existsSync(markerPath), "runPersonalize must write a durable completion marker after approved suggestions are applied");
222
+ const marker = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
223
+ assert.strictEqual(marker.completed, true);
224
+ assert.ok(typeof marker.completedAt === "string");
225
+ });
226
+ it("returns a successful result when all suggestions are rejected", async () => {
227
+ const { runPersonalize: raw } = await import("../src/personalize.js");
228
+ const runPersonalize = raw;
229
+ const pi = makePi(JSON.stringify({
230
+ commentary: "No changes needed.",
231
+ files: [
232
+ { path: ".pi/agent/system.md", action: "skip" },
233
+ { path: "AGENTS.md", action: "skip" },
234
+ ],
235
+ }));
236
+ const ctx = makeCtx(testDir, []);
237
+ const result = await runPersonalize(ctx, pi);
238
+ assert.ok(result, "runPersonalize must return a result, not void");
239
+ assert.strictEqual(result.completed, true, "all-rejected personalization must resolve as a successful completion");
240
+ });
241
+ it("returns a failed result when suggestion generation fails", async () => {
242
+ const { runPersonalize: raw } = await import("../src/personalize.js");
243
+ const runPersonalize = raw;
244
+ const pi = makePi("not valid JSON");
245
+ const ctx = makeCtx(testDir, []);
246
+ const result = await runPersonalize(ctx, pi);
247
+ assert.ok(result, "runPersonalize must return a result, not void");
248
+ assert.strictEqual(result.completed, false, "a generation failure must resolve as a failed personalization, distinguishable from success");
249
+ });
250
+ it("does not persist a completion marker when suggestion generation fails", async () => {
251
+ const { runPersonalize } = await import("../src/personalize.js");
252
+ const pi = makePi("not valid JSON");
253
+ const ctx = makeCtx(testDir, []);
254
+ await runPersonalize(ctx, pi);
255
+ const markerPath = path.join(testDir, ".vault-mind", "personalized.json");
256
+ assert.strictEqual(fs.existsSync(markerPath), false, "runPersonalize must not leave a completion marker after a failed personalization");
257
+ });
195
258
  it("presentAndApplyDiff creates missing directories for new files", async () => {
196
259
  const { presentAndApplyDiff } = await import("../src/personalize.js");
197
260
  const ctx = makeCtx(testDir, [true]);
@@ -81,6 +81,13 @@ describe("REST setup routes", () => {
81
81
  const { status } = await fetchJson(port, "GET", "/vm/status");
82
82
  assert.equal(status, 401);
83
83
  });
84
+ it("GET /vm/status returns the configured vault identity", async () => {
85
+ writeVaultConfig(vaultPath);
86
+ const { status, body } = await fetchJson(port, "GET", "/vm/status", { token: TEST_TOKEN });
87
+ assert.equal(status, 200);
88
+ const b = body;
89
+ assert.equal(b.vaultPath, vaultPath, "status must expose the resolved vault path so clients can recheck vault identity");
90
+ });
84
91
  it("POST /vm/setup scaffolds config and vault-named collection file", async () => {
85
92
  const { status, body } = await fetchJson(port, "POST", "/vm/setup", {
86
93
  body: { vault: vaultPath },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vault-mind",
3
- "version": "0.16.4",
3
+ "version": "0.16.6",
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",