pi-vault-mind 0.16.6 → 0.16.8
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 +43 -0
- package/README.md +22 -46
- package/dist/packages/obsidian/src/client.js +20 -1
- package/dist/src/auth.d.ts +6 -1
- package/dist/src/auth.js +46 -7
- package/dist/src/embedding-probe.js +29 -3
- package/dist/src/model-router.d.ts +11 -0
- package/dist/src/model-router.js +120 -2
- package/dist/src/model-utils.d.ts +10 -0
- package/dist/src/model-utils.js +24 -0
- package/dist/src/personalize.d.ts +8 -3
- package/dist/src/personalize.js +341 -66
- package/dist/src/scaffold.js +4 -4
- package/dist/src/server.d.ts +2 -1
- package/dist/src/server.js +72 -11
- package/dist/src/utils.js +7 -4
- package/dist/test/config-merge.test.js +16 -19
- package/dist/test/embedding-probe.test.js +82 -20
- package/dist/test/model-router.test.js +79 -1
- package/dist/test/model-utils.test.js +100 -0
- package/dist/test/personalize.test.js +643 -13
- package/dist/test/rest-vm.test.js +47 -9
- package/dist/test/server-ws.test.js +55 -0
- package/dist/test/settings-ui.test.js +14 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.8 / 0.6.11 — 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Persistent agent-model sequence editing.** The extension owns authenticated `GET/PUT /vm/model-router` routes, the typed client and configuration adapter expose the contract, and Settings now edits one primary model plus ordered fallbacks through the shared `ModelSequenceEditor` without writing Pi's model catalog.
|
|
8
|
+
- **Explicit personalization cancellation.** The first-run Personalize card now exposes a separate Cancel action while work is active. Cancellation invalidates the controller attempt, uses Pi's existing abort transport, restores the idle model/action controls immediately, and prevents a stale completion from revealing chat or writing the personalization marker.
|
|
9
|
+
- **Durable chat diff lifecycle.** Persisted vault-edit events hydrate actionable `DiffMessage` cards after reconnect, apply/reject actions retain their direction across retry, and the client authenticates its WebSocket connection. The remaining dedicated full-review/MergeView experience is tracked separately in the roadmap.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Embedding-only discovery catalogs.** Ollama and OpenAI-compatible probe results now retain explicit or known embedding-capable models, exclude known chat/generative families, preserve exact IDs/dimensions/order, and keep a configured current selection available without polluting newly discovered choices.
|
|
14
|
+
- **Personalization parser and write boundary.** Aborted assistant results are classified as cancellation rather than malformed JSON; normally completed malformed output still reports an error. Proposal parsing validates the complete two-file schema, preserves fenced Markdown and escaped content, rejects duplicate or alternate paths and empty create/update content, and permits writes only to the resolved agent-dir `system.md` and vault-root `AGENTS.md`.
|
|
15
|
+
- **Configuration persistence integrity.** Model-router configuration is written atomically; collection/injector configuration mutations preserve sibling vault configuration and explicit injector opt-outs rather than reintroducing scaffold defaults.
|
|
16
|
+
- **Portable chat state restoration.** Diff-card and composer lifecycle updates now propagate reactive completion/retry state through both the sandbox source and the Obsidian port without stale one-shot snapshots.
|
|
17
|
+
|
|
18
|
+
### Verified
|
|
19
|
+
|
|
20
|
+
- Completed the ReturnVape seven-step setup walkthrough, Local embedding discovery, populated folder defaults, the enabled context-automation choice, setup save, configured-panel handoff, first-run model selection, Cancel recovery, proposal review, and durable personalization completion in Obsidian 1.12.7 on macOS. The known Auto-start persistence defect remains tracked separately.
|
|
21
|
+
- Added focused regressions for model-router round trips, mixed-provider embedding filtering, configuration merge/injector invariants, persisted diff hydration and retry direction, composer completion, cancellation races, malformed-versus-aborted personalization output, strict proposal schema/path validation, and fenced Markdown preservation.
|
|
22
|
+
|
|
23
|
+
## 0.16.7 / 0.6.10 — 2026-07-18
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Obsidian Markdown lifecycle.** Assistant Markdown renders through Obsidian's native renderer, renderer components unload on panel close or remount, stale asynchronous opens cannot remount a closed panel, and deferred message fills cannot hijack a replacement message placeholder.
|
|
28
|
+
- **Generated sandbox constants.** Root builds now format the sandbox extension-package catalog they regenerate, so the release build cannot invalidate the sandbox verification suite.
|
|
29
|
+
- **Repository-only documentation.** Removed the obsolete MkDocs configuration, GitHub Pages deployment workflow, publication badges, and generated-site instructions while retaining maintained Markdown documentation in the repository.
|
|
30
|
+
|
|
31
|
+
### Tests
|
|
32
|
+
|
|
33
|
+
- Added focused regressions for Markdown cleanup ownership, direct remount disposal, close-during-refresh cancellation, and deferred placeholder isolation.
|
|
34
|
+
|
|
35
|
+
## 0.6.9 — 2026-07-18
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **First-run personalization recovery.** The configured-vault onboarding card now retains its active personalization state until durable runtime status confirms completion, including when the prompt acknowledgement fails or a permission request is pending. The chat feed and composer remain correctly gated until then.
|
|
40
|
+
- **Portable model selection.** The shared panel contract now passes opaque model IDs end-to-end; the Obsidian adapter resolves each ID to Pi's provider/model RPC payload internally, preserving IDs that contain `/` or `:`.
|
|
41
|
+
|
|
42
|
+
### Tests
|
|
43
|
+
|
|
44
|
+
- Added focused regressions for acknowledgement failure, pending permission delivery, re-entry protection, and exact model-ID selection ordering.
|
|
45
|
+
|
|
3
46
|
## 0.16.6 / 0.6.8 — 2026-07-18
|
|
4
47
|
|
|
5
48
|
### Fixed
|
package/README.md
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/pi-vault-mind)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/mariozechner/pi)
|
|
6
|
-
[](https://kylebrodeur.github.io/pi-vault-mind/)
|
|
7
|
-
[](https://github.com/kylebrodeur/pi-vault-mind/actions/workflows/docs.yml)
|
|
8
6
|
|
|
9
7
|
Passive Obsidian vault extension for the [pi](https://github.com/mariozechner/pi) agent ecosystem. Watches `@agent` markers in your vault, dispatches forked subagents, and stores results in LanceDB with vector + FTS + graph. Multi-agent "Drop & Forget" workflow.
|
|
10
8
|
|
|
@@ -19,8 +17,8 @@ Passive Obsidian vault extension for the [pi](https://github.com/mariozechner/pi
|
|
|
19
17
|
- **LanceDB Vector + FTS + Graph** — hybrid semantic + keyword search with automatic entity extraction and BFS graph traversal. All local, no external binaries.
|
|
20
18
|
- **JSONL Source-of-Truth + LanceDB Index** — every fact lives in a durable, human-readable, version-control-friendly `collections/*.jsonl` file. The LanceDB index is derived and rebuildable via `/vm reindex --all --reembed`.
|
|
21
19
|
- **Bidirectional Obsidian Sync** — substantial entries (`>200 chars` or tagged `decision`/`insight`/`requirement`) auto-write to `Vault/Agent/Inbox/`. Graph entities render as Obsidian Canvas files.
|
|
22
|
-
- **
|
|
23
|
-
- **Interactive Setup Wizard** —
|
|
20
|
+
- **Vault-Scoped Configuration Surface** — Vault Mind setup/config now lives under `<vault>/.vault-mind/`, with the Obsidian setup wizard and `/vm setup` both writing the vault-local surface instead of a shared global config.
|
|
21
|
+
- **Interactive Setup Wizard** — the Obsidian setup wizard handles first-run runtime/install/provider/folder/preferences/review flow, and `/vm setup` remains available for CLI/repair use.
|
|
24
22
|
|
|
25
23
|
## Architecture
|
|
26
24
|
|
|
@@ -212,19 +210,20 @@ pi install git:git@github.com:kylebrodeur/pi-vault-mind
|
|
|
212
210
|
/vm setup
|
|
213
211
|
```
|
|
214
212
|
|
|
215
|
-
This
|
|
213
|
+
This configures the current vault's setup surface: vault path, embedding
|
|
214
|
+
provider details, folder layout, and scaffolding.
|
|
216
215
|
|
|
217
|
-
Or via CLI for scripting:
|
|
216
|
+
Or via CLI for scripting/repair:
|
|
218
217
|
|
|
219
218
|
```bash
|
|
220
|
-
/vm setup --vault /home/you/Obsidian/MyVault --
|
|
221
|
-
/vm setup --vault /home/you/Obsidian/MyVault --provider ollama --model embeddinggemma
|
|
219
|
+
/vm setup --vault /home/you/Obsidian/MyVault --remoteUrl https://your-embedding-service-url.example.com --model embeddinggemma
|
|
222
220
|
```
|
|
223
221
|
|
|
224
|
-
Config is written to
|
|
225
|
-
|
|
222
|
+
Config is written to the vault-local Vault Mind surface under
|
|
223
|
+
`<vault>/.vault-mind/` rather than a shared global config file.
|
|
226
224
|
|
|
227
|
-
You can re-run `/vm setup` anytime to view or change settings
|
|
225
|
+
You can re-run `/vm setup` anytime to view or change settings, and the
|
|
226
|
+
Obsidian plugin's setup wizard uses the same extension-owned routes.
|
|
228
227
|
|
|
229
228
|
### 3. Start using
|
|
230
229
|
|
|
@@ -237,7 +236,7 @@ Entries are automatically embedded and stored in LanceDB. If graph is enabled, e
|
|
|
237
236
|
|
|
238
237
|
### 4. Adapt the config
|
|
239
238
|
|
|
240
|
-
Edit
|
|
239
|
+
Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
|
|
241
240
|
|
|
242
241
|
```json
|
|
243
242
|
{
|
|
@@ -346,7 +345,7 @@ Edit `pi-vault-mind.config.json` to match your domain:
|
|
|
346
345
|
| Command | Purpose |
|
|
347
346
|
| ------------------------------ | -------------------------------------------------------- |
|
|
348
347
|
| `/vm help` | Show usage help |
|
|
349
|
-
| `/vm setup` |
|
|
348
|
+
| `/vm setup` | Interactive vault-local setup/config wizard (runtime, embedding, scaffold) |
|
|
350
349
|
| `/vm validate` | Health check LanceDB, config, and all collection paths |
|
|
351
350
|
| `/vm approve [collection]` | Batch-review pending entries |
|
|
352
351
|
| `/vm settings` | Open interactive settings dashboard |
|
|
@@ -363,19 +362,16 @@ Edit `pi-vault-mind.config.json` to match your domain:
|
|
|
363
362
|
|
|
364
363
|
## Documentation
|
|
365
364
|
|
|
366
|
-
|
|
367
|
-
> The links below point to the source files in this repository.
|
|
365
|
+
Documentation is maintained directly in this repository.
|
|
368
366
|
|
|
369
367
|
### Getting started
|
|
370
368
|
|
|
371
369
|
| Doc | Description |
|
|
372
370
|
|---|---|
|
|
373
|
-
| [
|
|
374
|
-
| [
|
|
375
|
-
| [
|
|
376
|
-
| [
|
|
377
|
-
| [docs/getting-started/CLI_ONLY_WALKTHROUGH.md](docs/getting-started/CLI_ONLY_WALKTHROUGH.md) | Same setup + daily commands, entirely CLI — no Obsidian plugin required |
|
|
378
|
-
| [docs/getting-started/WALKTHROUGH_PROMPT.md](docs/getting-started/WALKTHROUGH_PROMPT.md) | Paste-into-pi guided setup with checkpoints between phases |
|
|
371
|
+
| [Install playbook](docs/_reference/reference_archive/getting-started/INSTALL.md) | Canonical install procedure for the extension, skills, Obsidian plugin, configuration, and external CLIs |
|
|
372
|
+
| [Getting started](docs/_reference/reference_archive/getting-started/GETTING_STARTED.md) | End-to-end setup and daily “drop and forget” workflow |
|
|
373
|
+
| [CLI-only walkthrough](docs/_reference/reference_archive/getting-started/CLI_ONLY_WALKTHROUGH.md) | Setup and daily commands without the Obsidian plugin |
|
|
374
|
+
| [ReturnVape walkthrough](docs/getting-started/WALKTHROUGH.md) | Current guided Obsidian test-vault procedure |
|
|
379
375
|
|
|
380
376
|
### Architecture & design
|
|
381
377
|
|
|
@@ -384,8 +380,6 @@ Edit `pi-vault-mind.config.json` to match your domain:
|
|
|
384
380
|
| [docs/architecture/AGENTS.md](docs/architecture/AGENTS.md) | Agent Roster and Multi-Agent Architecture ("Fork & Review" model) |
|
|
385
381
|
| [docs/architecture/EXTENSION_WIRING.md](docs/architecture/EXTENSION_WIRING.md) | Extension dependencies, runtime wiring, auto-install patterns |
|
|
386
382
|
| [docs/architecture/DISPATCHER_SPEC.md](docs/architecture/DISPATCHER_SPEC.md) | Technical spec for the passive file-watcher and subagent routing — incl. the "Fork & Dispatch" rationale and thread resume |
|
|
387
|
-
| [docs/architecture/AGENTS.md](docs/architecture/AGENTS.md) | Agent roster + the Miner's "Drop & Forget" ingestion pipeline |
|
|
388
|
-
| [docs/integrations/OBSIDIAN_SETUP.md](docs/integrations/OBSIDIAN_SETUP.md) | Recommended Obsidian vault structure, plugins, and CLI |
|
|
389
383
|
|
|
390
384
|
### Modal embedding service (local integration done)
|
|
391
385
|
|
|
@@ -401,8 +395,6 @@ full design.
|
|
|
401
395
|
| Doc | Description |
|
|
402
396
|
|---|---|
|
|
403
397
|
| [docs/integrations/MODAL_EMBEDDING.md](docs/integrations/MODAL_EMBEDDING.md) | Design of record: ADRs, HTTP contract, sync protocol, roadmap for the cloud embedding service |
|
|
404
|
-
| [docs/plans/modal-embedding-plan.md](docs/plans/modal-embedding-plan.md) | **Consolidated plan & next steps** — phases, owners, status, decision gate (in repo, not on the docs site) |
|
|
405
|
-
| [docs/plans/embedding-open-questions.md](docs/plans/embedding-open-questions.md) | Embedding-strategy decision log (decided + open questions) |
|
|
406
398
|
| [modal/](modal/) | The deployable Modal app: embedding service + bulk worker + sync + dataset generator |
|
|
407
399
|
| [eval/](eval/) | Retrieval eval harness + labeled benchmark datasets for picking the canonical model |
|
|
408
400
|
|
|
@@ -412,17 +404,10 @@ full design.
|
|
|
412
404
|
|---|---|
|
|
413
405
|
| [skills/vault-mind/SKILL.md](skills/vault-mind/SKILL.md) | The Manager skill — what pi auto-loads about this extension |
|
|
414
406
|
| [docs/CHANGELOG.md](docs/CHANGELOG.md) | Version history (rename from `pi-knowledge-store` to `pi-vault-mind` was v0.7.0) |
|
|
415
|
-
| [
|
|
416
|
-
| [
|
|
417
|
-
| [
|
|
418
|
-
| [
|
|
419
|
-
|
|
420
|
-
### Testing
|
|
421
|
-
|
|
422
|
-
| Doc | Description |
|
|
423
|
-
|---|---|
|
|
424
|
-
| [docs/testing/TESTING.md](docs/testing/TESTING.md) | Test plan for agent / human / HITL personas, regression suite |
|
|
425
|
-
| [docs/testing/E2E_MANUAL_TEST.md](docs/testing/E2E_MANUAL_TEST.md) | Manual end-to-end verification procedure (watcher → dispatch → vault) |
|
|
407
|
+
| [Tools reference](docs/_reference/reference_archive/tools.md) | Registered Vault Mind tools, parameters, and return shapes |
|
|
408
|
+
| [Commands reference](docs/_reference/reference_archive/commands.md) | Full `/vm` slash command tree |
|
|
409
|
+
| [Setup and configuration](docs/reference/setup-and-configuration.md) | Current integrated setup/configuration surface, ownership, routes, and live gaps |
|
|
410
|
+
| [Skill manifest](docs/_reference/reference_archive/skill.md) | Bundled skills and their trigger phrases |
|
|
426
411
|
|
|
427
412
|
### Development
|
|
428
413
|
|
|
@@ -430,22 +415,13 @@ full design.
|
|
|
430
415
|
|---|---|
|
|
431
416
|
| [docs/development/CONTRIBUTING.md](docs/development/CONTRIBUTING.md) | Dev setup, testing, and commit conventions |
|
|
432
417
|
| [docs/development/PUBLISHING.md](docs/development/PUBLISHING.md) | How to publish this extension to npm |
|
|
433
|
-
| [docs/development/FUTURE_WORK.md](docs/development/FUTURE_WORK.md) | Roadmap — codegraph integration, pagination, TUI rendering, etc. |
|
|
434
|
-
|
|
435
|
-
### Research
|
|
436
|
-
|
|
437
|
-
| Doc | Description |
|
|
438
|
-
|---|---|
|
|
439
|
-
| [docs/research/COMPETITOR_COMPARISON.md](docs/research/COMPETITOR_COMPARISON.md) | Tier 1/2/3 comparison vs. other Obsidian-LLM tools (22 competitors) |
|
|
440
|
-
| [docs/research/naming-decisions.md](docs/research/naming-decisions.md) | Historical record of the 2026-06-06 decision to name the project `pi-vault-mind` |
|
|
441
|
-
| [docs/research/obsidian-links-reviewed.csv](docs/research/obsidian-links-reviewed.csv) | Curated subset of starred Obsidian repos with adoption verdicts |
|
|
442
418
|
|
|
443
419
|
### Archive
|
|
444
420
|
|
|
445
421
|
| Doc | Description |
|
|
446
422
|
|---|---|
|
|
447
423
|
| [docs/_archive/](docs/_archive/) | Historical docs kept for context (e.g. the `pi-knowledge-store` → `pi-vault-mind` rename audit) |
|
|
448
|
-
| [docs/_archive/legacy-audit.md](docs/_archive/legacy-audit.md) | The 2026-06-08 legacy-terminology audit (139 findings, 13 blockers) and its resolution log. Resolved 2026-06-09
|
|
424
|
+
| [docs/_archive/legacy-audit.md](docs/_archive/legacy-audit.md) | The 2026-06-08 legacy-terminology audit (139 findings, 13 blockers) and its resolution log. Resolved 2026-06-09 and archived 2026-06-16. |
|
|
449
425
|
|
|
450
426
|
## Contributing
|
|
451
427
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
const encodeWebSocketAuthProtocol = (token) => {
|
|
2
|
+
const bytes = new TextEncoder().encode(token);
|
|
3
|
+
let binary = "";
|
|
4
|
+
for (const byte of bytes)
|
|
5
|
+
binary += String.fromCharCode(byte);
|
|
6
|
+
const encoded = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
7
|
+
return `pvm-auth.${encoded}`;
|
|
8
|
+
};
|
|
1
9
|
/**
|
|
2
10
|
* HTTP + WebSocket client for the pi-vault-mind extension.
|
|
3
11
|
*
|
|
@@ -58,7 +66,7 @@ export class VaultMindClient {
|
|
|
58
66
|
if (this.ws)
|
|
59
67
|
return;
|
|
60
68
|
try {
|
|
61
|
-
this.ws = new WebSocket(this.wsUrl,
|
|
69
|
+
this.ws = new WebSocket(this.wsUrl, encodeWebSocketAuthProtocol(this.config.token));
|
|
62
70
|
}
|
|
63
71
|
catch (err) {
|
|
64
72
|
this.setState({ connected: false, error: String(err), reconnecting: true });
|
|
@@ -144,6 +152,9 @@ export class VaultMindClient {
|
|
|
144
152
|
async setup(body) {
|
|
145
153
|
return (await this.httpJson("POST", "/vm/setup", body));
|
|
146
154
|
}
|
|
155
|
+
async cancelPersonalization() {
|
|
156
|
+
return (await this.httpJson("POST", "/vm/personalize/cancel"));
|
|
157
|
+
}
|
|
147
158
|
async probeEmbedding(body) {
|
|
148
159
|
return (await this.httpJson("POST", "/vm/embedding/probe", body));
|
|
149
160
|
}
|
|
@@ -286,6 +297,14 @@ export class VaultMindClient {
|
|
|
286
297
|
async getModels() {
|
|
287
298
|
return (await this.httpJson("GET", "/vault-mind/models"));
|
|
288
299
|
}
|
|
300
|
+
/** GET /vm/model-router — ordered provider/model primary and fallback identities. */
|
|
301
|
+
async getModelRouter() {
|
|
302
|
+
return (await this.httpJson("GET", "/vm/model-router"));
|
|
303
|
+
}
|
|
304
|
+
/** PUT /vm/model-router — replace the ordered primary/fallback sequence. */
|
|
305
|
+
async putModelRouter(config) {
|
|
306
|
+
return (await this.httpJson("PUT", "/vm/model-router", config));
|
|
307
|
+
}
|
|
289
308
|
/** POST /vm/token — write PVM_API_TOKEN to vault-mind.env */
|
|
290
309
|
async writeToken(token) {
|
|
291
310
|
return (await this.httpJson("POST", "/vm/token", { token }));
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare const resolveTokenIdentity: (token: string) => TokenIdentity | un
|
|
|
35
35
|
export interface IncomingLike {
|
|
36
36
|
headers: {
|
|
37
37
|
authorization?: string | string[] | undefined;
|
|
38
|
+
"sec-websocket-protocol"?: string | string[] | undefined;
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
export interface AuthResult {
|
|
@@ -46,7 +47,11 @@ export interface AuthResult {
|
|
|
46
47
|
/**
|
|
47
48
|
* Authorize a request against configured tokens.
|
|
48
49
|
*
|
|
49
|
-
*
|
|
50
|
+
* Credential resolution order:
|
|
51
|
+
* 1. A normal `Authorization: Bearer <token>` header.
|
|
52
|
+
* 2. An exact `pvm-auth.<base64url(UTF-8 token)>` WebSocket protocol.
|
|
53
|
+
*
|
|
54
|
+
* Token matching order:
|
|
50
55
|
* 1. Per-role `PVM_TOKEN_<ROLE>` env vars — returns the matched identity.
|
|
51
56
|
* 2. Legacy `PVM_API_TOKEN` — returns `ok: true` with no identity (full access).
|
|
52
57
|
* 3. No token configured → `ok: true` (auth is off).
|
package/dist/src/auth.js
CHANGED
|
@@ -89,10 +89,31 @@ const safeEqual = (a, b) => {
|
|
|
89
89
|
}
|
|
90
90
|
return mismatch === 0;
|
|
91
91
|
};
|
|
92
|
+
const WEBSOCKET_AUTH_PROTOCOL_PREFIX = "pvm-auth.";
|
|
93
|
+
const decodeWebSocketAuthProtocol = (protocol) => {
|
|
94
|
+
if (!protocol.startsWith(WEBSOCKET_AUTH_PROTOCOL_PREFIX))
|
|
95
|
+
return null;
|
|
96
|
+
const encoded = protocol.slice(WEBSOCKET_AUTH_PROTOCOL_PREFIX.length);
|
|
97
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded) || encoded.length % 4 === 1)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
const bytes = Buffer.from(encoded, "base64url");
|
|
101
|
+
if (bytes.toString("base64url") !== encoded)
|
|
102
|
+
return null;
|
|
103
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
92
109
|
/**
|
|
93
110
|
* Authorize a request against configured tokens.
|
|
94
111
|
*
|
|
95
|
-
*
|
|
112
|
+
* Credential resolution order:
|
|
113
|
+
* 1. A normal `Authorization: Bearer <token>` header.
|
|
114
|
+
* 2. An exact `pvm-auth.<base64url(UTF-8 token)>` WebSocket protocol.
|
|
115
|
+
*
|
|
116
|
+
* Token matching order:
|
|
96
117
|
* 1. Per-role `PVM_TOKEN_<ROLE>` env vars — returns the matched identity.
|
|
97
118
|
* 2. Legacy `PVM_API_TOKEN` — returns `ok: true` with no identity (full access).
|
|
98
119
|
* 3. No token configured → `ok: true` (auth is off).
|
|
@@ -106,12 +127,30 @@ export const authoriseRequest = (req) => {
|
|
|
106
127
|
// No tokens configured — auth is off, allow all requests regardless of header
|
|
107
128
|
return { ok: true };
|
|
108
129
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
let token;
|
|
131
|
+
if (raw !== undefined) {
|
|
132
|
+
if (!header)
|
|
133
|
+
return { ok: false, reason: "missing" };
|
|
134
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
135
|
+
if (!match)
|
|
136
|
+
return { ok: false, reason: "malformed" };
|
|
137
|
+
token = match[1];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const rawProtocol = req.headers["sec-websocket-protocol"];
|
|
141
|
+
if (rawProtocol === undefined)
|
|
142
|
+
return { ok: false, reason: "missing" };
|
|
143
|
+
if (Array.isArray(rawProtocol) && rawProtocol.length !== 1) {
|
|
144
|
+
return { ok: false, reason: "malformed" };
|
|
145
|
+
}
|
|
146
|
+
const protocol = Array.isArray(rawProtocol) ? rawProtocol[0] : rawProtocol;
|
|
147
|
+
if (protocol === undefined)
|
|
148
|
+
return { ok: false, reason: "malformed" };
|
|
149
|
+
const decoded = decodeWebSocketAuthProtocol(protocol);
|
|
150
|
+
if (decoded === null)
|
|
151
|
+
return { ok: false, reason: "malformed" };
|
|
152
|
+
token = decoded;
|
|
153
|
+
}
|
|
115
154
|
// 1. Per-role token match
|
|
116
155
|
const identity = resolveTokenIdentity(token);
|
|
117
156
|
if (identity)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveEmbeddingSecret } from "./embedding-secrets.js";
|
|
2
|
+
import { getModelCapabilities } from "./model-utils.js";
|
|
2
3
|
const normalizeEndpointUrl = (url) => {
|
|
3
4
|
const suffixes = ["/v1/embeddings", "/v1", "/"];
|
|
4
5
|
let base = url;
|
|
@@ -43,15 +44,40 @@ const isOpenAiCatalog = (body) => {
|
|
|
43
44
|
return false;
|
|
44
45
|
return body.data.every((d) => isObject(d) && hasStringField(d, "id"));
|
|
45
46
|
};
|
|
47
|
+
const catalogModelCapabilities = (model) => isObject(model.capabilities)
|
|
48
|
+
? getModelCapabilities({ id: model.id, capabilities: model.capabilities })
|
|
49
|
+
: getModelCapabilities({ id: model.id });
|
|
50
|
+
const hasExplicitCapabilities = (capabilities) => isObject(capabilities) &&
|
|
51
|
+
(typeof capabilities.chat === "boolean" || typeof capabilities.embedding === "boolean");
|
|
52
|
+
const KNOWN_OPENAI_EMBEDDING_FAMILY = /(^|[/_:.-])(?:e5|gte|instructor)(?=$|[/_:.-])/;
|
|
53
|
+
const isOpenAiEmbeddingChoice = (model) => {
|
|
54
|
+
const capabilities = catalogModelCapabilities(model);
|
|
55
|
+
const id = model.id.toLowerCase();
|
|
56
|
+
if (capabilities.embedding)
|
|
57
|
+
return true;
|
|
58
|
+
if (hasExplicitCapabilities(model.capabilities))
|
|
59
|
+
return false;
|
|
60
|
+
if (KNOWN_OPENAI_EMBEDDING_FAMILY.test(id))
|
|
61
|
+
return true;
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
46
64
|
const normalizeModels = (body) => {
|
|
47
65
|
if (isOllamaCatalog(body)) {
|
|
48
|
-
return body.models
|
|
66
|
+
return body.models
|
|
67
|
+
.filter((model) => catalogModelCapabilities({ id: model.name, capabilities: model.capabilities }).embedding)
|
|
68
|
+
.map((model) => ({ id: model.name, name: model.name, dim: null }));
|
|
49
69
|
}
|
|
50
70
|
if (isModalCatalog(body)) {
|
|
51
|
-
return body.models.map((
|
|
71
|
+
return body.models.map((model) => ({
|
|
72
|
+
id: model.key,
|
|
73
|
+
name: model.key,
|
|
74
|
+
dim: model.native_dim,
|
|
75
|
+
}));
|
|
52
76
|
}
|
|
53
77
|
if (isOpenAiCatalog(body)) {
|
|
54
|
-
return body.data
|
|
78
|
+
return body.data
|
|
79
|
+
.filter(isOpenAiEmbeddingChoice)
|
|
80
|
+
.map((model) => ({ id: model.id, name: model.id, dim: null }));
|
|
55
81
|
}
|
|
56
82
|
return null;
|
|
57
83
|
};
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
* equivalent extension-exposed path) in a future refactor rather than
|
|
11
11
|
* maintaining its own copy.
|
|
12
12
|
*/
|
|
13
|
+
export interface ModelRouterModelReference {
|
|
14
|
+
providerId: string;
|
|
15
|
+
modelId: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ModelRouterConfig {
|
|
18
|
+
sequence: ModelRouterModelReference[];
|
|
19
|
+
}
|
|
13
20
|
/** Tuned default primary model + fallback sequence for the `auto` router profile. */
|
|
14
21
|
export declare const defaultModelRouterChoices: () => {
|
|
15
22
|
primary: string;
|
|
@@ -25,3 +32,7 @@ export declare const scaffoldModelRouterConfig: (cwd: string) => {
|
|
|
25
32
|
created: boolean;
|
|
26
33
|
path: string;
|
|
27
34
|
};
|
|
35
|
+
/** Read the active ordered primary/fallback sequence without creating or modifying its file. */
|
|
36
|
+
export declare const readModelRouterConfig: (cwd: string) => ModelRouterConfig;
|
|
37
|
+
/** Replace only the active ordered primary/fallback sequence, preserving every other router option. */
|
|
38
|
+
export declare const writeModelRouterConfig: (cwd: string, value: unknown) => ModelRouterConfig;
|
package/dist/src/model-router.js
CHANGED
|
@@ -10,8 +10,72 @@
|
|
|
10
10
|
* equivalent extension-exposed path) in a future refactor rather than
|
|
11
11
|
* maintaining its own copy.
|
|
12
12
|
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
13
14
|
import * as fs from "node:fs";
|
|
14
15
|
import { ensureDir, resolveVaultMindPaths } from "./utils.js";
|
|
16
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
const writeModelRouterFile = (filePath, value) => {
|
|
18
|
+
ensureDir(filePath);
|
|
19
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
|
|
22
|
+
fs.renameSync(temporaryPath, filePath);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
try {
|
|
26
|
+
fs.unlinkSync(temporaryPath);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const parseModelReference = (value) => {
|
|
33
|
+
const separator = value.indexOf("/");
|
|
34
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
35
|
+
throw new Error(`Invalid model-router model reference: ${value}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
providerId: value.slice(0, separator),
|
|
39
|
+
modelId: value.slice(separator + 1),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const serializeModelReference = (reference) => `${reference.providerId}/${reference.modelId}`;
|
|
43
|
+
const readPrimaryModel = (config) => {
|
|
44
|
+
const profileName = typeof config.defaultProfile === "string" && config.defaultProfile.length > 0
|
|
45
|
+
? config.defaultProfile
|
|
46
|
+
: "auto";
|
|
47
|
+
const profiles = isRecord(config.profiles) ? config.profiles : {};
|
|
48
|
+
const profile = isRecord(profiles[profileName]) ? profiles[profileName] : {};
|
|
49
|
+
for (const tier of ["high", "medium", "low"]) {
|
|
50
|
+
const tierConfig = profile[tier];
|
|
51
|
+
if (isRecord(tierConfig) && typeof tierConfig.model === "string") {
|
|
52
|
+
return tierConfig.model;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
const validateModelRouterConfig = (value) => {
|
|
58
|
+
if (!isRecord(value) || !Array.isArray(value.sequence) || value.sequence.length === 0) {
|
|
59
|
+
throw new Error("Model-router sequence must contain at least one model.");
|
|
60
|
+
}
|
|
61
|
+
const sequence = value.sequence.map((entry, index) => {
|
|
62
|
+
if (!isRecord(entry)) {
|
|
63
|
+
throw new Error(`Model-router sequence entry ${index} must be an object.`);
|
|
64
|
+
}
|
|
65
|
+
const { providerId, modelId } = entry;
|
|
66
|
+
if (typeof providerId !== "string" ||
|
|
67
|
+
providerId.length === 0 ||
|
|
68
|
+
providerId.trim() !== providerId ||
|
|
69
|
+
providerId.includes("/")) {
|
|
70
|
+
throw new Error(`Model-router sequence entry ${index} has an invalid providerId.`);
|
|
71
|
+
}
|
|
72
|
+
if (typeof modelId !== "string" || modelId.length === 0 || modelId.trim() !== modelId) {
|
|
73
|
+
throw new Error(`Model-router sequence entry ${index} has an invalid modelId.`);
|
|
74
|
+
}
|
|
75
|
+
return { providerId, modelId };
|
|
76
|
+
});
|
|
77
|
+
return { sequence };
|
|
78
|
+
};
|
|
15
79
|
/** Tuned default primary model + fallback sequence for the `auto` router profile. */
|
|
16
80
|
export const defaultModelRouterChoices = () => {
|
|
17
81
|
return {
|
|
@@ -63,7 +127,61 @@ export const scaffoldModelRouterConfig = (cwd) => {
|
|
|
63
127
|
},
|
|
64
128
|
},
|
|
65
129
|
};
|
|
66
|
-
|
|
67
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
130
|
+
writeModelRouterFile(configPath, config);
|
|
68
131
|
return { created: true, path: configPath };
|
|
69
132
|
};
|
|
133
|
+
/** Read the active ordered primary/fallback sequence without creating or modifying its file. */
|
|
134
|
+
export const readModelRouterConfig = (cwd) => {
|
|
135
|
+
const { modelRouter: configPath } = resolveVaultMindPaths(cwd);
|
|
136
|
+
if (!fs.existsSync(configPath)) {
|
|
137
|
+
const { fallbackSequence } = defaultModelRouterChoices();
|
|
138
|
+
return { sequence: fallbackSequence.map(parseModelReference) };
|
|
139
|
+
}
|
|
140
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
141
|
+
if (!isRecord(parsed))
|
|
142
|
+
throw new Error("Invalid model-router configuration.");
|
|
143
|
+
const fallback = isRecord(parsed.rateLimitFallback)
|
|
144
|
+
? parsed.rateLimitFallback.fallbackSequence
|
|
145
|
+
: undefined;
|
|
146
|
+
let serializedSequence = [];
|
|
147
|
+
if (Array.isArray(fallback)) {
|
|
148
|
+
if (!fallback.every((value) => typeof value === "string")) {
|
|
149
|
+
throw new Error("Model-router fallback sequence contains a non-string entry.");
|
|
150
|
+
}
|
|
151
|
+
serializedSequence = [...fallback];
|
|
152
|
+
}
|
|
153
|
+
const primary = readPrimaryModel(parsed);
|
|
154
|
+
if (primary && serializedSequence[0] !== primary)
|
|
155
|
+
serializedSequence.unshift(primary);
|
|
156
|
+
if (serializedSequence.length === 0) {
|
|
157
|
+
throw new Error("Model-router configuration has no active model sequence.");
|
|
158
|
+
}
|
|
159
|
+
return { sequence: serializedSequence.map(parseModelReference) };
|
|
160
|
+
};
|
|
161
|
+
/** Replace only the active ordered primary/fallback sequence, preserving every other router option. */
|
|
162
|
+
export const writeModelRouterConfig = (cwd, value) => {
|
|
163
|
+
const request = validateModelRouterConfig(value);
|
|
164
|
+
const { modelRouter: configPath } = resolveVaultMindPaths(cwd);
|
|
165
|
+
scaffoldModelRouterConfig(cwd);
|
|
166
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
167
|
+
if (!isRecord(parsed))
|
|
168
|
+
throw new Error("Invalid model-router configuration.");
|
|
169
|
+
const profileName = typeof parsed.defaultProfile === "string" && parsed.defaultProfile.length > 0
|
|
170
|
+
? parsed.defaultProfile
|
|
171
|
+
: "auto";
|
|
172
|
+
const primary = serializeModelReference(request.sequence[0]);
|
|
173
|
+
const profiles = isRecord(parsed.profiles) ? parsed.profiles : {};
|
|
174
|
+
const profile = isRecord(profiles[profileName]) ? profiles[profileName] : {};
|
|
175
|
+
for (const tier of ["high", "medium", "low"]) {
|
|
176
|
+
const tierConfig = isRecord(profile[tier]) ? profile[tier] : {};
|
|
177
|
+
profile[tier] = { ...tierConfig, model: primary };
|
|
178
|
+
}
|
|
179
|
+
parsed.profiles = { ...profiles, [profileName]: profile };
|
|
180
|
+
const rateLimitFallback = isRecord(parsed.rateLimitFallback) ? parsed.rateLimitFallback : {};
|
|
181
|
+
parsed.rateLimitFallback = {
|
|
182
|
+
...rateLimitFallback,
|
|
183
|
+
fallbackSequence: request.sequence.map(serializeModelReference),
|
|
184
|
+
};
|
|
185
|
+
writeModelRouterFile(configPath, parsed);
|
|
186
|
+
return readModelRouterConfig(cwd);
|
|
187
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ModelCapabilitySource {
|
|
2
|
+
id: string;
|
|
3
|
+
capabilities?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface ModelCapabilities {
|
|
6
|
+
chat: boolean;
|
|
7
|
+
embedding: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Resolve explicit capability metadata first, then known embedding-only model families. */
|
|
10
|
+
export declare function getModelCapabilities(model: ModelCapabilitySource): ModelCapabilities;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const KNOWN_EMBEDDING_MODELS = [
|
|
2
|
+
"embeddinggemma",
|
|
3
|
+
"nomic-embed-text",
|
|
4
|
+
"mxbai-embed-large",
|
|
5
|
+
"all-minilm",
|
|
6
|
+
"paraphrase-multilingual",
|
|
7
|
+
];
|
|
8
|
+
/** Resolve explicit capability metadata first, then known embedding-only model families. */
|
|
9
|
+
export function getModelCapabilities(model) {
|
|
10
|
+
const explicitChat = model.capabilities?.chat;
|
|
11
|
+
const explicitEmbedding = model.capabilities?.embedding;
|
|
12
|
+
if (typeof explicitChat === "boolean" || typeof explicitEmbedding === "boolean") {
|
|
13
|
+
const embedding = typeof explicitEmbedding === "boolean" ? explicitEmbedding : false;
|
|
14
|
+
return {
|
|
15
|
+
chat: typeof explicitChat === "boolean" ? explicitChat : !embedding,
|
|
16
|
+
embedding,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const id = model.id.toLowerCase();
|
|
20
|
+
const knownModel = KNOWN_EMBEDDING_MODELS.some((name) => id.includes(name));
|
|
21
|
+
const embeddingFamily = /(^|[/_:.-])(?:bge-|embed(?:ding)?(?:$|[/_:.-]))/.test(id);
|
|
22
|
+
const embedding = knownModel || embeddingFamily;
|
|
23
|
+
return { chat: !embedding, embedding };
|
|
24
|
+
}
|
|
@@ -27,19 +27,24 @@ export interface SuggestionResult {
|
|
|
27
27
|
commentary?: string;
|
|
28
28
|
files: FileSuggestion[];
|
|
29
29
|
}
|
|
30
|
+
export interface PersonalizationCancelResult {
|
|
31
|
+
cancelled: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Abort the currently active personalization for a resolved vault, if one exists. */
|
|
34
|
+
export declare const cancelPersonalization: (vaultPath: string) => PersonalizationCancelResult;
|
|
30
35
|
/** Resolve the vault path: configured default vault wins, otherwise ctx.cwd. */
|
|
31
36
|
export declare const resolveVaultPath: (ctx: ExtensionContext) => string;
|
|
32
37
|
/** Load current agent configuration files if they exist. */
|
|
33
38
|
export declare const loadCurrentConfig: (vaultPath: string) => ConfigFiles;
|
|
34
39
|
/** Analyze the vault structure, tags, and LanceDB state. */
|
|
35
|
-
export declare const analyzeVaultProfile: (ctx: ExtensionContext) => Promise<VaultProfile>;
|
|
40
|
+
export declare const analyzeVaultProfile: (ctx: ExtensionContext, signal?: AbortSignal) => Promise<VaultProfile>;
|
|
36
41
|
export declare const parseSuggestionJson: (text: string) => SuggestionResult;
|
|
37
42
|
/** Ask the current session model to suggest personalized config updates. */
|
|
38
|
-
export declare const generateSuggestions: (pi: ExtensionAPI, profile: VaultProfile, currentConfig: ConfigFiles) => Promise<SuggestionResult>;
|
|
43
|
+
export declare const generateSuggestions: (pi: ExtensionAPI, profile: VaultProfile, currentConfig: ConfigFiles, signal?: AbortSignal) => Promise<SuggestionResult>;
|
|
39
44
|
/** Format old vs new as a markdown diff block. */
|
|
40
45
|
export declare const formatDiff: (current: string | null, proposed: string) => string;
|
|
41
46
|
/** Present each suggestion diff to the user and write approved files. */
|
|
42
|
-
export declare const presentAndApplyDiff: (suggestions: SuggestionResult, ctx: ExtensionContext, currentConfig: ConfigFiles) => Promise<{
|
|
47
|
+
export declare const presentAndApplyDiff: (suggestions: SuggestionResult, ctx: ExtensionContext, currentConfig: ConfigFiles, signal?: AbortSignal) => Promise<{
|
|
43
48
|
approved: string[];
|
|
44
49
|
rejected: string[];
|
|
45
50
|
}>;
|