pi-vault-mind 0.16.7 → 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,25 @@
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
+
3
23
  ## 0.16.7 / 0.6.10 — 2026-07-18
4
24
 
5
25
  ### Fixed
package/README.md CHANGED
@@ -17,8 +17,8 @@ Passive Obsidian vault extension for the [pi](https://github.com/mariozechner/pi
17
17
  - **LanceDB Vector + FTS + Graph** — hybrid semantic + keyword search with automatic entity extraction and BFS graph traversal. All local, no external binaries.
18
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`.
19
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.
20
- - **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.
21
- - **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.
22
22
 
23
23
  ## Architecture
24
24
 
@@ -210,19 +210,20 @@ pi install git:git@github.com:kylebrodeur/pi-vault-mind
210
210
  /vm setup
211
211
  ```
212
212
 
213
- 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.
214
215
 
215
- Or via CLI for scripting:
216
+ Or via CLI for scripting/repair:
216
217
 
217
218
  ```bash
218
- /vm setup --vault /home/you/Obsidian/MyVault --provider transformers
219
- /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
220
220
  ```
221
221
 
222
- Config is written to `~/.pi/agent/vault-mind.config.json` it applies globally
223
- 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.
224
224
 
225
- 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.
226
227
 
227
228
  ### 3. Start using
228
229
 
@@ -235,7 +236,7 @@ Entries are automatically embedded and stored in LanceDB. If graph is enabled, e
235
236
 
236
237
  ### 4. Adapt the config
237
238
 
238
- Edit `pi-vault-mind.config.json` to match your domain:
239
+ Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
239
240
 
240
241
  ```json
241
242
  {
@@ -344,7 +345,7 @@ Edit `pi-vault-mind.config.json` to match your domain:
344
345
  | Command | Purpose |
345
346
  | ------------------------------ | -------------------------------------------------------- |
346
347
  | `/vm help` | Show usage help |
347
- | `/vm setup` | **Interactive global config wizard** (vault, embedding) |
348
+ | `/vm setup` | Interactive vault-local setup/config wizard (runtime, embedding, scaffold) |
348
349
  | `/vm validate` | Health check LanceDB, config, and all collection paths |
349
350
  | `/vm approve [collection]` | Batch-review pending entries |
350
351
  | `/vm settings` | Open interactive settings dashboard |
@@ -405,7 +406,7 @@ full design.
405
406
  | [docs/CHANGELOG.md](docs/CHANGELOG.md) | Version history (rename from `pi-knowledge-store` to `pi-vault-mind` was v0.7.0) |
406
407
  | [Tools reference](docs/_reference/reference_archive/tools.md) | Registered Vault Mind tools, parameters, and return shapes |
407
408
  | [Commands reference](docs/_reference/reference_archive/commands.md) | Full `/vm` slash command tree |
408
- | [Configuration reference](docs/_reference/reference_archive/configuration.md) | Complete `pi-vault-mind.config.json` schema |
409
+ | [Setup and configuration](docs/reference/setup-and-configuration.md) | Current integrated setup/configuration surface, ownership, routes, and live gaps |
409
410
  | [Skill manifest](docs/_reference/reference_archive/skill.md) | Bundled skills and their trigger phrases |
410
411
 
411
412
  ### Development
@@ -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
  }>;