pi-vault-mind 0.16.11 → 0.16.13

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,23 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## 0.16.12 / 0.6.16 — 2026-07-20
5
+
6
+ ### Fixed
7
+
8
+ - **Minimal config before bridge start.** The setup wizard now writes a minimal `.vault-mind/vault-mind.config.json` with the vault path before starting the bridge, so `startServer` picks up the correct `vaultPath` immediately instead of falling back to `process.cwd()`.
9
+ - **Model-router vault isolation.** `model-router.json` is now always vault-scoped under `<vault>/.vault-mind/.pi/model-router.json`. The global `~/.pi/model-router.json` fallback is removed.
10
+ - **Stale `server.json` cleanup.** Dead PIDs and mismatched vault paths now delete the stale discovery file and spawn a fresh runtime instead of looping or throwing.
11
+ - **Vault path re-resolution after setup.** `POST /vm/setup` now updates the running server's `vaultPath` from the newly written config, preventing stale path references until restart.
12
+ - **Runtime files relocated.** `ensureVaultRuntime.ts` and `startFreshRuntime.ts` moved from `packages/obsidian/src/chat/` to `packages/obsidian/src/runtime/`.
13
+ - **Agent folder creation on setup save.** The four default Agent/ folders (Inbox, Library, Presentations, Journal) are now created during `POST /vm/setup` using the user's configured paths or defaults.
14
+ - **Expanded vault reset.** `reset-test-vault.sh` now also removes `Pi-Sessions/`, `AGENTS.md`, `.env.1pass`, `.omp/`, Agent/ subdirectories, and the entire `vault-mind` plugin directory.
15
+
16
+ ### Verification
17
+
18
+ - Added stale-discovery cleanup tests (dead PID, mismatched vaultPath, healthy adoption).
19
+ - Expanded reset-test-vault assertions for all newly removed artifacts.
20
+
3
21
  ## 0.16.11 / 0.6.15 — 2026-07-20
4
22
 
5
23
  ### Fixed
@@ -11,6 +11,6 @@
11
11
  * - VAULT_MIND_CONFIG_KEYS: fully-qualified dotted paths (for static access)
12
12
  */
13
13
  /** Bare key names on vaultMind.embedding — used by setup wizard, CLI, HTTP, and plugin. */
14
- export declare const EMBEDDING_FLAT_KEYS: readonly ["remoteUrl", "localUrl", "model", "apiKey", "dim", "useTransformers", "remoteApiKey", "remoteReadApiKey", "remoteWriteApiKey", "fallback", "sync", "collectionModels", "coalesce"];
14
+ export declare const EMBEDDING_FLAT_KEYS: readonly ["remoteUrl", "localUrl", "model", "apiKey", "dim", "useTransformers", "remoteApiKey", "fallback", "sync", "collectionModels", "coalesce"];
15
15
  /** Fully-qualified dotted key paths that the plugin's reconfigure form reads/writes. */
16
16
  export declare const VAULT_MIND_CONFIG_KEYS: readonly ["vaultMind.embedding.remoteUrl", "vaultMind.embedding.localUrl", "vaultMind.embedding.model", "vaultMind.embedding.dim", "vaultMind.embedding.useTransformers", "vaultMind.embedding.remoteApiKey", "vaultMind.embedding.fallback", "vaultMind.embedding.sync", "vaultMind.ftsEnabled", "vaultMind.graph", "vaultMind.vaults.default.autoStart", "extensionCompatibility.pi-context.enabled"];
@@ -19,8 +19,6 @@ export const EMBEDDING_FLAT_KEYS = [
19
19
  "dim",
20
20
  "useTransformers",
21
21
  "remoteApiKey",
22
- "remoteReadApiKey",
23
- "remoteWriteApiKey",
24
22
  "fallback",
25
23
  "sync",
26
24
  "collectionModels",
@@ -5,8 +5,6 @@ export interface EmbeddingProbeRequest {
5
5
  model?: string;
6
6
  transientSecrets?: {
7
7
  apiKey?: string;
8
- readApiKey?: string;
9
- writeApiKey?: string;
10
8
  };
11
9
  }
12
10
  export interface EmbeddingModelOption {
@@ -99,17 +99,11 @@ const resolveAuthorizationHeader = (request, cfg) => {
99
99
  return resolveEmbeddingSecret(cfg, "localApiKey");
100
100
  return undefined;
101
101
  }
102
- // Remote read path: never select write-scoped credentials.
103
- if (request.transientSecrets?.readApiKey)
104
- return request.transientSecrets.readApiKey;
102
+ // Remote path: use the single remote API key.
105
103
  if (request.transientSecrets?.apiKey)
106
104
  return request.transientSecrets.apiKey;
107
- if (cfg) {
108
- const readToken = resolveEmbeddingSecret(cfg, "remoteReadApiKey");
109
- if (readToken)
110
- return readToken;
105
+ if (cfg)
111
106
  return resolveEmbeddingSecret(cfg, "remoteApiKey");
112
- }
113
107
  return undefined;
114
108
  };
115
109
  export async function probeEmbeddingEndpoint(request, options) {
@@ -14,8 +14,6 @@ import { getTokenEnvPath } from "./utils.js";
14
14
  const DEFINITIONS = {
15
15
  localApiKey: { env: "PVM_LOCAL_EMBEDDING_API_KEY", legacy: "apiKey" },
16
16
  remoteApiKey: { env: "PVM_REMOTE_EMBEDDING_API_KEY", legacy: "remoteApiKey" },
17
- remoteReadApiKey: { env: "PVM_MODAL_READ_TOKEN", legacy: "remoteReadApiKey" },
18
- remoteWriteApiKey: { env: "PVM_MODAL_WRITE_TOKEN", legacy: "remoteWriteApiKey" },
19
17
  };
20
18
  export const EMBEDDING_SECRET_MASK = "••••••••";
21
19
  const getEnv = (opts, key) => {
@@ -76,12 +74,7 @@ const getSecretSource = (cfg, kind, envRecord, opts) => {
76
74
  };
77
75
  /** Return the status of all four embedding secret kinds in declaration order. */
78
76
  export const readEmbeddingSecretStatus = (cfg, opts) => {
79
- const kinds = [
80
- "localApiKey",
81
- "remoteApiKey",
82
- "remoteReadApiKey",
83
- "remoteWriteApiKey",
84
- ];
77
+ const kinds = ["localApiKey", "remoteApiKey"];
85
78
  const envRecord = readDotEnv(getEnvPath(opts));
86
79
  return {
87
80
  secrets: kinds.map((kind) => {
package/dist/src/lance.js CHANGED
@@ -4,7 +4,8 @@ import * as lancedb from "@lancedb/lancedb";
4
4
  import { LanceSchema, TextEmbeddingFunction } from "@lancedb/lancedb/embedding";
5
5
  import * as arrow from "apache-arrow";
6
6
  import { EmbeddingCoalescer } from "./embed-queue.js";
7
- import { createProvider } from "./embedding-providers.js";
7
+ import { createProvider, DEFAULT_OLLAMA_API_KEY, OpenAICompatibleProvider, } from "./embedding-providers.js";
8
+ import { resolveEmbeddingSecret } from "./embedding-secrets.js";
8
9
  import { namespacedTableName, resolveDim, resolveModel } from "./modal-config.js";
9
10
  let db = null;
10
11
  // ── Connection ──────────────────────────────────────────────────────────────
@@ -261,9 +262,19 @@ const fallbackQueryEmbed = async (cfg, collection, text) => {
261
262
  const canonical = resolveModel(cfg, collection);
262
263
  if ((cfg.embedding.model || "embeddinggemma") !== canonical)
263
264
  return null;
265
+ const localCfg = {
266
+ ...cfg,
267
+ embedding: { ...cfg.embedding, remoteUrl: "" },
268
+ };
264
269
  try {
265
- const fn = await createProvider(cfg, { collection });
266
- const vecs = await fn.embed([text], "query");
270
+ const provider = new OpenAICompatibleProvider({
271
+ url: cfg.embedding.localUrl,
272
+ model: resolveModel(localCfg, collection),
273
+ apiKey: resolveEmbeddingSecret(localCfg, "localApiKey") ?? DEFAULT_OLLAMA_API_KEY,
274
+ dims: resolveDim(localCfg, collection),
275
+ });
276
+ await provider.init();
277
+ const vecs = await provider.embed([text], "query");
267
278
  return vecs[0];
268
279
  }
269
280
  catch (err) {
@@ -484,9 +495,19 @@ const fallbackDocumentEmbed = async (cfg, collection, text) => {
484
495
  const canonical = resolveModel(cfg, collection);
485
496
  if ((cfg.embedding.model || "embeddinggemma") !== canonical)
486
497
  return null;
498
+ const localCfg = {
499
+ ...cfg,
500
+ embedding: { ...cfg.embedding, remoteUrl: "" },
501
+ };
487
502
  try {
488
- const fn = await createProvider(cfg, { collection });
489
- const vecs = await fn.embed([text], "document");
503
+ const provider = new OpenAICompatibleProvider({
504
+ url: cfg.embedding.localUrl,
505
+ model: resolveModel(localCfg, collection),
506
+ apiKey: resolveEmbeddingSecret(localCfg, "localApiKey") ?? DEFAULT_OLLAMA_API_KEY,
507
+ dims: resolveDim(localCfg, collection),
508
+ });
509
+ await provider.init();
510
+ const vecs = await provider.embed([text], "document");
490
511
  return vecs[0];
491
512
  }
492
513
  catch (err) {
@@ -18,12 +18,8 @@
18
18
  export interface ModalClientConfig {
19
19
  /** Base URL of the deployed ASGI app (no trailing slash needed). */
20
20
  baseUrl: string;
21
- /** Fallback bearer token (legacy single token for all operations). */
21
+ /** Bearer token for all operations. */
22
22
  apiToken?: string;
23
- /** Read-scoped token for search/export operations. Falls back to `apiToken`. */
24
- readToken?: string;
25
- /** Write-scoped token for embed/reindex operations. Falls back to `apiToken`. */
26
- writeToken?: string;
27
23
  /** Per-request timeout in ms (default 120s — bulk submits can be large). */
28
24
  timeoutMs?: number;
29
25
  }
@@ -105,12 +101,10 @@ export interface ExportPage {
105
101
  export declare class ModalEmbeddingClient {
106
102
  private baseUrl;
107
103
  private apiToken;
108
- private readToken;
109
- private writeToken;
110
104
  private timeoutMs;
111
105
  constructor(cfg: ModalClientConfig);
112
- /** Return the bearer token to use for the requested operation scope. */
113
- private tokenFor;
106
+ /** Return the bearer token to use for requests. */
107
+ private token;
114
108
  private request;
115
109
  /** Liveness check; also returns the server's default model. */
116
110
  health(): Promise<{
@@ -155,7 +149,7 @@ export declare class ModalEmbeddingClient {
155
149
  since?: number;
156
150
  limit?: number;
157
151
  }): Promise<ExportPage>;
158
- /** Low-level binary request helper with scoped token selection. */
152
+ /** Low-level binary request helper. */
159
153
  private requestBuffer;
160
154
  /** Pull one page of rows with seq > since as an Arrow IPC stream.
161
155
  * Vectors are always included (no include_vectors flag). The watermark /
@@ -18,31 +18,26 @@
18
18
  export class ModalEmbeddingClient {
19
19
  baseUrl;
20
20
  apiToken;
21
- readToken;
22
- writeToken;
23
21
  timeoutMs;
24
22
  constructor(cfg) {
25
23
  this.baseUrl = cfg.baseUrl.replace(/\/$/, "");
26
24
  this.apiToken = cfg.apiToken;
27
- this.readToken = cfg.readToken ?? cfg.apiToken;
28
- this.writeToken = cfg.writeToken ?? cfg.apiToken;
29
25
  this.timeoutMs = cfg.timeoutMs ?? 120_000;
30
26
  }
31
- /** Return the bearer token to use for the requested operation scope. */
32
- tokenFor(operation) {
33
- const token = operation === "read" ? this.readToken : this.writeToken;
34
- if (!token)
35
- throw new Error(`Modal ${operation} token not configured`);
36
- return token;
27
+ /** Return the bearer token to use for requests. */
28
+ token() {
29
+ if (!this.apiToken)
30
+ throw new Error("Modal API token not configured");
31
+ return this.apiToken;
37
32
  }
38
- async request(method, path, body, operation = "write") {
33
+ async request(method, path, body) {
39
34
  const controller = new AbortController();
40
35
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
41
36
  try {
42
37
  const resp = await fetch(`${this.baseUrl}${path}`, {
43
38
  method,
44
39
  headers: {
45
- Authorization: `Bearer ${this.tokenFor(operation)}`,
40
+ Authorization: `Bearer ${this.token()}`,
46
41
  ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
47
42
  },
48
43
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -60,17 +55,17 @@ export class ModalEmbeddingClient {
60
55
  }
61
56
  /** Liveness check; also returns the server's default model. */
62
57
  health() {
63
- return this.request("GET", "/health", undefined, "read");
58
+ return this.request("GET", "/health", undefined);
64
59
  }
65
60
  /** Registry of available embedders (public; no auth). Use native_dim to
66
61
  * resolve a model's output dim up-front instead of waiting for the first
67
62
  * /embed response. (Additive — Agent B request #2.) */
68
63
  models() {
69
- return this.request("GET", "/models", undefined, "read");
64
+ return this.request("GET", "/models", undefined);
70
65
  }
71
66
  /** Server-side store + compute stats (rows per namespace, index state, GPU). */
72
67
  stats() {
73
- return this.request("GET", "/stats", undefined, "read");
68
+ return this.request("GET", "/stats", undefined);
74
69
  }
75
70
  /** Embed text on demand. Use task="query" for search, "document" for storage. */
76
71
  embed(texts, opts = {}) {
@@ -79,7 +74,7 @@ export class ModalEmbeddingClient {
79
74
  model: opts.model,
80
75
  dim: opts.dim,
81
76
  task: opts.task ?? "query",
82
- }, "write");
77
+ });
83
78
  }
84
79
  /** Submit a bulk embedding job; embeds + stores server-side. */
85
80
  submitJob(collection, records, opts = {}) {
@@ -88,10 +83,10 @@ export class ModalEmbeddingClient {
88
83
  records,
89
84
  model: opts.model,
90
85
  dim: opts.dim,
91
- }, "write");
86
+ });
92
87
  }
93
88
  jobStatus(jobId) {
94
- return this.request("GET", `/jobs/${encodeURIComponent(jobId)}`, undefined, "read");
89
+ return this.request("GET", `/jobs/${encodeURIComponent(jobId)}`, undefined);
95
90
  }
96
91
  /** List recent jobs (newest first). Additive — surfaces GET /jobs so
97
92
  * `/vm remote jobs` can list, not just poll a known id. (Agent B request #1.) */
@@ -100,12 +95,12 @@ export class ModalEmbeddingClient {
100
95
  if (limit != null)
101
96
  p.set("limit", String(limit));
102
97
  const qs = p.toString();
103
- return this.request("GET", `/jobs${qs ? `?${qs}` : ""}`, undefined, "read");
98
+ return this.request("GET", `/jobs${qs ? `?${qs}` : ""}`, undefined);
104
99
  }
105
100
  /** Cooperatively cancel a running/queued job. The worker stops after its
106
101
  * current batch and writes status=cancelled. */
107
102
  cancelJob(jobId) {
108
- return this.request("POST", `/jobs/${encodeURIComponent(jobId)}/cancel`, undefined, "write");
103
+ return this.request("POST", `/jobs/${encodeURIComponent(jobId)}/cancel`, undefined);
109
104
  }
110
105
  /** Poll a job until it reaches a terminal state. */
111
106
  async waitForJob(jobId, pollMs = 2000) {
@@ -118,7 +113,7 @@ export class ModalEmbeddingClient {
118
113
  }
119
114
  /** List the collections/tables held in the server-side vector store. */
120
115
  async syncCollections() {
121
- const out = await this.request("GET", "/sync/collections", undefined, "read");
116
+ const out = await this.request("GET", "/sync/collections", undefined);
122
117
  return out.collections;
123
118
  }
124
119
  /** Pull one page of rows with seq > since. Remember next_watermark. */
@@ -130,16 +125,16 @@ export class ModalEmbeddingClient {
130
125
  p.set("dim", String(opts.dim));
131
126
  p.set("since", String(opts.since ?? 0));
132
127
  p.set("limit", String(opts.limit ?? 500));
133
- return this.request("GET", `/sync/export?${p.toString()}`, undefined, "read");
128
+ return this.request("GET", `/sync/export?${p.toString()}`, undefined);
134
129
  }
135
- /** Low-level binary request helper with scoped token selection. */
136
- async requestBuffer(method, path, operation = "read") {
130
+ /** Low-level binary request helper. */
131
+ async requestBuffer(method, path) {
137
132
  const controller = new AbortController();
138
133
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
139
134
  try {
140
135
  const resp = await fetch(`${this.baseUrl}${path}`, {
141
136
  method,
142
- headers: { Authorization: `Bearer ${this.tokenFor(operation)}` },
137
+ headers: { Authorization: `Bearer ${this.token()}` },
143
138
  signal: controller.signal,
144
139
  });
145
140
  if (!resp.ok) {
@@ -166,7 +161,7 @@ export class ModalEmbeddingClient {
166
161
  p.set("dim", String(opts.dim));
167
162
  p.set("since", String(opts.since ?? 0));
168
163
  p.set("limit", String(opts.limit ?? 500));
169
- const resp = await this.requestBuffer("GET", `/sync/export?${p.toString()}`, "read");
164
+ const resp = await this.requestBuffer("GET", `/sync/export?${p.toString()}`);
170
165
  const nextWatermark = Number(resp.headers.get("X-Next-Watermark") ?? opts.since ?? 0);
171
166
  const done = (resp.headers.get("X-Done") ?? "true") === "true";
172
167
  const count = Number(resp.headers.get("X-Count") ?? 0);
@@ -10,12 +10,8 @@
10
10
  */
11
11
  import { ModalEmbeddingClient } from "./modal-client.js";
12
12
  import type { VaultMindConfig } from "./types.js";
13
- /** Env var name for the unscoped remote embedding API key (preferred over config). */
13
+ /** Env var name for the remote embedding API key (preferred over config). */
14
14
  export declare const MODAL_TOKEN_ENV = "PVM_REMOTE_EMBEDDING_API_KEY";
15
- /** Env var name for the read-scoped Modal token (search/export). */
16
- export declare const MODAL_READ_TOKEN_ENV = "PVM_MODAL_READ_TOKEN";
17
- /** Env var name for the write-scoped Modal token (embed/reindex). */
18
- export declare const MODAL_WRITE_TOKEN_ENV = "PVM_MODAL_WRITE_TOKEN";
19
15
  /**
20
16
  * Dotenv fallback path for the Modal token, resolved from the vault cwd:
21
17
  * `<vault>/.vault-mind/vault-mind.env`. Lazy so tests can override cwd.
@@ -35,23 +31,9 @@ export declare const MODAL_REMOTE_URL = "https://kylebrodeur--pi-vault-mind-embe
35
31
  * Never log the resolved token.
36
32
  */
37
33
  export declare const resolveModalToken: (cfg: VaultMindConfig) => string | undefined;
38
- /**
39
- * Resolve a scoped Modal token for the requested operation.
40
- * Resolution order:
41
- * 1. `PVM_MODAL_{READ|WRITE}_TOKEN` env var
42
- * 2. `<vault>/.vault-mind/vault-mind.env`
43
- * 3. `vaultMind.embedding.remoteReadApiKey` / `remoteWriteApiKey` in config
44
- * 4. Fallback to the unscoped `PVM_REMOTE_EMBEDDING_API_KEY` (env, dotenv, then config)
45
- *
46
- * `PVM_API_TOKEN` is intentionally not consulted — it is reserved for the
47
- * extension's bridge authentication.
48
- *
49
- * Never log the resolved token.
50
- */
51
- export declare const getModalToken: (cfg: VaultMindConfig, operation: "read" | "write") => string | undefined;
52
34
  /** Resolve only an explicitly configured remote URL. */
53
35
  export declare const resolveBaseUrl: (cfg: VaultMindConfig) => string | undefined;
54
- /** True when Modal is usable: an explicit remote URL and at least one resolvable scoped token are present. */
36
+ /** True when Modal is usable: an explicit remote URL and a resolvable remote token are present. */
55
37
  export declare const isModalConfigured: (cfg: VaultMindConfig) => boolean;
56
38
  /**
57
39
  * Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
@@ -11,12 +11,8 @@
11
11
  import * as fs from "node:fs";
12
12
  import { ModalEmbeddingClient } from "./modal-client.js";
13
13
  import { deriveCollectionNameFromVaultPath, getTokenEnvPath, normalizeCollectionName, } from "./utils.js";
14
- /** Env var name for the unscoped remote embedding API key (preferred over config). */
14
+ /** Env var name for the remote embedding API key (preferred over config). */
15
15
  export const MODAL_TOKEN_ENV = "PVM_REMOTE_EMBEDDING_API_KEY";
16
- /** Env var name for the read-scoped Modal token (search/export). */
17
- export const MODAL_READ_TOKEN_ENV = "PVM_MODAL_READ_TOKEN";
18
- /** Env var name for the write-scoped Modal token (embed/reindex). */
19
- export const MODAL_WRITE_TOKEN_ENV = "PVM_MODAL_WRITE_TOKEN";
20
16
  /**
21
17
  * Dotenv fallback path for the Modal token, resolved from the vault cwd:
22
18
  * `<vault>/.vault-mind/vault-mind.env`. Lazy so tests can override cwd.
@@ -68,36 +64,10 @@ export const resolveModalToken = (cfg) => {
68
64
  return dotenv[MODAL_TOKEN_ENV];
69
65
  return cfg.embedding.remoteApiKey;
70
66
  };
71
- /**
72
- * Resolve a scoped Modal token for the requested operation.
73
- * Resolution order:
74
- * 1. `PVM_MODAL_{READ|WRITE}_TOKEN` env var
75
- * 2. `<vault>/.vault-mind/vault-mind.env`
76
- * 3. `vaultMind.embedding.remoteReadApiKey` / `remoteWriteApiKey` in config
77
- * 4. Fallback to the unscoped `PVM_REMOTE_EMBEDDING_API_KEY` (env, dotenv, then config)
78
- *
79
- * `PVM_API_TOKEN` is intentionally not consulted — it is reserved for the
80
- * extension's bridge authentication.
81
- *
82
- * Never log the resolved token.
83
- */
84
- export const getModalToken = (cfg, operation) => {
85
- const dotenv = readDotEnv(modalTokenEnvPath());
86
- if (operation === "read") {
87
- return (process.env[MODAL_READ_TOKEN_ENV] ||
88
- dotenv[MODAL_READ_TOKEN_ENV] ||
89
- cfg.embedding.remoteReadApiKey ||
90
- resolveModalToken(cfg));
91
- }
92
- return (process.env[MODAL_WRITE_TOKEN_ENV] ||
93
- dotenv[MODAL_WRITE_TOKEN_ENV] ||
94
- cfg.embedding.remoteWriteApiKey ||
95
- resolveModalToken(cfg));
96
- };
97
67
  /** Resolve only an explicitly configured remote URL. */
98
68
  export const resolveBaseUrl = (cfg) => cfg.embedding.remoteUrl;
99
- /** True when Modal is usable: an explicit remote URL and at least one resolvable scoped token are present. */
100
- export const isModalConfigured = (cfg) => !!cfg.embedding.remoteUrl && (!!getModalToken(cfg, "read") || !!getModalToken(cfg, "write"));
69
+ /** True when Modal is usable: an explicit remote URL and a resolvable remote token are present. */
70
+ export const isModalConfigured = (cfg) => !!cfg.embedding.remoteUrl && !!resolveModalToken(cfg);
101
71
  /**
102
72
  * Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
103
73
  * configured (no base URL / token) so callers can degrade gracefully.
@@ -107,11 +77,9 @@ export const createModalClient = (cfg) => {
107
77
  if (!baseUrl)
108
78
  return null;
109
79
  const apiToken = resolveModalToken(cfg);
110
- const readToken = getModalToken(cfg, "read");
111
- const writeToken = getModalToken(cfg, "write");
112
- if (!apiToken && !readToken && !writeToken)
80
+ if (!apiToken)
113
81
  return null;
114
- const clientCfg = { baseUrl, apiToken, readToken, writeToken };
82
+ const clientCfg = { baseUrl, apiToken };
115
83
  return new ModalEmbeddingClient(clientCfg);
116
84
  };
117
85
  /**
@@ -1,8 +1,9 @@
1
1
  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
+ import { enableACM } from "./context-capture.js";
4
5
  import { getStatus } from "./lance.js";
5
- import { expandHome, getPersonalizedMarkerPath, loadConfig, resolveAgentDir } from "./utils.js";
6
+ import { expandHome, getPersonalizedMarkerPath, hasPiContextTools, isPersonalized, loadConfig, resolveAgentDir, } from "./utils.js";
6
7
  class PersonalizationCancelledError extends Error {
7
8
  constructor() {
8
9
  super("Personalization cancelled");
@@ -598,6 +599,8 @@ export const runPersonalize = async (ctx, pi) => {
598
599
  ctx.ui.notify(`Personalization failed: ${message}`, "error");
599
600
  return { completed: false };
600
601
  }
602
+ if (isPersonalized(vaultPath))
603
+ return { completed: true };
601
604
  const vaultKey = personalizationVaultKey(vaultPath);
602
605
  activePersonalizations.get(vaultKey)?.abort();
603
606
  const controller = new AbortController();
@@ -625,6 +628,11 @@ export const runPersonalize = async (ctx, pi) => {
625
628
  // abort check and synchronous write in one event-loop turn so cancellation
626
629
  // cannot report an aborted run after the marker has been committed.
627
630
  fs.writeFileSync(markerPath, JSON.stringify({ completed: true, completedAt: new Date().toISOString() }, null, 2));
631
+ const sessionCfg = loadConfig(ctx.cwd);
632
+ const piCtxCfg = sessionCfg.extensionCompatibility?.["pi-context"];
633
+ if (piCtxCfg?.enabled && piCtxCfg?.autoEnableAcm !== false && hasPiContextTools(pi)) {
634
+ enableACM(pi);
635
+ }
628
636
  if (activePersonalizations.get(vaultKey) === controller) {
629
637
  activePersonalizations.delete(vaultKey);
630
638
  }
@@ -200,6 +200,22 @@ export const scaffoldVaultConfig = (vaultPath, collectionOverride) => {
200
200
  existing.vaultMind = {};
201
201
  changed = true;
202
202
  }
203
+ if (!existing.vaultMind.graph) {
204
+ existing.vaultMind.graph = { enabled: true, canvasSync: false };
205
+ changed = true;
206
+ }
207
+ else if (typeof existing.vaultMind.graph === "object" &&
208
+ existing.vaultMind.graph !== null) {
209
+ // Preserve any provided canvasPath while ensuring default graph flags exist.
210
+ if (typeof existing.vaultMind.graph.enabled !== "boolean") {
211
+ existing.vaultMind.graph.enabled = true;
212
+ changed = true;
213
+ }
214
+ if (typeof existing.vaultMind.graph.canvasSync !== "boolean") {
215
+ existing.vaultMind.graph.canvasSync = false;
216
+ changed = true;
217
+ }
218
+ }
203
219
  if (changed) {
204
220
  fs.writeFileSync(cfgDest, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
205
221
  updated.push(cfgDest);
@@ -46,7 +46,7 @@ import { cancelPersonalization } from "./personalize.js";
46
46
  import { scaffoldVaultConfig } from "./scaffold.js";
47
47
  import { archiveSession, deleteSession, exportSession, getSession, listSessions, renameSession, resolveSessionsDir, searchSessions, } from "./session-search.js";
48
48
  import { listAllTools } from "./tool-catalog.js";
49
- import { ensureDir, expandHome, findConfig, getConfigPath, isPersonalized, loadConfig, resolveVaultFile, resolveVaultMindPaths, shrinkHome, } from "./utils.js";
49
+ import { ensureDir, expandHome, findConfig, getConfigPath, isPersonalized, loadConfig, resolveVaultFile, resolveVaultFolder, resolveVaultMindPaths, shrinkHome, } from "./utils.js";
50
50
  import { classifySearchMode, normalizeGraphResult, searchVaultNative } from "./vault-tools.js";
51
51
  import { createManualDispatch, processQueue, scanFile, startWatcher, stopWatcher, } from "./watcher.js";
52
52
  export function createServerState(port = 11435) {
@@ -339,7 +339,7 @@ export function startServer(pi, serverState, watcherState, readinessCallback) {
339
339
  res.end(JSON.stringify({ error: "Method not allowed" }));
340
340
  return;
341
341
  }
342
- withAuth(req, res, () => handleVmSetup(req, res), { requireWrite: true });
342
+ withAuth(req, res, () => handleVmSetup(req, res, serverState), { requireWrite: true });
343
343
  break;
344
344
  case "/vm/token":
345
345
  if (req.method !== "POST") {
@@ -1266,7 +1266,7 @@ function handleWatcherToggle(res, serverState) {
1266
1266
  res.writeHead(200);
1267
1267
  res.end(JSON.stringify({ ok: true, watcher: true }));
1268
1268
  }
1269
- function handleVmSetup(req, res) {
1269
+ function handleVmSetup(req, res, serverState) {
1270
1270
  readBody(req)
1271
1271
  .then((raw) => {
1272
1272
  let parsed;
@@ -1291,7 +1291,12 @@ function handleVmSetup(req, res) {
1291
1291
  !Array.isArray(config.vaultMind.vaults.default)
1292
1292
  ? { ...config.vaultMind.vaults.default }
1293
1293
  : {};
1294
- if (parsed.vault || typeof parsed.preferences?.autoStart === "boolean") {
1294
+ if (parsed.vault ||
1295
+ typeof parsed.preferences?.autoStart === "boolean" ||
1296
+ typeof parsed.preferences?.autoSync === "boolean" ||
1297
+ typeof parsed.preferences?.autoSyncMinLength === "number" ||
1298
+ parsed.folders?.collectionPrefix?.trim() ||
1299
+ parsed.folders?.canvasPath?.trim()) {
1295
1300
  const nextDefaultVault = { ...existingDefaultVault };
1296
1301
  if (parsed.vault) {
1297
1302
  nextDefaultVault.path = shrinkHome(parsed.vault);
@@ -1303,7 +1308,23 @@ function handleVmSetup(req, res) {
1303
1308
  if (typeof parsed.preferences?.autoStart === "boolean") {
1304
1309
  nextDefaultVault.autoStart = parsed.preferences.autoStart;
1305
1310
  }
1311
+ if (typeof parsed.preferences?.autoSync === "boolean") {
1312
+ nextDefaultVault.autoSync = parsed.preferences.autoSync;
1313
+ }
1314
+ if (typeof parsed.preferences?.autoSyncMinLength === "number") {
1315
+ nextDefaultVault.autoSyncMinLength = parsed.preferences.autoSyncMinLength;
1316
+ }
1317
+ if (parsed.folders?.collectionPrefix?.trim()) {
1318
+ nextDefaultVault.collectionPrefix = parsed.folders.collectionPrefix.trim();
1319
+ }
1306
1320
  config.vaultMind.vaults.default = nextDefaultVault;
1321
+ if (parsed.folders?.canvasPath?.trim()) {
1322
+ config.vaultMind.graph = config.vaultMind.graph || {
1323
+ enabled: true,
1324
+ canvasSync: false,
1325
+ };
1326
+ config.vaultMind.graph.canvasPath = parsed.folders.canvasPath.trim();
1327
+ }
1307
1328
  }
1308
1329
  if (parsed.remoteUrl) {
1309
1330
  config.vaultMind.embedding.remoteUrl = parsed.remoteUrl;
@@ -1339,6 +1360,26 @@ function handleVmSetup(req, res) {
1339
1360
  // regardless of whether this config write happened first.
1340
1361
  const scaffold = scaffoldVaultConfig(process.cwd(), parsed.collection);
1341
1362
  scaffoldModelRouterConfig(process.cwd());
1363
+ // Re-resolve vault path from the newly written config
1364
+ const updatedCfg = loadConfig(process.cwd());
1365
+ const defaultVault = updatedCfg.vaultMind.vaults?.default?.path;
1366
+ if (defaultVault && fs.existsSync(defaultVault)) {
1367
+ try {
1368
+ serverState.vaultPath = fs.realpathSync.native(defaultVault);
1369
+ }
1370
+ catch {
1371
+ serverState.vaultPath = path.resolve(defaultVault);
1372
+ }
1373
+ }
1374
+ // Create configured Agent/ folders if they don't exist
1375
+ const folderKeys = ["inbox", "library", "presentations", "journal"];
1376
+ for (const key of folderKeys) {
1377
+ const folderRel = resolveVaultFolder(updatedCfg.vaultMind, key);
1378
+ const folderAbs = path.join(process.cwd(), folderRel);
1379
+ if (!fs.existsSync(folderAbs)) {
1380
+ fs.mkdirSync(folderAbs, { recursive: true });
1381
+ }
1382
+ }
1342
1383
  res.writeHead(200);
1343
1384
  res.end(JSON.stringify({ ok: true, ...scaffold }));
1344
1385
  })
@@ -1418,8 +1459,6 @@ function handleVaultMindConfig(res) {
1418
1459
  const EMBEDDING_SECRET_KINDS = [
1419
1460
  "localApiKey",
1420
1461
  "remoteApiKey",
1421
- "remoteReadApiKey",
1422
- "remoteWriteApiKey",
1423
1462
  ];
1424
1463
  async function handlePutEmbeddingSecrets(req, res) {
1425
1464
  try {
@@ -494,7 +494,7 @@ export const setupWizard = async (ctx, cliArgs) => {
494
494
  ctx.ui.notify([
495
495
  "Modal endpoint discovery:",
496
496
  `- Remote URL: ${remoteUrl}`,
497
- "- Set token with: /vm remote token or ./scripts/fetch-modal-token.sh --write",
497
+ "- Paste the Modal token into the Remote API key field to save it.",
498
498
  ].join("\n"), "info");
499
499
  }
500
500
  else if (provider.startsWith("Custom")) {
@@ -103,16 +103,10 @@ export interface EmbeddingConfig {
103
103
  useTransformers?: boolean;
104
104
  /**
105
105
  * Bearer token for the remote endpoint. Prefer
106
- * `PVM_REMOTE_EMBEDDING_API_KEY` / `PVM_MODAL_*_TOKEN` env vars over
107
- * committing this to config; env vars always win. Falls back to
108
- * `remoteReadApiKey` for read paths and `remoteWriteApiKey` for
109
- * write paths when those scoped overrides are set.
106
+ * `PVM_REMOTE_EMBEDDING_API_KEY` env var over committing this to config;
107
+ * env vars always win.
110
108
  */
111
109
  remoteApiKey?: string;
112
- /** Read-scoped remote token (search/export). Falls back to `remoteApiKey`. */
113
- remoteReadApiKey?: string;
114
- /** Write-scoped remote token (embed/reindex). Falls back to `remoteApiKey`. */
115
- remoteWriteApiKey?: string;
116
110
  /**
117
111
  * Offline fallback policy. When the remote endpoint is unreachable
118
112
  * the search/append paths try a same-space local provider
@@ -355,7 +349,7 @@ export interface AskIntakeResult {
355
349
  path: string;
356
350
  };
357
351
  }
358
- export type EmbeddingSecretKind = "localApiKey" | "remoteApiKey" | "remoteReadApiKey" | "remoteWriteApiKey";
352
+ export type EmbeddingSecretKind = "localApiKey" | "remoteApiKey";
359
353
  export interface EmbeddingSecretStatus {
360
354
  kind: EmbeddingSecretKind;
361
355
  configured: boolean;
@@ -368,8 +362,6 @@ export interface EmbeddingSecretStatusResponse {
368
362
  export interface PutEmbeddingSecretsRequest {
369
363
  localApiKey?: string | null;
370
364
  remoteApiKey?: string | null;
371
- remoteReadApiKey?: string | null;
372
- remoteWriteApiKey?: string | null;
373
365
  }
374
366
  export interface PutEmbeddingSecretsResponse {
375
367
  ok: boolean;
package/dist/src/utils.js CHANGED
@@ -230,7 +230,7 @@ export const resolveVaultMindPaths = (cwd) => {
230
230
  const configPath = getConfigPath(cwd);
231
231
  const tokenEnvPath = getTokenEnvPath(cwd);
232
232
  const modelsJsonPath = path.join(agentDir, "models.json");
233
- const modelRouterPath = path.join(path.dirname(agentDir), "model-router.json");
233
+ const modelRouterPath = path.join(resolveVaultMindDir(cwd), ".pi", "model-router.json");
234
234
  return {
235
235
  vaultPath: cwd,
236
236
  agentDir,
@@ -8,8 +8,6 @@ const PROVIDER_ENV_KEYS = [
8
8
  "PVM_API_TOKEN",
9
9
  "PVM_LOCAL_EMBEDDING_API_KEY",
10
10
  "PVM_REMOTE_EMBEDDING_API_KEY",
11
- "PVM_MODAL_READ_TOKEN",
12
- "PVM_MODAL_WRITE_TOKEN",
13
11
  ];
14
12
  const configWith = (embedding = {}) => ({
15
13
  dataDir: ".lancedb",
@@ -253,43 +251,19 @@ describe("probeEmbeddingEndpoint", () => {
253
251
  request: {
254
252
  target: "local",
255
253
  url: "http://127.0.0.1:11434",
256
- transientSecrets: {
257
- apiKey: "LOCAL_TRANSIENT_SENTINEL",
258
- readApiKey: "REMOTE_READ_TRANSIENT_SENTINEL",
259
- writeApiKey: "REMOTE_WRITE_TRANSIENT_SENTINEL",
260
- },
254
+ transientSecrets: { apiKey: "LOCAL_TRANSIENT_SENTINEL" },
261
255
  },
262
256
  config: configWith({ apiKey: "LOCAL_CONFIG_SENTINEL" }),
263
257
  expectedAuthorization: "Bearer LOCAL_TRANSIENT_SENTINEL",
264
258
  },
265
259
  {
266
- name: "remote prefers the transient readApiKey over unscoped and write keys",
260
+ name: "remote uses only the transient apiKey",
267
261
  request: {
268
262
  target: "remote",
269
263
  url: "https://probe.example.test",
270
- transientSecrets: {
271
- apiKey: "REMOTE_TRANSIENT_SENTINEL",
272
- readApiKey: "REMOTE_READ_TRANSIENT_SENTINEL",
273
- writeApiKey: "REMOTE_WRITE_TRANSIENT_SENTINEL",
274
- },
275
- },
276
- config: configWith({
277
- remoteApiKey: "REMOTE_CONFIG_SENTINEL",
278
- remoteReadApiKey: "REMOTE_READ_CONFIG_SENTINEL",
279
- }),
280
- expectedAuthorization: "Bearer REMOTE_READ_TRANSIENT_SENTINEL",
281
- },
282
- {
283
- name: "remote falls back to the transient unscoped apiKey when readApiKey is absent",
284
- request: {
285
- target: "remote",
286
- url: "https://probe.example.test",
287
- transientSecrets: {
288
- apiKey: "REMOTE_TRANSIENT_SENTINEL",
289
- writeApiKey: "REMOTE_WRITE_TRANSIENT_SENTINEL",
290
- },
264
+ transientSecrets: { apiKey: "REMOTE_TRANSIENT_SENTINEL" },
291
265
  },
292
- config: configWith({ remoteReadApiKey: "REMOTE_READ_CONFIG_SENTINEL" }),
266
+ config: configWith({ remoteApiKey: "REMOTE_CONFIG_SENTINEL" }),
293
267
  expectedAuthorization: "Bearer REMOTE_TRANSIENT_SENTINEL",
294
268
  },
295
269
  ];
@@ -315,8 +289,6 @@ describe("probeEmbeddingEndpoint", () => {
315
289
  'PVM_API_TOKEN="BRIDGE_TOKEN_SENTINEL"',
316
290
  'PVM_LOCAL_EMBEDDING_API_KEY="LOCAL_STORED_SENTINEL"',
317
291
  'PVM_REMOTE_EMBEDDING_API_KEY="REMOTE_STORED_SENTINEL"',
318
- 'PVM_MODAL_READ_TOKEN="REMOTE_READ_STORED_SENTINEL"',
319
- 'PVM_MODAL_WRITE_TOKEN="REMOTE_WRITE_STORED_SENTINEL"',
320
292
  'UNRELATED_SETTING="keep-me"',
321
293
  "",
322
294
  ].join("\n");
@@ -332,12 +304,11 @@ describe("probeEmbeddingEndpoint", () => {
332
304
  };
333
305
  const transientRequest = {
334
306
  ...remoteRequest,
335
- transientSecrets: { readApiKey: "REMOTE_READ_TRANSIENT_SENTINEL" },
307
+ transientSecrets: { apiKey: "REMOTE_TRANSIENT_SENTINEL" },
336
308
  };
337
309
  const config = configWith({
338
310
  apiKey: "LOCAL_LEGACY_SENTINEL",
339
311
  remoteApiKey: "REMOTE_LEGACY_SENTINEL",
340
- remoteReadApiKey: "REMOTE_READ_LEGACY_SENTINEL",
341
312
  });
342
313
  const originalRequests = structuredClone({ localRequest, remoteRequest, transientRequest });
343
314
  const originalConfig = structuredClone(config);
@@ -460,11 +431,11 @@ describe("probeEmbeddingEndpoint", () => {
460
431
  const result = await probeEmbeddingEndpoint({
461
432
  target: "remote",
462
433
  url: "https://probe.example.test",
463
- transientSecrets: { readApiKey: transientSentinel },
434
+ transientSecrets: { apiKey: transientSentinel },
464
435
  }, {
465
436
  fetch,
466
437
  now: () => 12,
467
- config: configWith({ remoteReadApiKey: storedSentinel }),
438
+ config: configWith({ remoteApiKey: storedSentinel }),
468
439
  });
469
440
  assert.equal(result.ok, false);
470
441
  assert.equal(result.error?.code, "provider_error");
@@ -46,7 +46,7 @@ describe("embedding secret resolution and mutation", () => {
46
46
  assert.equal(resolveEmbeddingSecret(cfg, "remoteApiKey", { cwd, environment: {} }), "REMOTE_LEGACY_SENTINEL");
47
47
  });
48
48
  it("secret status reports environment store legacy and none without fragments", () => {
49
- const cfg = makeVaultConfig({ remoteReadApiKey: "LEGACY_READ_SENTINEL" });
49
+ const cfg = makeVaultConfig({ remoteApiKey: "LEGACY_REMOTE_SENTINEL" });
50
50
  writeVaultFile(envPath(cwd), 'PVM_REMOTE_EMBEDDING_API_KEY="REMOTE_PROVIDER_SENTINEL"\n');
51
51
  const result = readEmbeddingSecretStatus(cfg, {
52
52
  cwd,
@@ -66,47 +66,29 @@ describe("embedding secret resolution and mutation", () => {
66
66
  masked: EMBEDDING_SECRET_MASK,
67
67
  source: "extension-secret-store",
68
68
  },
69
- {
70
- kind: "remoteReadApiKey",
71
- configured: true,
72
- masked: EMBEDDING_SECRET_MASK,
73
- source: "legacy-config",
74
- },
75
- {
76
- kind: "remoteWriteApiKey",
77
- configured: false,
78
- masked: null,
79
- source: "none",
80
- },
81
69
  ],
82
70
  });
83
71
  const serialized = JSON.stringify(result);
84
72
  assert.equal(serialized.includes("ENV_LOCAL_SENTINEL"), false);
85
73
  assert.equal(serialized.includes("REMOTE_PROVIDER_SENTINEL"), false);
86
- assert.equal(serialized.includes("LEGACY_READ_SENTINEL"), false);
74
+ assert.equal(serialized.includes("LEGACY_REMOTE_SENTINEL"), false);
87
75
  });
88
76
  it("redactEmbeddingSecrets masks all provider credentials without mutating config", () => {
89
77
  const config = makeUniversalConfig({
90
78
  apiKey: "LOCAL_PROVIDER_SENTINEL",
91
79
  remoteApiKey: "REMOTE_PROVIDER_SENTINEL",
92
- remoteReadApiKey: "REMOTE_READ_SENTINEL",
93
- remoteWriteApiKey: "REMOTE_WRITE_SENTINEL",
94
80
  });
95
81
  const result = redactEmbeddingSecrets(config);
96
82
  assert.deepEqual(result.vaultMind.embedding, {
97
83
  remoteUrl: "https://pvm.modal.run",
98
84
  apiKey: EMBEDDING_SECRET_MASK,
99
85
  remoteApiKey: EMBEDDING_SECRET_MASK,
100
- remoteReadApiKey: EMBEDDING_SECRET_MASK,
101
- remoteWriteApiKey: EMBEDDING_SECRET_MASK,
102
86
  });
103
87
  assert.equal(config.vaultMind.embedding.apiKey, "LOCAL_PROVIDER_SENTINEL");
104
88
  assert.equal(config.vaultMind.embedding.remoteApiKey, "REMOTE_PROVIDER_SENTINEL");
105
89
  const serialized = JSON.stringify(result);
106
90
  assert.equal(serialized.includes("LOCAL_PROVIDER_SENTINEL"), false);
107
91
  assert.equal(serialized.includes("REMOTE_PROVIDER_SENTINEL"), false);
108
- assert.equal(serialized.includes("REMOTE_READ_SENTINEL"), false);
109
- assert.equal(serialized.includes("REMOTE_WRITE_SENTINEL"), false);
110
92
  });
111
93
  it("writeEmbeddingSecrets preserves unrelated dotenv entries and writes mode 0600 on POSIX", () => {
112
94
  writeVaultFile(envPath(cwd), '# operator note\nPVM_API_TOKEN="BRIDGE_ONLY_SENTINEL"\nUNRELATED_SETTING="keep-me"\n');
@@ -126,11 +108,11 @@ describe("embedding secret resolution and mutation", () => {
126
108
  assert.equal(serialized.includes("REMOTE_PROVIDER_SENTINEL"), false);
127
109
  });
128
110
  it("successful mutation response retains untouched legacy secret status", () => {
129
- writeConfig(cwd, makeUniversalConfig({ remoteReadApiKey: "UNTOUCHED_LEGACY_SENTINEL" }));
111
+ writeConfig(cwd, makeUniversalConfig({ apiKey: "UNTOUCHED_LEGACY_SENTINEL" }));
130
112
  const result = writeEmbeddingSecrets({ remoteApiKey: "REMOTE_REPLACEMENT_SENTINEL" }, { cwd, environment: {} });
131
113
  assert.equal(result.ok, true);
132
- assert.deepEqual(result.secrets.find(({ kind }) => kind === "remoteReadApiKey"), {
133
- kind: "remoteReadApiKey",
114
+ assert.deepEqual(result.secrets.find(({ kind }) => kind === "localApiKey"), {
115
+ kind: "localApiKey",
134
116
  configured: true,
135
117
  masked: EMBEDDING_SECRET_MASK,
136
118
  source: "legacy-config",
@@ -143,8 +125,6 @@ describe("embedding secret resolution and mutation", () => {
143
125
  const originalConfig = makeUniversalConfig({
144
126
  apiKey: "LOCAL_LEGACY_SENTINEL",
145
127
  remoteApiKey: "REMOTE_LEGACY_SENTINEL",
146
- remoteReadApiKey: "READ_LEGACY_SENTINEL",
147
- remoteWriteApiKey: "WRITE_LEGACY_SENTINEL",
148
128
  });
149
129
  originalConfig.vaultMind.graph = { enabled: true };
150
130
  writeConfig(cwd, originalConfig);
@@ -152,19 +132,14 @@ describe("embedding secret resolution and mutation", () => {
152
132
  "# preserve me",
153
133
  'PVM_LOCAL_EMBEDDING_API_KEY="LOCAL_STORE_SENTINEL"',
154
134
  'PVM_REMOTE_EMBEDDING_API_KEY="REMOTE_STORE_SENTINEL"',
155
- 'PVM_MODAL_READ_TOKEN="READ_STORE_SENTINEL"',
156
- 'PVM_MODAL_WRITE_TOKEN="WRITE_STORE_SENTINEL"',
157
135
  'UNRELATED_SETTING="keep-me"',
158
136
  "",
159
137
  ].join("\n"));
160
- const result = writeEmbeddingSecrets({ remoteApiKey: null, remoteReadApiKey: "READ_REPLACEMENT_SENTINEL" }, { cwd, environment: {} });
138
+ const result = writeEmbeddingSecrets({ remoteApiKey: null }, { cwd, environment: {} });
161
139
  assert.equal(result.ok, true);
162
140
  const updatedEnv = fs.readFileSync(envPath(cwd), "utf-8");
163
141
  assert.equal(updatedEnv.includes('PVM_LOCAL_EMBEDDING_API_KEY="LOCAL_STORE_SENTINEL"'), true);
164
142
  assert.equal(updatedEnv.includes("PVM_REMOTE_EMBEDDING_API_KEY="), false);
165
- assert.equal(updatedEnv.includes('PVM_MODAL_READ_TOKEN="READ_REPLACEMENT_SENTINEL"'), true);
166
- assert.equal(updatedEnv.includes("READ_STORE_SENTINEL"), false);
167
- assert.equal(updatedEnv.includes('PVM_MODAL_WRITE_TOKEN="WRITE_STORE_SENTINEL"'), true);
168
143
  assert.equal(updatedEnv.includes('UNRELATED_SETTING="keep-me"'), true);
169
144
  const parsedConfig = JSON.parse(fs.readFileSync(configPath(cwd), "utf-8"));
170
145
  assert.deepEqual(parsedConfig, {
@@ -176,23 +151,20 @@ describe("embedding secret resolution and mutation", () => {
176
151
  embedding: {
177
152
  remoteUrl: "https://pvm.modal.run",
178
153
  apiKey: "LOCAL_LEGACY_SENTINEL",
179
- remoteWriteApiKey: "WRITE_LEGACY_SENTINEL",
180
154
  },
181
155
  graph: { enabled: true },
182
156
  },
183
157
  });
184
158
  const serialized = JSON.stringify(result);
185
- assert.equal(serialized.includes("READ_REPLACEMENT_SENTINEL"), false);
186
159
  assert.equal(serialized.includes("REMOTE_STORE_SENTINEL"), false);
187
160
  const envBeforeRejectedWrite = fs.readFileSync(envPath(cwd), "utf-8");
188
161
  const configBeforeRejectedWrite = fs.readFileSync(configPath(cwd), "utf-8");
189
- const rejected = writeEmbeddingSecrets({ localApiKey: "", remoteWriteApiKey: "SHOULD_NOT_COMMIT_SENTINEL" }, { cwd, environment: {} });
162
+ const rejected = writeEmbeddingSecrets({ localApiKey: "" }, { cwd, environment: {} });
190
163
  assert.equal(rejected.ok, false);
191
164
  assert.match(rejected.error ?? "", /empty/i);
192
165
  assert.equal(fs.readFileSync(envPath(cwd), "utf-8"), envBeforeRejectedWrite);
193
166
  assert.equal(fs.readFileSync(configPath(cwd), "utf-8"), configBeforeRejectedWrite);
194
167
  const rejectedSerialized = JSON.stringify(rejected);
195
- assert.equal(rejectedSerialized.includes("SHOULD_NOT_COMMIT_SENTINEL"), false);
196
168
  assert.equal(rejectedSerialized.includes("LOCAL_LEGACY_SENTINEL"), false);
197
169
  });
198
170
  it("config-cleanup failure restores the previous env file", () => {
@@ -3,7 +3,7 @@ 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, beforeEach, describe, it } from "node:test";
6
- import { createModalClient, getModalToken, isModalConfigured, modalTokenEnvPath, namespacedTableName, resolveBaseUrl, resolveDim, resolveModalToken, resolveModel, } from "../src/modal-config.js";
6
+ import { createModalClient, isModalConfigured, modalTokenEnvPath, namespacedTableName, resolveBaseUrl, resolveDim, resolveModalToken, resolveModel, } from "../src/modal-config.js";
7
7
  const makeCfg = (over = {}) => ({
8
8
  dataDir: ".lancedb",
9
9
  embedding: { remoteUrl: "https://pvm.modal.run" },
@@ -14,12 +14,7 @@ const makeCfg = (over = {}) => ({
14
14
  const legacyEmbedding = (fields) => fields;
15
15
  const REMOTE_ENV = "PVM_REMOTE_EMBEDDING_API_KEY";
16
16
  const BRIDGE_ENV = "PVM_API_TOKEN";
17
- const PROVIDER_ENV_KEYS = [
18
- REMOTE_ENV,
19
- BRIDGE_ENV,
20
- "PVM_MODAL_READ_TOKEN",
21
- "PVM_MODAL_WRITE_TOKEN",
22
- ];
17
+ const PROVIDER_ENV_KEYS = [REMOTE_ENV, BRIDGE_ENV];
23
18
  describe("modal-config", () => {
24
19
  let originalCwd;
25
20
  let testDir;
@@ -67,23 +62,15 @@ describe("modal-config", () => {
67
62
  it("PVM_API_TOKEN never resolves a remote provider credential", () => {
68
63
  const cfg = makeCfg();
69
64
  process.env[BRIDGE_ENV] = "BRIDGE_ONLY_SENTINEL";
70
- const fromProcess = [
71
- resolveModalToken(cfg),
72
- getModalToken(cfg, "read"),
73
- getModalToken(cfg, "write"),
74
- ];
65
+ const fromProcess = resolveModalToken(cfg);
75
66
  delete process.env[BRIDGE_ENV];
76
67
  fs.mkdirSync(path.dirname(modalTokenEnvPath()), { recursive: true });
77
68
  fs.writeFileSync(modalTokenEnvPath(), 'PVM_API_TOKEN="BRIDGE_ONLY_SENTINEL"\n', "utf-8");
78
- const fromStore = [
79
- resolveModalToken(cfg),
80
- getModalToken(cfg, "read"),
81
- getModalToken(cfg, "write"),
82
- ];
69
+ const fromStore = resolveModalToken(cfg);
83
70
  const result = { fromProcess, fromStore };
84
71
  assert.deepEqual(result, {
85
- fromProcess: [undefined, undefined, undefined],
86
- fromStore: [undefined, undefined, undefined],
72
+ fromProcess: undefined,
73
+ fromStore: undefined,
87
74
  });
88
75
  assert.equal(JSON.stringify(result).includes("BRIDGE_ONLY_SENTINEL"), false);
89
76
  assert.equal(JSON.stringify(result).includes("REMOTE_PROVIDER_SENTINEL"), false);
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { afterEach, beforeEach, describe, it } from "node:test";
7
7
  import { cancelPersonalization, runPersonalize } from "../src/personalize.js";
8
+ import { getPersonalizedMarkerPath } from "../src/utils.js";
8
9
  const setupConfig = (dir) => {
9
10
  const configDir = path.join(dir, ".vault-mind");
10
11
  fs.mkdirSync(configDir, { recursive: true });
@@ -320,6 +321,30 @@ describe("personalize", () => {
320
321
  const markerPath = path.join(testDir, ".vault-mind", "personalized.json");
321
322
  assert.strictEqual(fs.existsSync(markerPath), false, "runPersonalize must not leave a completion marker after a failed personalization");
322
323
  });
324
+ it("resolves as completed without invoking assistant or confirmation when a durable personalization marker already exists", async () => {
325
+ const markerPath = getPersonalizedMarkerPath(testDir);
326
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
327
+ fs.writeFileSync(markerPath, JSON.stringify({ completed: true, completedAt: new Date().toISOString() }, null, 2), "utf-8");
328
+ let assistantInvoked = false;
329
+ let confirmInvoked = false;
330
+ const pi = {
331
+ on: () => { },
332
+ sendMessage: () => {
333
+ assistantInvoked = true;
334
+ throw new Error("assistant must not be invoked when marker exists");
335
+ },
336
+ };
337
+ const ctx = makeCtx(testDir, []);
338
+ ctx.ui.confirm = async () => {
339
+ confirmInvoked = true;
340
+ throw new Error("confirmation must not be invoked when marker exists");
341
+ };
342
+ const result = await runPersonalize(ctx, pi);
343
+ assert.strictEqual(assistantInvoked, false, "runPersonalize must not invoke the assistant when a durable marker already exists");
344
+ assert.strictEqual(confirmInvoked, false, "runPersonalize must not invoke confirmation UI when a durable marker already exists");
345
+ assert.ok(result, "runPersonalize must return a result, not void");
346
+ assert.strictEqual(result.completed, true, "an existing durable marker must resolve personalization as already completed");
347
+ });
323
348
  it("presentAndApplyDiff creates missing directories for new files", async () => {
324
349
  const { presentAndApplyDiff } = await import("../src/personalize.js");
325
350
  const ctx = makeCtx(testDir, [true]);
@@ -10,8 +10,6 @@ const managerToken = "manager-route-token";
10
10
  const isolatedEnvVars = [
11
11
  "PVM_LOCAL_EMBEDDING_API_KEY",
12
12
  "PVM_REMOTE_EMBEDDING_API_KEY",
13
- "PVM_MODAL_READ_TOKEN",
14
- "PVM_MODAL_WRITE_TOKEN",
15
13
  "PVM_TOKEN_MANAGER",
16
14
  "PVM_TOKEN_MINER",
17
15
  ];
@@ -170,6 +168,23 @@ describe("REST setup routes", () => {
170
168
  const cfg = JSON.parse(fs.readFileSync(getConfigPath(vaultPath), "utf-8"));
171
169
  assert.equal(cfg.vaultMind.vaults.default.autoStart, false);
172
170
  });
171
+ it("POST /vm/setup persists new setup fields on config", async () => {
172
+ const { status, body } = await fetchJson(port, "POST", "/vm/setup", {
173
+ body: {
174
+ vault: vaultPath,
175
+ folders: { collectionPrefix: "notes", canvasPath: "Graph/Canvas.canvas" },
176
+ preferences: { autoSync: false, autoSyncMinLength: 200 },
177
+ },
178
+ token: TEST_TOKEN,
179
+ });
180
+ assert.equal(status, 200);
181
+ assert.equal(body.ok, true);
182
+ const cfg = JSON.parse(fs.readFileSync(getConfigPath(vaultPath), "utf-8"));
183
+ assert.equal(cfg.vaultMind.vaults.default.autoSync, false);
184
+ assert.equal(cfg.vaultMind.vaults.default.autoSyncMinLength, 200);
185
+ assert.equal(cfg.vaultMind.vaults.default.collectionPrefix, "notes");
186
+ assert.equal(cfg.vaultMind.graph.canvasPath, "Graph/Canvas.canvas");
187
+ });
173
188
  it("POST /vm/setup preserves default-vault siblings when toggling autoStart", async () => {
174
189
  writeVaultConfig(vaultPath, {
175
190
  version: 2,
@@ -182,8 +197,10 @@ describe("REST setup routes", () => {
182
197
  autoStart: false,
183
198
  autoSync: false,
184
199
  collectionPrefix: "research",
200
+ autoSyncMinLength: 150,
185
201
  },
186
202
  },
203
+ graph: { enabled: true, canvasSync: false, canvasPath: "Old/Canvas.canvas" },
187
204
  },
188
205
  });
189
206
  const { status, body } = await fetchJson(port, "POST", "/vm/setup", {
@@ -196,6 +213,8 @@ describe("REST setup routes", () => {
196
213
  assert.equal(cfg.vaultMind.vaults.default.autoStart, true);
197
214
  assert.equal(cfg.vaultMind.vaults.default.autoSync, false);
198
215
  assert.equal(cfg.vaultMind.vaults.default.collectionPrefix, "research");
216
+ assert.equal(cfg.vaultMind.vaults.default.autoSyncMinLength, 150);
217
+ assert.equal(cfg.vaultMind.graph.canvasPath, "Old/Canvas.canvas");
199
218
  });
200
219
  it("POST /vm/setup returns 401 without token", async () => {
201
220
  const { status } = await fetchJson(port, "POST", "/vm/setup", {
@@ -245,8 +264,6 @@ describe("REST setup routes", () => {
245
264
  localUrl: "http://127.0.0.1:11434/v1",
246
265
  apiKey: sentinels[0],
247
266
  remoteApiKey: sentinels[1],
248
- remoteReadApiKey: sentinels[2],
249
- remoteWriteApiKey: sentinels[3],
250
267
  },
251
268
  },
252
269
  });
@@ -255,23 +272,19 @@ describe("REST setup routes", () => {
255
272
  });
256
273
  assert.equal(status, 200);
257
274
  const response = body;
258
- for (const key of ["apiKey", "remoteApiKey", "remoteReadApiKey", "remoteWriteApiKey"]) {
275
+ for (const key of ["apiKey", "remoteApiKey"]) {
259
276
  assert.equal(response.config.vaultMind.embedding[key], "••••••••");
260
277
  }
261
278
  assertResponseExcludes(body, sentinels);
262
279
  });
263
- it("GET /vault-mind/config returns four fixed-mask secret statuses", async () => {
280
+ it("GET /vault-mind/config returns two fixed-mask secret statuses", async () => {
264
281
  const sentinels = [
265
282
  "LOCAL-STATUS-SECRET-PREFIX-opaque-END-LOCAL",
266
283
  "REMOTE-STATUS-SECRET-PREFIX-opaque-END-REMOTE",
267
- "READ-STATUS-SECRET-PREFIX-opaque-END-READ",
268
- "WRITE-STATUS-SECRET-PREFIX-opaque-END-WRITE",
269
284
  ];
270
285
  writeSecretEnv(vaultPath, [
271
286
  `PVM_LOCAL_EMBEDDING_API_KEY="${sentinels[0]}"`,
272
287
  `PVM_REMOTE_EMBEDDING_API_KEY="${sentinels[1]}"`,
273
- `PVM_MODAL_READ_TOKEN="${sentinels[2]}"`,
274
- `PVM_MODAL_WRITE_TOKEN="${sentinels[3]}"`,
275
288
  "",
276
289
  ].join("\n"));
277
290
  const { status, body } = await fetchJson(port, "GET", "/vault-mind/config", {
@@ -293,18 +306,6 @@ describe("REST setup routes", () => {
293
306
  masked: "••••••••",
294
307
  source: "extension-secret-store",
295
308
  },
296
- {
297
- kind: "remoteReadApiKey",
298
- configured: true,
299
- masked: "••••••••",
300
- source: "extension-secret-store",
301
- },
302
- {
303
- kind: "remoteWriteApiKey",
304
- configured: true,
305
- masked: "••••••••",
306
- source: "extension-secret-store",
307
- },
308
309
  ],
309
310
  });
310
311
  assertResponseExcludes(body, sentinels);
@@ -348,7 +349,7 @@ describe("REST setup routes", () => {
348
349
  injectors: [],
349
350
  vaultMind: {
350
351
  dataDir: ".lancedb",
351
- embedding: { remoteReadApiKey: legacySecret },
352
+ embedding: { remoteApiKey: legacySecret },
352
353
  graph: { enabled: true },
353
354
  },
354
355
  });
@@ -356,7 +357,7 @@ describe("REST setup routes", () => {
356
357
  const envBefore = fs.readFileSync(secretEnvPath(vaultPath), "utf-8");
357
358
  const configBefore = fs.readFileSync(getConfigPath(vaultPath), "utf-8");
358
359
  const { status, body } = await fetchJson(port, "PUT", "/vm/embedding/secrets", {
359
- body: { localApiKey: attemptedSecret, remoteWriteApiKey: "" },
360
+ body: { remoteApiKey: "" },
360
361
  token: managerToken,
361
362
  });
362
363
  assert.equal(status, 400);
@@ -406,10 +407,8 @@ describe("REST setup routes", () => {
406
407
  });
407
408
  it("PUT /vm/embedding/secrets migrates legacy JSON and preserves unrelated env entries", async () => {
408
409
  const legacyRemote = "LEGACY-REMOTE-SECRET-PREFIX-opaque-END-REMOTE";
409
- const legacyRead = "LEGACY-READ-SECRET-PREFIX-opaque-END-READ";
410
- const untouchedLegacyWrite = "UNTOUCHED-WRITE-SECRET-PREFIX-opaque-END-WRITE";
410
+ const untouchedLegacyLocal = "UNTOUCHED-LOCAL-SECRET-PREFIX-opaque-END-LOCAL";
411
411
  const replacementRemote = "MIGRATED-REMOTE-SECRET-PREFIX-opaque-END-REMOTE";
412
- const replacementRead = "MIGRATED-READ-SECRET-PREFIX-opaque-END-READ";
413
412
  const bridgeToken = "BRIDGE-STORE-SECRET-PREFIX-opaque-END-BRIDGE";
414
413
  process.env.PVM_TOKEN_MANAGER = managerToken;
415
414
  writeVaultConfig(vaultPath, {
@@ -420,16 +419,15 @@ describe("REST setup routes", () => {
420
419
  dataDir: ".lancedb",
421
420
  embedding: {
422
421
  remoteUrl: "https://embedding.example.test",
422
+ apiKey: untouchedLegacyLocal,
423
423
  remoteApiKey: legacyRemote,
424
- remoteReadApiKey: legacyRead,
425
- remoteWriteApiKey: untouchedLegacyWrite,
426
424
  },
427
425
  graph: { enabled: true },
428
426
  },
429
427
  });
430
428
  writeSecretEnv(vaultPath, ["# operator note", `PVM_API_TOKEN="${bridgeToken}"`, 'UNRELATED_SETTING="keep-me"', ""].join("\n"));
431
429
  const { status, body } = await fetchJson(port, "PUT", "/vm/embedding/secrets", {
432
- body: { remoteApiKey: replacementRemote, remoteReadApiKey: replacementRead },
430
+ body: { remoteApiKey: replacementRemote },
433
431
  token: managerToken,
434
432
  });
435
433
  assert.equal(status, 200);
@@ -438,25 +436,21 @@ describe("REST setup routes", () => {
438
436
  assert.equal(stored.includes(`PVM_API_TOKEN="${bridgeToken}"`), true);
439
437
  assert.equal(stored.includes('UNRELATED_SETTING="keep-me"'), true);
440
438
  assert.equal(stored.includes(replacementRemote), true);
441
- assert.equal(stored.includes(replacementRead), true);
442
439
  const migrated = JSON.parse(fs.readFileSync(getConfigPath(vaultPath), "utf-8"));
443
440
  assert.equal("remoteApiKey" in migrated.vaultMind.embedding, false);
444
- assert.equal("remoteReadApiKey" in migrated.vaultMind.embedding, false);
445
- assert.equal(migrated.vaultMind.embedding.remoteWriteApiKey, untouchedLegacyWrite);
441
+ assert.equal(migrated.vaultMind.embedding.apiKey, untouchedLegacyLocal);
446
442
  assert.deepEqual(migrated.vaultMind.graph, { enabled: true });
447
443
  const response = body;
448
- assert.deepEqual(response.secrets.find(({ kind }) => kind === "remoteWriteApiKey"), {
449
- kind: "remoteWriteApiKey",
444
+ assert.deepEqual(response.secrets.find(({ kind }) => kind === "localApiKey"), {
445
+ kind: "localApiKey",
450
446
  configured: true,
451
447
  masked: "••••••••",
452
448
  source: "legacy-config",
453
449
  });
454
450
  assertResponseExcludes(body, [
455
451
  legacyRemote,
456
- legacyRead,
457
- untouchedLegacyWrite,
452
+ untouchedLegacyLocal,
458
453
  replacementRemote,
459
- replacementRead,
460
454
  bridgeToken,
461
455
  ]);
462
456
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vault-mind",
3
- "version": "0.16.11",
3
+ "version": "0.16.13",
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",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Fetch the Modal API token from 1Password and export or persist it.
2
+ # Fetch the Modal embedding API token from 1Password and export or persist it.
3
3
  # Usage:
4
4
  # ./scripts/fetch-modal-token.sh --export # prints an export line
5
5
  # ./scripts/fetch-modal-token.sh --write [vault-path] # writes to <vault>/.vault-mind/vault-mind.env
@@ -32,10 +32,10 @@ if ! command -v op >/dev/null 2>&1; then
32
32
  fi
33
33
 
34
34
  # Check if already exported to avoid clobbering a live session.
35
- if [ -n "${PVM_API_TOKEN:-}" ]; then
36
- echo "⚠️ PVM_API_TOKEN is already exported; skipping 1Password fetch." >&2
35
+ if [ -n "${PVM_REMOTE_EMBEDDING_API_KEY:-}" ]; then
36
+ echo "⚠️ PVM_REMOTE_EMBEDDING_API_KEY is already exported; skipping 1Password fetch." >&2
37
37
  if [ "$mode" = "export" ]; then
38
- echo "export PVM_API_TOKEN=\"${PVM_API_TOKEN}\""
38
+ echo "export PVM_REMOTE_EMBEDDING_API_KEY=\"${PVM_REMOTE_EMBEDDING_API_KEY}\""
39
39
  else
40
40
  echo "Token already available in environment."
41
41
  fi
@@ -55,13 +55,13 @@ if [ -z "$TOKEN" ]; then
55
55
  fi
56
56
 
57
57
  if [ "$mode" = "export" ]; then
58
- echo "export PVM_API_TOKEN=\"${TOKEN}\""
58
+ echo "export PVM_REMOTE_EMBEDDING_API_KEY=\"${TOKEN}\""
59
59
  else
60
60
  mkdir -p "$ENV_DIR"
61
- printf 'PVM_API_TOKEN="%s"\n' "$TOKEN" > "$ENV_FILE"
61
+ printf 'PVM_REMOTE_EMBEDDING_API_KEY="%s"\n' "$TOKEN" > "$ENV_FILE"
62
62
  # chmod is a no-op on Windows (Git Bash / MINGW); skip it to avoid errors.
63
63
  if [[ "$(uname -s)" != MINGW* ]]; then
64
64
  chmod 600 "$ENV_FILE"
65
65
  fi
66
- echo "✅ Wrote PVM_API_TOKEN to ${ENV_FILE}"
66
+ echo "✅ Wrote PVM_REMOTE_EMBEDDING_API_KEY to ${ENV_FILE}"
67
67
  fi
@@ -2,7 +2,7 @@
2
2
  # scripts/reset-test-vault.sh
3
3
  #
4
4
  # Completely reset a vault for walkthrough testing.
5
- # Removes all pi-vault-mind state while preserving vault content and Obsidian plugins.
5
+ # Removes all pi-vault-mind state and runtime artifacts while preserving vault content and Obsidian plugins.
6
6
  #
7
7
  # Usage:
8
8
  # ./scripts/reset-test-vault.sh [vault-path]
@@ -71,19 +71,32 @@ rm -rf "$VAULT/.lancedb"
71
71
  rm -rf "$VAULT/collections"
72
72
  rm -f "$VAULT/_sync_state.json"
73
73
  rm -rf "$VAULT/.vault-mind"
74
- rm -f "$VAULT/.obsidian/plugins/vault-mind/data.json"
75
- rm -f "$HOME/.pi/agent/vault-mind.config.json" # remove global config — vault-scoped only
74
+ rm -rf "$VAULT/.omp"
75
+ rm -rf "$VAULT/Pi-Sessions"
76
+ rm -f "$VAULT/AGENTS.md"
77
+ rm -f "$VAULT/.env.1pass"
78
+ # Remove Agent subdirectories scaffolded by setup, preserving user content
79
+ for dir in Inbox Library Presentations Journal; do
80
+ rm -rf "$VAULT/Agent/$dir"
81
+ done
82
+ # Remove Agent root only if empty after subdirectory cleanup
83
+ rmdir "$VAULT/Agent" 2>/dev/null || true
84
+ rm -rf "$VAULT/.obsidian/plugins/vault-mind"
76
85
 
77
86
 
78
87
  echo ""
79
88
  echo "✓ Vault reset complete. State removed:"
89
+ echo " - .omp/ (harness runtime state)"
80
90
  echo " - .vault-mind/ (config, token, framework agent dir, queue)"
91
+ echo " - .obsidian/plugins/vault-mind/ (plugin, reinstall via BRAT)"
81
92
  echo " - .pi/ (legacy agent dir, if present)"
82
93
  echo " - .lancedb/ (vector index)"
83
94
  echo " - collections/ (JSONL data)"
95
+ echo " - Pi-Sessions/ (session files)"
96
+ echo " - AGENTS.md (personalized agent delegation)"
97
+ echo " - .env.1pass (runtime config)"
98
+ echo " - Agent/ subdirectories (Inbox, Library, Presentations, Journal)"
84
99
  echo ""
85
100
  echo "Preserved:"
86
101
  echo " - Vault content (notes, folders)"
87
- echo " - .obsidian/ (plugins, themes, settings; BRAT owns plugin updates)"
88
- echo ""
89
102
  echo "Next: Pull the released plugin through BRAT, then open Vault Mind → Setup tab"