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 +18 -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 +18 -5
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", "
|
|
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;
|
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(
|
|
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
|
|
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({
|
|
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: {
|
|
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: {
|
|
434
|
+
transientSecrets: { apiKey: transientSentinel },
|
|
464
435
|
}, {
|
|
465
436
|
fetch,
|
|
466
437
|
now: () => 12,
|
|
467
|
-
config: configWith({
|
|
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({
|
|
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("
|
|
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({
|
|
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 === "
|
|
133
|
-
kind: "
|
|
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
|
|
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: ""
|
|
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,
|
|
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:
|
|
86
|
-
fromStore:
|
|
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"
|
|
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
|
|
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: {
|
|
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: {
|
|
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
|
|
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
|
|
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(
|
|
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 === "
|
|
449
|
-
kind: "
|
|
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
|
-
|
|
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.
|
|
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 "${
|
|
36
|
-
echo "⚠️
|
|
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
|
|
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
|
|
58
|
+
echo "export PVM_REMOTE_EMBEDDING_API_KEY=\"${TOKEN}\""
|
|
59
59
|
else
|
|
60
60
|
mkdir -p "$ENV_DIR"
|
|
61
|
-
printf '
|
|
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
|
|
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 -
|
|
75
|
-
rm -
|
|
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"
|