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 +23 -0
- package/dist/src/commands.js +2 -1
- package/dist/src/personalize.d.ts +3 -1
- package/dist/src/personalize.js +8 -2
- package/dist/src/server.js +37 -13
- package/dist/src/utils.d.ts +3 -0
- package/dist/src/utils.js +13 -0
- package/dist/test/personalize.test.js +63 -0
- package/dist/test/rest-setup.test.js +7 -0
- package/package.json +1 -1
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
|
package/dist/src/commands.js
CHANGED
|
@@ -1760,7 +1760,8 @@ export const registerCommands = (pi) => {
|
|
|
1760
1760
|
case "remote":
|
|
1761
1761
|
return handleModal(rest, ctx, pi);
|
|
1762
1762
|
case "personalize":
|
|
1763
|
-
|
|
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<
|
|
47
|
+
export declare const runPersonalize: (ctx: ExtensionContext, pi: ExtensionAPI) => Promise<{
|
|
48
|
+
completed: boolean;
|
|
49
|
+
}>;
|
package/dist/src/personalize.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/src/server.js
CHANGED
|
@@ -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
|
|
102
|
-
|
|
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"
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
1081
|
-
|
|
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,
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -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.
|
|
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",
|