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 +30 -0
- package/dist/src/config-keys.d.ts +1 -1
- package/dist/src/config-keys.js +0 -2
- package/dist/src/embedding-probe.d.ts +0 -2
- package/dist/src/embedding-probe.js +2 -8
- package/dist/src/embedding-secrets.js +1 -8
- package/dist/src/lance.js +26 -5
- package/dist/src/modal-client.d.ts +4 -10
- package/dist/src/modal-client.js +21 -26
- package/dist/src/modal-config.d.ts +2 -20
- package/dist/src/modal-config.js +5 -37
- package/dist/src/personalize.js +9 -1
- package/dist/src/scaffold.js +16 -0
- package/dist/src/server.js +45 -6
- package/dist/src/settings-ui.js +1 -1
- package/dist/src/types.d.ts +3 -11
- package/dist/src/utils.js +1 -1
- package/dist/test/embedding-probe.test.js +7 -36
- package/dist/test/embedding-secrets.test.js +7 -35
- package/dist/test/modal-config.test.js +6 -19
- package/dist/test/personalize.test.js +25 -0
- package/dist/test/rest-setup.test.js +32 -38
- package/package.json +1 -1
- package/scripts/fetch-modal-token.sh +7 -7
- package/scripts/reset-test-vault.sh +64 -19
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", "
|
|
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"];
|
package/dist/src/config-keys.js
CHANGED
|
@@ -99,17 +99,11 @@ const resolveAuthorizationHeader = (request, cfg) => {
|
|
|
99
99
|
return resolveEmbeddingSecret(cfg, "localApiKey");
|
|
100
100
|
return undefined;
|
|
101
101
|
}
|
|
102
|
-
// Remote
|
|
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
|
|
266
|
-
|
|
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
|
|
489
|
-
|
|
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
|
-
/**
|
|
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
|
|
113
|
-
private
|
|
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
|
|
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 /
|
package/dist/src/modal-client.js
CHANGED
|
@@ -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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|
|
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
|
-
}
|
|
86
|
+
});
|
|
92
87
|
}
|
|
93
88
|
jobStatus(jobId) {
|
|
94
|
-
return this.request("GET", `/jobs/${encodeURIComponent(jobId)}`, undefined
|
|
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
|
|
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
|
|
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
|
|
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
|
|
128
|
+
return this.request("GET", `/sync/export?${p.toString()}`, undefined);
|
|
134
129
|
}
|
|
135
|
-
/** Low-level binary request helper
|
|
136
|
-
async requestBuffer(method, path
|
|
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.
|
|
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()}
|
|
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
|
|
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
|
|
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
|
package/dist/src/modal-config.js
CHANGED
|
@@ -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
|
|
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
|
|
100
|
-
export const isModalConfigured = (cfg) => !!cfg.embedding.remoteUrl &&
|
|
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
|
-
|
|
111
|
-
const writeToken = getModalToken(cfg, "write");
|
|
112
|
-
if (!apiToken && !readToken && !writeToken)
|
|
80
|
+
if (!apiToken)
|
|
113
81
|
return null;
|
|
114
|
-
const clientCfg = { baseUrl, apiToken
|
|
82
|
+
const clientCfg = { baseUrl, apiToken };
|
|
115
83
|
return new ModalEmbeddingClient(clientCfg);
|
|
116
84
|
};
|
|
117
85
|
/**
|
package/dist/src/personalize.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/scaffold.js
CHANGED
|
@@ -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);
|
package/dist/src/server.js
CHANGED
|
@@ -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 ||
|
|
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 {
|
package/dist/src/settings-ui.js
CHANGED
|
@@ -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
|
-
"-
|
|
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")) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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`
|
|
107
|
-
*
|
|
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"
|
|
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;
|