pi-vault-mind 0.8.0 → 0.8.3

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +44 -44
  3. package/agents/broadcaster.agent.md +30 -0
  4. package/agents/heavy-lifter.agent.md +30 -0
  5. package/agents/manager.agent.md +29 -0
  6. package/agents/miner.agent.md +29 -0
  7. package/dist/src/agent-bus/Agent.d.ts +49 -0
  8. package/dist/src/agent-bus/Agent.js +97 -0
  9. package/dist/src/agent-bus/AgentLoader.d.ts +33 -0
  10. package/dist/src/agent-bus/AgentLoader.js +102 -0
  11. package/dist/src/agent-bus/AgentRegistry.d.ts +17 -0
  12. package/dist/src/agent-bus/AgentRegistry.js +20 -0
  13. package/dist/src/agent-bus/LLMProvider.d.ts +36 -0
  14. package/dist/src/agent-bus/LLMProvider.js +125 -0
  15. package/dist/src/agent-bus/MessageBus.d.ts +47 -0
  16. package/dist/src/agent-bus/MessageBus.js +81 -0
  17. package/dist/src/agent-bus/TaskQueue.d.ts +27 -0
  18. package/dist/src/agent-bus/TaskQueue.js +57 -0
  19. package/dist/src/agent-bus/index.d.ts +6 -0
  20. package/dist/src/agent-bus/index.js +6 -0
  21. package/dist/src/agents/BroadcasterAgent.d.ts +19 -0
  22. package/dist/src/agents/BroadcasterAgent.js +70 -0
  23. package/dist/src/agents/HeavyLifterAgent.d.ts +26 -0
  24. package/dist/src/agents/HeavyLifterAgent.js +73 -0
  25. package/dist/src/agents/ManagerAgent.d.ts +20 -0
  26. package/dist/src/agents/ManagerAgent.js +71 -0
  27. package/dist/src/agents/MinerAgent.d.ts +19 -0
  28. package/dist/src/agents/MinerAgent.js +70 -0
  29. package/dist/src/agents/index.d.ts +4 -0
  30. package/dist/src/agents/index.js +4 -0
  31. package/dist/src/auth.d.ts +49 -2
  32. package/dist/src/auth.js +149 -7
  33. package/dist/src/autosync.d.ts +1 -1
  34. package/dist/src/autosync.js +3 -3
  35. package/dist/src/bridge.d.ts +45 -0
  36. package/dist/src/bridge.js +189 -0
  37. package/dist/src/commands.d.ts +1 -1
  38. package/dist/src/commands.js +419 -295
  39. package/dist/src/context-capture.d.ts +22 -0
  40. package/dist/src/context-capture.js +41 -0
  41. package/dist/src/discover-schema.d.ts +1 -1
  42. package/dist/src/discover-schema.js +4 -4
  43. package/dist/src/embed-queue.d.ts +1 -1
  44. package/dist/src/embed-queue.js +1 -1
  45. package/dist/src/engine.d.ts +65 -0
  46. package/dist/src/engine.js +244 -0
  47. package/dist/src/events.js +1 -1
  48. package/dist/src/graph.d.ts +3 -3
  49. package/dist/src/identity-injector.d.ts +15 -0
  50. package/dist/src/identity-injector.js +102 -0
  51. package/dist/src/index.js +88 -49
  52. package/dist/src/intake.js +4 -4
  53. package/dist/src/lance.d.ts +11 -37
  54. package/dist/src/lance.js +183 -240
  55. package/dist/src/modal-client.d.ts +14 -4
  56. package/dist/src/modal-client.js +50 -34
  57. package/dist/src/modal-config.d.ts +26 -10
  58. package/dist/src/modal-config.js +36 -5
  59. package/dist/src/personalize.d.ts +47 -0
  60. package/dist/src/personalize.js +352 -0
  61. package/dist/src/server.d.ts +30 -4
  62. package/dist/src/server.js +146 -64
  63. package/dist/src/settings-ui.d.ts +3 -1
  64. package/dist/src/settings-ui.js +84 -77
  65. package/dist/src/status.d.ts +25 -0
  66. package/dist/src/status.js +49 -0
  67. package/dist/src/sync.d.ts +1 -1
  68. package/dist/src/sync.js +19 -19
  69. package/dist/src/tombstone.js +2 -2
  70. package/dist/src/tools.js +112 -130
  71. package/dist/src/types.d.ts +86 -18
  72. package/dist/src/types.js +7 -4
  73. package/dist/src/utils.d.ts +5 -1
  74. package/dist/src/utils.js +24 -8
  75. package/dist/src/vault-tools.d.ts +29 -0
  76. package/dist/src/vault-tools.js +314 -0
  77. package/dist/src/vault-writer.d.ts +4 -5
  78. package/dist/src/vault-writer.js +13 -13
  79. package/dist/src/vm-handlers.d.ts +15 -0
  80. package/dist/src/vm-handlers.js +469 -0
  81. package/dist/src/watcher.d.ts +18 -4
  82. package/dist/src/watcher.js +109 -98
  83. package/dist/test/auth.test.js +57 -7
  84. package/dist/test/bridge-identity.test.js +388 -0
  85. package/dist/test/discover-schema.test.js +2 -2
  86. package/dist/test/engine-identity.test.js +119 -0
  87. package/dist/test/index.test.js +5 -6
  88. package/dist/test/lance-modal.test.js +7 -7
  89. package/dist/test/modal-config.test.js +51 -29
  90. package/dist/test/personalize.test.js +212 -0
  91. package/dist/test/rest-queue.test.js +1 -1
  92. package/dist/test/rest-setup.test.js +21 -21
  93. package/dist/test/{rest-wiki.test.js → rest-vm.test.js} +27 -23
  94. package/dist/test/server-ws.test.js +4 -4
  95. package/dist/test/sync.test.js +2 -2
  96. package/dist/test/tombstone.test.js +1 -1
  97. package/dist/test/vault-tools.test.js +224 -0
  98. package/package.json +2 -1
  99. package/scripts/check-deps.sh +207 -0
  100. package/scripts/e2e-ws.mjs +4 -4
  101. package/scripts/modal-e2e-smoke.mjs +1 -1
  102. package/scripts/setup-test-config.sh +1 -5
  103. package/scripts/test-slash-commands.mjs +197 -0
  104. package/skills/.agents/broadcaster.agent.md +31 -0
  105. package/skills/.agents/heavy-lifter.agent.md +31 -0
  106. package/skills/.agents/manager.agent.md +30 -0
  107. package/skills/.agents/miner.agent.md +30 -0
  108. package/skills/vault-mind/SKILL.md +50 -50
  109. package/skills/vault-mind/references/tool-reference.md +25 -25
  110. package/skills/vault-mind-broadcaster/SKILL.md +1 -1
  111. package/skills/vault-mind-heavy-lifter/SKILL.md +3 -3
  112. package/skills/vault-mind-manager/SKILL.md +7 -7
  113. package/skills/vault-mind-miner/SKILL.md +11 -11
  114. package/skills/vault-mind-setup/SKILL.md +65 -265
  115. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +6 -6
  116. package/skills/vault-mind-setup/references/pi-extension-wiring.md +24 -33
  117. package/skills/vault-mind-setup/references/troubleshooting-tree.md +25 -26
  118. package/dist/src/auto-index.d.ts +0 -47
  119. package/dist/src/auto-index.js +0 -122
@@ -13,13 +13,17 @@
13
13
  * This client is the typed mirror of the server contract. The server (Agent A)
14
14
  * owns it; additive changes here are mirrored in the server's `modal/web.py`.
15
15
  * The local wiring lives in `src/lance.ts` (provider), `src/sync.ts`, and
16
- * `/wiki modal` commands (see docs/MODAL_EMBEDDING.md "Local integration").
16
+ * `/vm remote` commands (see docs/MODAL_EMBEDDING.md "Local integration").
17
17
  */
18
18
  export interface ModalClientConfig {
19
19
  /** Base URL of the deployed ASGI app (no trailing slash needed). */
20
20
  baseUrl: string;
21
- /** Bearer token matching the `pi-vault-mind-auth` Modal secret. */
22
- apiToken: string;
21
+ /** Fallback bearer token (legacy single token for all operations). */
22
+ apiToken?: string;
23
+ /** Read-scoped token for search/export operations. Falls back to `apiToken`. */
24
+ readToken?: string;
25
+ /** Write-scoped token for embed/reindex operations. Falls back to `apiToken`. */
26
+ writeToken?: string;
23
27
  /** Per-request timeout in ms (default 120s — bulk submits can be large). */
24
28
  timeoutMs?: number;
25
29
  }
@@ -101,8 +105,12 @@ export interface ExportPage {
101
105
  export declare class ModalEmbeddingClient {
102
106
  private baseUrl;
103
107
  private apiToken;
108
+ private readToken;
109
+ private writeToken;
104
110
  private timeoutMs;
105
111
  constructor(cfg: ModalClientConfig);
112
+ /** Return the bearer token to use for the requested operation scope. */
113
+ private tokenFor;
106
114
  private request;
107
115
  /** Liveness check; also returns the server's default model. */
108
116
  health(): Promise<{
@@ -128,7 +136,7 @@ export declare class ModalEmbeddingClient {
128
136
  }): Promise<JobSubmitResult>;
129
137
  jobStatus(jobId: string): Promise<JobStatus>;
130
138
  /** List recent jobs (newest first). Additive — surfaces GET /jobs so
131
- * `/wiki modal jobs` can list, not just poll a known id. (Agent B request #1.) */
139
+ * `/vm remote jobs` can list, not just poll a known id. (Agent B request #1.) */
132
140
  listJobs(limit?: number): Promise<JobListResponse>;
133
141
  /** Cooperatively cancel a running/queued job. The worker stops after its
134
142
  * current batch and writes status=cancelled. */
@@ -147,6 +155,8 @@ export declare class ModalEmbeddingClient {
147
155
  since?: number;
148
156
  limit?: number;
149
157
  }): Promise<ExportPage>;
158
+ /** Low-level binary request helper with scoped token selection. */
159
+ private requestBuffer;
150
160
  /** Pull one page of rows with seq > since as an Arrow IPC stream.
151
161
  * Vectors are always included (no include_vectors flag). The watermark /
152
162
  * done / count come back as response headers (X-Next-Watermark, X-Done,
@@ -13,25 +13,36 @@
13
13
  * This client is the typed mirror of the server contract. The server (Agent A)
14
14
  * owns it; additive changes here are mirrored in the server's `modal/web.py`.
15
15
  * The local wiring lives in `src/lance.ts` (provider), `src/sync.ts`, and
16
- * `/wiki modal` commands (see docs/MODAL_EMBEDDING.md "Local integration").
16
+ * `/vm remote` commands (see docs/MODAL_EMBEDDING.md "Local integration").
17
17
  */
18
18
  export class ModalEmbeddingClient {
19
19
  baseUrl;
20
20
  apiToken;
21
+ readToken;
22
+ writeToken;
21
23
  timeoutMs;
22
24
  constructor(cfg) {
23
25
  this.baseUrl = cfg.baseUrl.replace(/\/$/, "");
24
26
  this.apiToken = cfg.apiToken;
27
+ this.readToken = cfg.readToken ?? cfg.apiToken;
28
+ this.writeToken = cfg.writeToken ?? cfg.apiToken;
25
29
  this.timeoutMs = cfg.timeoutMs ?? 120_000;
26
30
  }
27
- async request(method, path, body) {
31
+ /** Return the bearer token to use for the requested operation scope. */
32
+ tokenFor(operation) {
33
+ const token = operation === "read" ? this.readToken : this.writeToken;
34
+ if (!token)
35
+ throw new Error(`Modal ${operation} token not configured`);
36
+ return token;
37
+ }
38
+ async request(method, path, body, operation = "write") {
28
39
  const controller = new AbortController();
29
40
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
30
41
  try {
31
42
  const resp = await fetch(`${this.baseUrl}${path}`, {
32
43
  method,
33
44
  headers: {
34
- Authorization: `Bearer ${this.apiToken}`,
45
+ Authorization: `Bearer ${this.tokenFor(operation)}`,
35
46
  ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
36
47
  },
37
48
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -49,17 +60,17 @@ export class ModalEmbeddingClient {
49
60
  }
50
61
  /** Liveness check; also returns the server's default model. */
51
62
  health() {
52
- return this.request("GET", "/health");
63
+ return this.request("GET", "/health", undefined, "read");
53
64
  }
54
65
  /** Registry of available embedders (public; no auth). Use native_dim to
55
66
  * resolve a model's output dim up-front instead of waiting for the first
56
67
  * /embed response. (Additive — Agent B request #2.) */
57
68
  models() {
58
- return this.request("GET", "/models");
69
+ return this.request("GET", "/models", undefined, "read");
59
70
  }
60
71
  /** Server-side store + compute stats (rows per namespace, index state, GPU). */
61
72
  stats() {
62
- return this.request("GET", "/stats");
73
+ return this.request("GET", "/stats", undefined, "read");
63
74
  }
64
75
  /** Embed text on demand. Use task="query" for search, "document" for storage. */
65
76
  embed(texts, opts = {}) {
@@ -68,7 +79,7 @@ export class ModalEmbeddingClient {
68
79
  model: opts.model,
69
80
  dim: opts.dim,
70
81
  task: opts.task ?? "query",
71
- });
82
+ }, "write");
72
83
  }
73
84
  /** Submit a bulk embedding job; embeds + stores server-side. */
74
85
  submitJob(collection, records, opts = {}) {
@@ -77,24 +88,24 @@ export class ModalEmbeddingClient {
77
88
  records,
78
89
  model: opts.model,
79
90
  dim: opts.dim,
80
- });
91
+ }, "write");
81
92
  }
82
93
  jobStatus(jobId) {
83
- return this.request("GET", `/jobs/${encodeURIComponent(jobId)}`);
94
+ return this.request("GET", `/jobs/${encodeURIComponent(jobId)}`, undefined, "read");
84
95
  }
85
96
  /** List recent jobs (newest first). Additive — surfaces GET /jobs so
86
- * `/wiki modal jobs` can list, not just poll a known id. (Agent B request #1.) */
97
+ * `/vm remote jobs` can list, not just poll a known id. (Agent B request #1.) */
87
98
  listJobs(limit) {
88
99
  const p = new URLSearchParams();
89
100
  if (limit != null)
90
101
  p.set("limit", String(limit));
91
102
  const qs = p.toString();
92
- return this.request("GET", `/jobs${qs ? `?${qs}` : ""}`);
103
+ return this.request("GET", `/jobs${qs ? `?${qs}` : ""}`, undefined, "read");
93
104
  }
94
105
  /** Cooperatively cancel a running/queued job. The worker stops after its
95
106
  * current batch and writes status=cancelled. */
96
107
  cancelJob(jobId) {
97
- return this.request("POST", `/jobs/${encodeURIComponent(jobId)}/cancel`);
108
+ return this.request("POST", `/jobs/${encodeURIComponent(jobId)}/cancel`, undefined, "write");
98
109
  }
99
110
  /** Poll a job until it reaches a terminal state. */
100
111
  async waitForJob(jobId, pollMs = 2000) {
@@ -107,7 +118,7 @@ export class ModalEmbeddingClient {
107
118
  }
108
119
  /** List the collections/tables held in the server-side vector store. */
109
120
  async syncCollections() {
110
- const out = await this.request("GET", "/sync/collections");
121
+ const out = await this.request("GET", "/sync/collections", undefined, "read");
111
122
  return out.collections;
112
123
  }
113
124
  /** Pull one page of rows with seq > since. Remember next_watermark. */
@@ -119,7 +130,27 @@ export class ModalEmbeddingClient {
119
130
  p.set("dim", String(opts.dim));
120
131
  p.set("since", String(opts.since ?? 0));
121
132
  p.set("limit", String(opts.limit ?? 500));
122
- return this.request("GET", `/sync/export?${p.toString()}`);
133
+ return this.request("GET", `/sync/export?${p.toString()}`, undefined, "read");
134
+ }
135
+ /** Low-level binary request helper with scoped token selection. */
136
+ async requestBuffer(method, path, operation = "read") {
137
+ const controller = new AbortController();
138
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
139
+ try {
140
+ const resp = await fetch(`${this.baseUrl}${path}`, {
141
+ method,
142
+ headers: { Authorization: `Bearer ${this.tokenFor(operation)}` },
143
+ signal: controller.signal,
144
+ });
145
+ if (!resp.ok) {
146
+ const detail = await resp.text().catch(() => "");
147
+ throw new Error(`Modal ${method} ${path} failed: ${resp.status} ${detail}`);
148
+ }
149
+ return resp;
150
+ }
151
+ finally {
152
+ clearTimeout(timer);
153
+ }
123
154
  }
124
155
  /** Pull one page of rows with seq > since as an Arrow IPC stream.
125
156
  * Vectors are always included (no include_vectors flag). The watermark /
@@ -135,26 +166,11 @@ export class ModalEmbeddingClient {
135
166
  p.set("dim", String(opts.dim));
136
167
  p.set("since", String(opts.since ?? 0));
137
168
  p.set("limit", String(opts.limit ?? 500));
138
- const controller = new AbortController();
139
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
140
- try {
141
- const resp = await fetch(`${this.baseUrl}/sync/export?${p.toString()}`, {
142
- method: "GET",
143
- headers: { Authorization: `Bearer ${this.apiToken}` },
144
- signal: controller.signal,
145
- });
146
- if (!resp.ok) {
147
- const detail = await resp.text().catch(() => "");
148
- throw new Error(`Modal GET /sync/export failed: ${resp.status} ${detail}`);
149
- }
150
- const nextWatermark = Number(resp.headers.get("X-Next-Watermark") ?? opts.since ?? 0);
151
- const done = (resp.headers.get("X-Done") ?? "true") === "true";
152
- const count = Number(resp.headers.get("X-Count") ?? 0);
153
- return { data: await resp.arrayBuffer(), nextWatermark, done, count };
154
- }
155
- finally {
156
- clearTimeout(timer);
157
- }
169
+ const resp = await this.requestBuffer("GET", `/sync/export?${p.toString()}`, "read");
170
+ const nextWatermark = Number(resp.headers.get("X-Next-Watermark") ?? opts.since ?? 0);
171
+ const done = (resp.headers.get("X-Done") ?? "true") === "true";
172
+ const count = Number(resp.headers.get("X-Count") ?? 0);
173
+ return { data: await resp.arrayBuffer(), nextWatermark, done, count };
158
174
  }
159
175
  /**
160
176
  * Drain every remaining page for a collection, invoking `onPage` for each.
@@ -9,9 +9,13 @@
9
9
  * No HTTP is reimplemented here — `ModalEmbeddingClient` owns that.
10
10
  */
11
11
  import { ModalEmbeddingClient } from "./modal-client.js";
12
- import type { WikiConfig } from "./types.js";
12
+ import type { VaultMindConfig } from "./types.js";
13
13
  /** Env var name for the Modal bearer token (preferred over config). */
14
14
  export declare const MODAL_TOKEN_ENV = "PVM_API_TOKEN";
15
+ /** Env var name for the read-scoped Modal token (search/export). */
16
+ export declare const MODAL_READ_TOKEN_ENV = "PVM_MODAL_READ_TOKEN";
17
+ /** Env var name for the write-scoped Modal token (embed/reindex). */
18
+ export declare const MODAL_WRITE_TOKEN_ENV = "PVM_MODAL_WRITE_TOKEN";
15
19
  /** Dotenv-style fallback path for the Modal token. */
16
20
  export declare const MODAL_TOKEN_ENV_PATH: string;
17
21
  /** Canonical deployed Modal app name. */
@@ -22,33 +26,45 @@ export declare const modalUrl: (workspace: string) => string;
22
26
  * Resolve the Modal API token. Resolution order:
23
27
  * 1. `PVM_API_TOKEN` env var
24
28
  * 2. `~/.pi/agent/vault-mind.env`
25
- * 3. `wiki.embedding.modal.apiToken` in config
29
+ * 3. `vaultMind.embedding.modal.apiToken` in config
30
+ *
31
+ * Never log the resolved token.
32
+ */
33
+ export declare const resolveModalToken: (cfg: VaultMindConfig) => string | undefined;
34
+ /**
35
+ * Resolve a scoped Modal token for the requested operation.
36
+ * Resolution order:
37
+ * 1. `PVM_MODAL_{READ|WRITE}_TOKEN` env var
38
+ * 2. `~/.pi/agent/vault-mind.env`
39
+ * 3. `vaultMind.embedding.modal.{readToken|writeToken}` in config
40
+ * 4. Fallback to the unscoped `apiToken` (env, dotenv, then config)
41
+ *
26
42
  * Never log the resolved token.
27
43
  */
28
- export declare const resolveModalToken: (cfg: WikiConfig) => string | undefined;
44
+ export declare const getModalToken: (cfg: VaultMindConfig, operation: "read" | "write") => string | undefined;
29
45
  /** Resolve the explicit baseUrl or derive it from workspace. */
30
- export declare const resolveBaseUrl: (cfg: WikiConfig) => string | undefined;
46
+ export declare const resolveBaseUrl: (cfg: VaultMindConfig) => string | undefined;
31
47
  /** Resolve the configured workspace slug, if any. */
32
- export declare const resolveWorkspace: (cfg: WikiConfig) => string | undefined;
33
- /** True when Modal is usable: a base URL (explicit or derived) and a resolvable token are present. */
34
- export declare const isModalConfigured: (cfg: WikiConfig) => boolean;
48
+ export declare const resolveWorkspace: (cfg: VaultMindConfig) => string | undefined;
49
+ /** True when Modal is usable: a base URL (explicit or derived) and at least one resolvable scoped token are present. */
50
+ export declare const isModalConfigured: (cfg: VaultMindConfig) => boolean;
35
51
  /**
36
52
  * Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
37
53
  * configured (no base URL / token) so callers can degrade gracefully.
38
54
  */
39
- export declare const createModalClient: (cfg: WikiConfig) => ModalEmbeddingClient | null;
55
+ export declare const createModalClient: (cfg: VaultMindConfig) => ModalEmbeddingClient | null;
40
56
  /**
41
57
  * The canonical embedder key for a collection. Per-collection overrides win;
42
58
  * otherwise the global Modal config model; otherwise "embeddinggemma"
43
59
  * (the eval-confirmed baseline — kept as the default *model name*, never as a
44
60
  * hard-coded dimension).
45
61
  */
46
- export declare const resolveModel: (cfg: WikiConfig, collection?: string) => string;
62
+ export declare const resolveModel: (cfg: VaultMindConfig, collection?: string) => string;
47
63
  /**
48
64
  * The configured output dimension for a collection, if known. Per-collection
49
65
  * overrides win; otherwise the global Modal config `dim`. Returns undefined
50
66
  * when unset — callers resolve the native dim lazily via `/models`.
51
67
  */
52
- export declare const resolveDim: (cfg: WikiConfig, collection?: string) => number | undefined;
68
+ export declare const resolveDim: (cfg: VaultMindConfig, collection?: string) => number | undefined;
53
69
  /** Namespaced local table name mirroring the server's ADR-3 scheme. */
54
70
  export declare const namespacedTableName: (collection: string, model: string, dim: number) => string;
@@ -13,6 +13,10 @@ import * as path from "node:path";
13
13
  import { ModalEmbeddingClient } from "./modal-client.js";
14
14
  /** Env var name for the Modal bearer token (preferred over config). */
15
15
  export const MODAL_TOKEN_ENV = "PVM_API_TOKEN";
16
+ /** Env var name for the read-scoped Modal token (search/export). */
17
+ export const MODAL_READ_TOKEN_ENV = "PVM_MODAL_READ_TOKEN";
18
+ /** Env var name for the write-scoped Modal token (embed/reindex). */
19
+ export const MODAL_WRITE_TOKEN_ENV = "PVM_MODAL_WRITE_TOKEN";
16
20
  /** Dotenv-style fallback path for the Modal token. */
17
21
  export const MODAL_TOKEN_ENV_PATH = path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent", "vault-mind.env");
18
22
  /** Canonical deployed Modal app name. */
@@ -48,7 +52,8 @@ export const modalUrl = (workspace) => `https://${workspace}--${MODAL_APP_NAME}-
48
52
  * Resolve the Modal API token. Resolution order:
49
53
  * 1. `PVM_API_TOKEN` env var
50
54
  * 2. `~/.pi/agent/vault-mind.env`
51
- * 3. `wiki.embedding.modal.apiToken` in config
55
+ * 3. `vaultMind.embedding.modal.apiToken` in config
56
+ *
52
57
  * Never log the resolved token.
53
58
  */
54
59
  export const resolveModalToken = (cfg) => {
@@ -59,6 +64,30 @@ export const resolveModalToken = (cfg) => {
59
64
  return dotenv[MODAL_TOKEN_ENV];
60
65
  return cfg.embedding.modal?.apiToken;
61
66
  };
67
+ /**
68
+ * Resolve a scoped Modal token for the requested operation.
69
+ * Resolution order:
70
+ * 1. `PVM_MODAL_{READ|WRITE}_TOKEN` env var
71
+ * 2. `~/.pi/agent/vault-mind.env`
72
+ * 3. `vaultMind.embedding.modal.{readToken|writeToken}` in config
73
+ * 4. Fallback to the unscoped `apiToken` (env, dotenv, then config)
74
+ *
75
+ * Never log the resolved token.
76
+ */
77
+ export const getModalToken = (cfg, operation) => {
78
+ const modal = cfg.embedding.modal;
79
+ const dotenv = readDotEnv(MODAL_TOKEN_ENV_PATH);
80
+ if (operation === "read") {
81
+ return (process.env[MODAL_READ_TOKEN_ENV] ||
82
+ dotenv[MODAL_READ_TOKEN_ENV] ||
83
+ modal?.readToken ||
84
+ resolveModalToken(cfg));
85
+ }
86
+ return (process.env[MODAL_WRITE_TOKEN_ENV] ||
87
+ dotenv[MODAL_WRITE_TOKEN_ENV] ||
88
+ modal?.writeToken ||
89
+ resolveModalToken(cfg));
90
+ };
62
91
  /** Resolve the explicit baseUrl or derive it from workspace. */
63
92
  export const resolveBaseUrl = (cfg) => {
64
93
  const modal = cfg.embedding.modal;
@@ -70,8 +99,8 @@ export const resolveBaseUrl = (cfg) => {
70
99
  };
71
100
  /** Resolve the configured workspace slug, if any. */
72
101
  export const resolveWorkspace = (cfg) => cfg.embedding.modal?.workspace;
73
- /** True when Modal is usable: a base URL (explicit or derived) and a resolvable token are present. */
74
- export const isModalConfigured = (cfg) => cfg.embedding.provider === "modal" && !!resolveBaseUrl(cfg) && !!resolveModalToken(cfg);
102
+ /** True when Modal is usable: a base URL (explicit or derived) and at least one resolvable scoped token are present. */
103
+ export const isModalConfigured = (cfg) => !!cfg.embedding.remoteUrl && (!!getModalToken(cfg, "read") || !!getModalToken(cfg, "write"));
75
104
  /**
76
105
  * Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
77
106
  * configured (no base URL / token) so callers can degrade gracefully.
@@ -81,9 +110,11 @@ export const createModalClient = (cfg) => {
81
110
  if (!baseUrl)
82
111
  return null;
83
112
  const apiToken = resolveModalToken(cfg);
84
- if (!apiToken)
113
+ const readToken = getModalToken(cfg, "read");
114
+ const writeToken = getModalToken(cfg, "write");
115
+ if (!apiToken && !readToken && !writeToken)
85
116
  return null;
86
- const clientCfg = { baseUrl, apiToken };
117
+ const clientCfg = { baseUrl, apiToken, readToken, writeToken };
87
118
  return new ModalEmbeddingClient(clientCfg);
88
119
  };
89
120
  /**
@@ -0,0 +1,47 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export interface VaultProfile {
3
+ vaultPath: string;
4
+ topFolders: Array<{
5
+ name: string;
6
+ noteCount: number;
7
+ }>;
8
+ totalNotes: number;
9
+ commonTags: Array<{
10
+ tag: string;
11
+ count: number;
12
+ }>;
13
+ domains: string[];
14
+ lancedbSummary?: string;
15
+ }
16
+ export interface ConfigFiles {
17
+ systemMd: string | null;
18
+ agentsMd: string | null;
19
+ }
20
+ export interface FileSuggestion {
21
+ path: string;
22
+ current: string | null;
23
+ proposed: string;
24
+ action: "create" | "update" | "skip";
25
+ }
26
+ export interface SuggestionResult {
27
+ commentary?: string;
28
+ files: FileSuggestion[];
29
+ }
30
+ /** Resolve the vault path: configured default vault wins, otherwise ctx.cwd. */
31
+ export declare const resolveVaultPath: (ctx: ExtensionContext) => string;
32
+ /** Load current agent configuration files if they exist. */
33
+ export declare const loadCurrentConfig: (vaultPath: string) => ConfigFiles;
34
+ /** Analyze the vault structure, tags, and LanceDB state. */
35
+ export declare const analyzeVaultProfile: (ctx: ExtensionContext) => Promise<VaultProfile>;
36
+ export declare const parseSuggestionJson: (text: string) => SuggestionResult;
37
+ /** Ask the current session model to suggest personalized config updates. */
38
+ export declare const generateSuggestions: (pi: ExtensionAPI, profile: VaultProfile, currentConfig: ConfigFiles) => Promise<SuggestionResult>;
39
+ /** Format old vs new as a markdown diff block. */
40
+ export declare const formatDiff: (current: string | null, proposed: string) => string;
41
+ /** Present each suggestion diff to the user and write approved files. */
42
+ export declare const presentAndApplyDiff: (suggestions: SuggestionResult, ctx: ExtensionContext, currentConfig: ConfigFiles) => Promise<{
43
+ approved: string[];
44
+ rejected: string[];
45
+ }>;
46
+ /** Orchestrate /vm personalize: analyze, suggest, confirm, apply. */
47
+ export declare const runPersonalize: (ctx: ExtensionContext, pi: ExtensionAPI) => Promise<void>;