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 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
  [![npm version](https://img.shields.io/npm/v/pi-vault-mind)](https://www.npmjs.com/package/pi-vault-mind)
4
4
  [![license](https://img.shields.io/npm/l/pi-vault-mind)](LICENSE)
5
5
  [![pi-extension](https://img.shields.io/badge/pi-extension-blue)](https://github.com/mariozechner/pi)
6
- [![docs](https://img.shields.io/badge/docs-pi--vault--mind-blue)](https://kylebrodeur.github.io/pi-vault-mind/)
7
- [![docs build](https://github.com/kylebrodeur/pi-vault-mind/actions/workflows/docs.yml/badge.svg)](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
- - **Two-Layer Config** — global (`~/.pi/agent/vault-mind.config.json`) for shared settings, project (`./pi-vault-mind.config.json`) for project-specific knowledge. Works across all projects from any directory.
23
- - **Interactive Setup Wizard** — `/vm setup` walks through vault path, embedding provider, and model selection. CLI mode: `--vault`, `--provider`, `--model` flags for scripting.
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 walks you through: vault path embedding provider → Ollama model (if applicable).
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 --provider transformers
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 `~/.pi/agent/vault-mind.config.json` it applies globally
225
- no matter which project directory you open pi from.
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 `pi-vault-mind.config.json` to match your domain:
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` | **Interactive global config wizard** (vault, embedding) |
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
- > **Full docs site:** [kylebrodeur.github.io/pi-vault-mind](https://kylebrodeur.github.io/pi-vault-mind/) (GH Pages, built from [`docs/`](docs/)).
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
- | [docs/getting-started/QUICKSTART.md](docs/getting-started/QUICKSTART.md) | **Fastest path** 60-second install, setup, first append |
374
- | [docs/getting-started/NEW_VAULT_WALKTHROUGH.md](docs/getting-started/NEW_VAULT_WALKTHROUGH.md) | **Fresh Mac new vault Modal sync → verified**, one linear path (incl. multi-vault storage isolation) |
375
- | [docs/getting-started/INSTALL.md](docs/getting-started/INSTALL.md) | **Canonical install playbook** all 5 layers (pi ext, skills, Obsidian, config, external CLIs) |
376
- | [docs/getting-started/GETTING_STARTED.md](docs/getting-started/GETTING_STARTED.md) | End-to-end workflow + daily "drop & forget" usage |
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
- | [docs/reference/tools.md](docs/reference/tools.md) | The 11 `vm_*` and `vm_query`/`vm_append`/etc. tools parameters, return shapes |
416
- | [docs/reference/commands.md](docs/reference/commands.md) | Full `/vm` slash command tree |
417
- | [docs/reference/configuration.md](docs/reference/configuration.md) | The complete `pi-vault-mind.config.json` schema |
418
- | [docs/reference/skill.md](docs/reference/skill.md) | Manifest of all bundled skills and their trigger phrases |
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; archived 2026-06-16 (in the repo, not on the docs site) |
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, [`Authorization: Bearer ${this.config.token}`]);
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 }));
@@ -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
- * Resolution order:
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
- * Resolution order:
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
- if (!header)
110
- return { ok: false, reason: "missing" };
111
- const m = header.match(/^Bearer\s+(.+)$/i);
112
- if (!m)
113
- return { ok: false, reason: "malformed" };
114
- const token = m[1];
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.map((m) => ({ id: m.name, name: m.name, dim: null }));
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((m) => ({ id: m.key, name: m.key, dim: m.native_dim }));
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.map((d) => ({ id: d.id, name: d.id, dim: null }));
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;
@@ -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
- ensureDir(configPath);
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
  }>;