pi-vault-mind 0.16.10 → 0.16.12

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,35 @@
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
+
21
+ ## 0.16.11 / 0.6.15 — 2026-07-20
22
+
23
+ ### Fixed
24
+
25
+ - **Panel recovery after bridge startup.** A later panel reconciliation now rehydrates the same Queue, Activity, session, collection, pending-edit, and Git metadata that initial controller startup loads, so a bridge that becomes available after the panel mounted no longer leaves those surfaces permanently stale.
26
+ - **Deterministic ReturnVape reset.** `reset-test-vault.sh` verifies the PID and port registered in `.vault-mind/server.json` serve the target vault before terminating and waiting for that runtime. A stale registry cannot signal an unrelated PID. The reset no longer copies local plugin artifacts; BRAT is the only test-vault update path.
27
+
28
+ ### Verification
29
+
30
+ - Added focused regressions for panel metadata recovery after an initial bridge outage, verified runtime termination, and stale-registry safety.
31
+
32
+
3
33
  ## 0.16.10 / 0.6.14 — 2026-07-20
4
34
 
5
35
  ### Added
@@ -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;