pi-vault-mind 0.14.2 → 0.16.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 (164) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/README.md +5 -5
  3. package/agents/broadcaster.agent.md +2 -2
  4. package/agents/heavy-lifter.agent.md +1 -1
  5. package/agents/main.agent.md +29 -0
  6. package/agents/manager.agent.md +1 -1
  7. package/agents/miner.agent.md +1 -1
  8. package/dist/packages/obsidian/src/client.js +331 -0
  9. package/dist/packages/obsidian/src/types.js +1 -0
  10. package/dist/src/activity.d.ts +49 -0
  11. package/dist/src/activity.js +175 -0
  12. package/dist/src/agent-bus/Agent.d.ts +9 -1
  13. package/dist/src/agent-bus/Agent.js +18 -2
  14. package/dist/src/agent-bus/index.d.ts +5 -5
  15. package/dist/src/agent-bus/index.js +1 -1
  16. package/dist/src/agent-queue.js +12 -3
  17. package/dist/src/agents/BroadcasterAgent.d.ts +1 -1
  18. package/dist/src/agents/BroadcasterAgent.js +2 -2
  19. package/dist/src/agents/HeavyLifterAgent.d.ts +1 -1
  20. package/dist/src/agents/HeavyLifterAgent.js +2 -2
  21. package/dist/src/agents/ManagerAgent.js +1 -1
  22. package/dist/src/agents/MinerAgent.d.ts +1 -1
  23. package/dist/src/agents/MinerAgent.js +2 -2
  24. package/dist/src/agents/index.d.ts +2 -2
  25. package/dist/src/agents/index.js +2 -2
  26. package/dist/src/auth.d.ts +17 -15
  27. package/dist/src/auth.js +26 -37
  28. package/dist/src/autosync.js +2 -2
  29. package/dist/src/commands.d.ts +2 -19
  30. package/dist/src/commands.js +301 -231
  31. package/dist/src/config-keys.d.ts +16 -0
  32. package/dist/src/config-keys.js +45 -0
  33. package/dist/src/discover-schema.js +4 -4
  34. package/dist/src/edits.d.ts +21 -0
  35. package/dist/src/edits.js +75 -0
  36. package/dist/src/embedding-probe.d.ts +34 -0
  37. package/dist/src/embedding-probe.js +142 -0
  38. package/dist/src/embedding-providers.d.ts +133 -0
  39. package/dist/src/embedding-providers.js +173 -0
  40. package/dist/src/embedding-secrets.d.ts +38 -0
  41. package/dist/src/embedding-secrets.js +291 -0
  42. package/dist/src/engine.d.ts +6 -0
  43. package/dist/src/engine.js +78 -6
  44. package/dist/src/events.js +7 -10
  45. package/dist/src/extension-packages.d.ts +9 -0
  46. package/dist/src/extension-packages.js +14 -0
  47. package/dist/src/git.d.ts +19 -0
  48. package/dist/src/git.js +78 -0
  49. package/dist/src/graph.d.ts +2 -1
  50. package/dist/src/graph.js +47 -13
  51. package/dist/src/identities-config.d.ts +6 -0
  52. package/dist/src/identities-config.js +39 -0
  53. package/dist/src/identity-injector.js +1 -1
  54. package/dist/src/index.js +9 -26
  55. package/dist/src/lance.d.ts +9 -0
  56. package/dist/src/lance.js +143 -203
  57. package/dist/src/modal-client.d.ts +16 -0
  58. package/dist/src/modal-client.js +16 -0
  59. package/dist/src/modal-config.d.ts +37 -14
  60. package/dist/src/modal-config.js +59 -32
  61. package/dist/src/model-router.d.ts +27 -0
  62. package/dist/src/model-router.js +67 -0
  63. package/dist/src/models.d.ts +42 -0
  64. package/dist/src/models.js +78 -0
  65. package/dist/src/personalize.js +17 -3
  66. package/dist/src/scaffold.d.ts +48 -0
  67. package/dist/src/scaffold.js +246 -0
  68. package/dist/src/server.d.ts +47 -4
  69. package/dist/src/server.js +988 -221
  70. package/dist/src/session-search.d.ts +63 -0
  71. package/dist/src/session-search.js +379 -0
  72. package/dist/src/settings-ui.d.ts +2 -2
  73. package/dist/src/settings-ui.js +76 -135
  74. package/dist/src/sync.d.ts +3 -3
  75. package/dist/src/sync.js +19 -15
  76. package/dist/src/tool-catalog.d.ts +18 -0
  77. package/dist/src/tool-catalog.js +232 -0
  78. package/dist/src/tools/marksman.js +1 -1
  79. package/dist/src/tools.js +111 -65
  80. package/dist/src/types.d.ts +127 -71
  81. package/dist/src/utils.d.ts +57 -6
  82. package/dist/src/utils.js +114 -28
  83. package/dist/src/vault-tools.d.ts +34 -0
  84. package/dist/src/vault-tools.js +99 -0
  85. package/dist/src/vault-writer.js +4 -1
  86. package/dist/src/vm-handlers.js +140 -43
  87. package/dist/src/watcher.js +9 -9
  88. package/dist/test/activity-tool.test.js +54 -0
  89. package/dist/test/agent-bus/Agent.test.js +174 -0
  90. package/dist/test/agent-bus/AgentLoader.test.js +180 -0
  91. package/dist/test/agent-bus/AgentRegistry.test.js +86 -0
  92. package/dist/test/agent-bus/LLMProvider.test.js +162 -0
  93. package/dist/test/agent-bus/TaskQueue.test.js +256 -0
  94. package/dist/test/agent-dispatch-context.test.js +73 -0
  95. package/dist/test/agent-queue.test.js +1 -1
  96. package/dist/test/agents/Agents.test.js +282 -0
  97. package/dist/test/auth.test.js +57 -56
  98. package/dist/test/bridge-identity.test.js +34 -3
  99. package/dist/test/classify-search-mode.test.js +51 -0
  100. package/dist/test/commands.test.js +410 -0
  101. package/dist/test/config-merge.test.js +98 -0
  102. package/dist/test/context-capture.test.js +56 -0
  103. package/dist/test/discover-schema.test.js +8 -5
  104. package/dist/test/dispatch.test.js +45 -91
  105. package/dist/test/edits.test.js +144 -0
  106. package/dist/test/embedding-probe.test.js +424 -0
  107. package/dist/test/embedding-providers.test.js +157 -0
  108. package/dist/test/embedding-secrets.test.js +210 -0
  109. package/dist/test/engine-identity.test.js +1 -1
  110. package/dist/test/events.test.js +675 -0
  111. package/dist/test/git-bridge.test.js +160 -0
  112. package/dist/test/graph.test.js +464 -0
  113. package/dist/test/helpers/server.js +81 -0
  114. package/dist/test/identity-injector.test.js +197 -0
  115. package/dist/test/index.test.js +5 -7
  116. package/dist/test/lance-modal.test.js +67 -2
  117. package/dist/test/lance.test.js +429 -0
  118. package/dist/test/modal-client.test.js +1 -1
  119. package/dist/test/modal-config.test.js +92 -79
  120. package/dist/test/model-readiness.test.js +122 -0
  121. package/dist/test/model-router.test.js +99 -0
  122. package/dist/test/obsidian-client.test.js +42 -0
  123. package/dist/test/personalize.test.js +1 -1
  124. package/dist/test/rest-models.test.js +153 -0
  125. package/dist/test/rest-queue.test.js +135 -87
  126. package/dist/test/rest-setup.test.js +625 -115
  127. package/dist/test/rest-vm.test.js +355 -94
  128. package/dist/test/server-cors.test.js +48 -0
  129. package/dist/test/server-ws.test.js +61 -34
  130. package/dist/test/session-crud.test.js +323 -0
  131. package/dist/test/session-search.test.js +139 -0
  132. package/dist/test/settings-ui.test.js +113 -4
  133. package/dist/test/sync.test.js +12 -12
  134. package/dist/test/tombstone.test.js +8 -10
  135. package/dist/test/tool-catalog.test.js +86 -0
  136. package/dist/test/utils.test.js +588 -0
  137. package/dist/test/vault-tools.test.js +103 -12
  138. package/dist/test/vault-writer.test.js +371 -0
  139. package/dist/test/vm-handlers.test.js +620 -0
  140. package/extension-packages.json +9 -0
  141. package/package.json +107 -90
  142. package/pi-vault-mind.config.example.json +24 -2
  143. package/scripts/check-deps.sh +35 -32
  144. package/scripts/cli-e2e.mjs +215 -0
  145. package/scripts/configuration-e2e.mjs +783 -0
  146. package/scripts/e2e-commands.mjs +391 -0
  147. package/scripts/e2e-ws.mjs +3 -2
  148. package/scripts/fetch-modal-token.sh +4 -3
  149. package/scripts/generate-config-keys-json.mjs +45 -0
  150. package/scripts/generate-extension-packages-json.mjs +55 -0
  151. package/scripts/pi-session-query +0 -0
  152. package/scripts/publish-obsidian-plugin.sh +0 -0
  153. package/scripts/reset-test-vault.sh +2 -2
  154. package/scripts/setup-test-config.sh +0 -0
  155. package/scripts/setup-vault-pi.sh +48 -9
  156. package/scripts/test-slash-commands.mjs +2 -2
  157. package/scripts/vault-pi.sh +12 -3
  158. package/skills/github-release-notes-file/SKILL.md +42 -0
  159. package/skills/obsidian-guided-test-session/SKILL.md +110 -0
  160. package/skills/obsidian-interactive-test/SKILL.md +88 -0
  161. package/skills/obsidian-plugin-publish/SKILL.md +58 -0
  162. package/skills/pi-vault-mind-release/SKILL.md +141 -0
  163. package/skills/pre-test-release/SKILL.md +116 -0
  164. package/dist/test/auto-indexer.test.js +0 -2
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Embedding provider secret contract.
3
+ *
4
+ * Bridge authentication (`PVM_API_TOKEN`) and provider credentials are fully
5
+ * decoupled. This module owns reading, writing, resolving, masking, and
6
+ * status reporting for the four embedding secret kinds. All four variables are
7
+ * stored in `<vault>/.vault-mind/vault-mind.env`; legacy JSON config copies are
8
+ * removed on write.
9
+ */
10
+ import type { EmbeddingSecretKind, EmbeddingSecretResolutionOptions, EmbeddingSecretStatus, EmbeddingSecretStatusResponse, PutEmbeddingSecretsRequest, PutEmbeddingSecretsResponse, UniversalConfig, VaultMindConfig } from "./types.js";
11
+ export type { EmbeddingSecretKind, EmbeddingSecretResolutionOptions, EmbeddingSecretStatus, EmbeddingSecretStatusResponse, PutEmbeddingSecretsRequest, PutEmbeddingSecretsResponse, };
12
+ export declare const EMBEDDING_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
13
+ /**
14
+ * Resolve one embedding secret. Precedence:
15
+ * 1. `options.environment` if supplied, else `process.env`
16
+ * 2. `<vault>/.vault-mind/vault-mind.env`
17
+ * 3. Matching legacy field in `cfg.embedding`
18
+ */
19
+ export declare const resolveEmbeddingSecret: (cfg: VaultMindConfig, kind: EmbeddingSecretKind, opts?: EmbeddingSecretResolutionOptions) => string | undefined;
20
+ /** Return the status of all four embedding secret kinds in declaration order. */
21
+ export declare const readEmbeddingSecretStatus: (cfg: VaultMindConfig, opts?: EmbeddingSecretResolutionOptions) => EmbeddingSecretStatusResponse;
22
+ /**
23
+ * Update a single env entry in `<vault>/.vault-mind/vault-mind.env` atomically,
24
+ * preserving all other entries and comments. This is the entry-preserving
25
+ * primitive reused by callers that only need to write one variable.
26
+ */
27
+ export declare const writeEnvEntry: (key: string, value: string, opts?: EmbeddingSecretResolutionOptions) => void;
28
+ /**
29
+ * Update the embedding secret store. Validation runs before any mutation. The
30
+ * env file is replaced atomically; if the matching legacy JSON config cleanup
31
+ * fails, the previous env file bytes are restored.
32
+ */
33
+ export declare const writeEmbeddingSecrets: (request: PutEmbeddingSecretsRequest, opts?: EmbeddingSecretResolutionOptions) => PutEmbeddingSecretsResponse;
34
+ /**
35
+ * Return a deep copy of `config` with all legacy provider credential fields
36
+ * replaced by the fixed mask. The original config is not mutated.
37
+ */
38
+ export declare const redactEmbeddingSecrets: (config: UniversalConfig) => UniversalConfig;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Embedding provider secret contract.
3
+ *
4
+ * Bridge authentication (`PVM_API_TOKEN`) and provider credentials are fully
5
+ * decoupled. This module owns reading, writing, resolving, masking, and
6
+ * status reporting for the four embedding secret kinds. All four variables are
7
+ * stored in `<vault>/.vault-mind/vault-mind.env`; legacy JSON config copies are
8
+ * removed on write.
9
+ */
10
+ import * as crypto from "node:crypto";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { getTokenEnvPath } from "./utils.js";
14
+ const DEFINITIONS = {
15
+ localApiKey: { env: "PVM_LOCAL_EMBEDDING_API_KEY", legacy: "apiKey" },
16
+ remoteApiKey: { env: "PVM_REMOTE_EMBEDDING_API_KEY", legacy: "remoteApiKey" },
17
+ remoteReadApiKey: { env: "PVM_MODAL_READ_TOKEN", legacy: "remoteReadApiKey" },
18
+ remoteWriteApiKey: { env: "PVM_MODAL_WRITE_TOKEN", legacy: "remoteWriteApiKey" },
19
+ };
20
+ export const EMBEDDING_SECRET_MASK = "••••••••";
21
+ const getEnv = (opts, key) => {
22
+ const env = opts?.environment ?? process.env;
23
+ return env[key];
24
+ };
25
+ const getEnvPath = (opts) => getTokenEnvPath(opts?.cwd ?? process.cwd());
26
+ /**
27
+ * Parse a dotenv-style file into a record while preserving the original byte
28
+ * content so writes can keep comments, blank lines, and unrelated variables.
29
+ */
30
+ const readDotEnv = (filePath) => {
31
+ if (!fs.existsSync(filePath))
32
+ return {};
33
+ const out = {};
34
+ for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
35
+ const trimmed = line.trim();
36
+ if (!trimmed || trimmed.startsWith("#"))
37
+ continue;
38
+ const eq = trimmed.indexOf("=");
39
+ if (eq === -1)
40
+ continue;
41
+ const key = trimmed.slice(0, eq).trim();
42
+ let value = trimmed.slice(eq + 1).trim();
43
+ if ((value.startsWith('"') && value.endsWith('"')) ||
44
+ (value.startsWith("'") && value.endsWith("'"))) {
45
+ value = value.slice(1, -1);
46
+ }
47
+ out[key] = value;
48
+ }
49
+ return out;
50
+ };
51
+ /**
52
+ * Resolve one embedding secret. Precedence:
53
+ * 1. `options.environment` if supplied, else `process.env`
54
+ * 2. `<vault>/.vault-mind/vault-mind.env`
55
+ * 3. Matching legacy field in `cfg.embedding`
56
+ */
57
+ export const resolveEmbeddingSecret = (cfg, kind, opts) => {
58
+ const def = DEFINITIONS[kind];
59
+ const fromEnv = getEnv(opts, def.env);
60
+ if (fromEnv)
61
+ return fromEnv;
62
+ const fromStore = readDotEnv(getEnvPath(opts))[def.env];
63
+ if (fromStore)
64
+ return fromStore;
65
+ return cfg.embedding[def.legacy];
66
+ };
67
+ const getSecretSource = (cfg, kind, envRecord, opts) => {
68
+ const def = DEFINITIONS[kind];
69
+ if (getEnv(opts, def.env))
70
+ return "extension-environment";
71
+ if (envRecord[def.env])
72
+ return "extension-secret-store";
73
+ if (cfg.embedding[def.legacy])
74
+ return "legacy-config";
75
+ return "none";
76
+ };
77
+ /** Return the status of all four embedding secret kinds in declaration order. */
78
+ export const readEmbeddingSecretStatus = (cfg, opts) => {
79
+ const kinds = [
80
+ "localApiKey",
81
+ "remoteApiKey",
82
+ "remoteReadApiKey",
83
+ "remoteWriteApiKey",
84
+ ];
85
+ const envRecord = readDotEnv(getEnvPath(opts));
86
+ return {
87
+ secrets: kinds.map((kind) => {
88
+ const source = getSecretSource(cfg, kind, envRecord, opts);
89
+ const configured = source !== "none";
90
+ return {
91
+ kind,
92
+ configured,
93
+ masked: configured ? EMBEDDING_SECRET_MASK : null,
94
+ source,
95
+ };
96
+ }),
97
+ };
98
+ };
99
+ /** Read the vault config object to use as the status baseline. */
100
+ const readVaultMindConfig = (configPath) => {
101
+ try {
102
+ if (fs.existsSync(configPath)) {
103
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
104
+ return raw.vaultMind ?? { dataDir: "", embedding: {} };
105
+ }
106
+ }
107
+ catch {
108
+ // fall through to empty config
109
+ }
110
+ return { dataDir: "", embedding: {} };
111
+ };
112
+ /** True when the value is an empty string; empty values are rejected before mutation. */
113
+ const isEmptyString = (value) => value === "";
114
+ /** Read a dotenv-style file as a list of lines, returning an empty list when absent. */
115
+ const readEnvLines = (filePath) => {
116
+ if (!fs.existsSync(filePath))
117
+ return [];
118
+ return fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
119
+ };
120
+ /**
121
+ * Update or remove a single env entry while preserving all other lines
122
+ * (comments, blanks, unrelated variables). If the key is absent and `value` is
123
+ * a string, it is appended. Duplicate occurrences collapse into one update.
124
+ */
125
+ const updateEnvLine = (lines, key, value) => {
126
+ const out = [];
127
+ let handled = false;
128
+ for (const line of lines) {
129
+ const trimmed = line.trim();
130
+ const match = trimmed.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=/);
131
+ if (!match || match[1] !== key) {
132
+ out.push(line);
133
+ continue;
134
+ }
135
+ if (handled) {
136
+ // Drop duplicate occurrences of the same key.
137
+ continue;
138
+ }
139
+ handled = true;
140
+ if (value === null) {
141
+ // Drop the line for a null deletion.
142
+ continue;
143
+ }
144
+ out.push(`${key}="${value}"`);
145
+ }
146
+ if (!handled && value !== null) {
147
+ out.push(`${key}="${value}"`);
148
+ }
149
+ return out;
150
+ };
151
+ /** Atomically replace the env file using a same-directory temporary file. */
152
+ const atomicWriteEnvFile = (envPath, lines) => {
153
+ let content = lines.join("\n");
154
+ if (!content.endsWith("\n"))
155
+ content += "\n";
156
+ const newBytes = Buffer.from(content, "utf-8");
157
+ const dir = path.dirname(envPath);
158
+ if (!fs.existsSync(dir))
159
+ fs.mkdirSync(dir, { recursive: true });
160
+ const tmp = path.join(dir, `.vault-mind.env.tmp-${crypto.randomBytes(8).toString("hex")}`);
161
+ try {
162
+ fs.writeFileSync(tmp, newBytes, { mode: process.platform === "win32" ? undefined : 0o600 });
163
+ fs.renameSync(tmp, envPath);
164
+ }
165
+ catch (err) {
166
+ try {
167
+ fs.unlinkSync(tmp);
168
+ }
169
+ catch {
170
+ // best-effort cleanup
171
+ }
172
+ throw err;
173
+ }
174
+ try {
175
+ if (process.platform !== "win32")
176
+ fs.chmodSync(envPath, 0o600);
177
+ }
178
+ catch {
179
+ // best-effort; no-op on win32 or when chmod is unavailable
180
+ }
181
+ };
182
+ /** Best-effort restore of previous env bytes on rollback. */
183
+ const restoreEnv = (envPath, bytes) => {
184
+ try {
185
+ fs.writeFileSync(envPath, bytes);
186
+ }
187
+ catch {
188
+ // rollback is best-effort; if it fails the caller sees the original error
189
+ }
190
+ };
191
+ /**
192
+ * Update a single env entry in `<vault>/.vault-mind/vault-mind.env` atomically,
193
+ * preserving all other entries and comments. This is the entry-preserving
194
+ * primitive reused by callers that only need to write one variable.
195
+ */
196
+ export const writeEnvEntry = (key, value, opts) => {
197
+ const envPath = getEnvPath(opts);
198
+ const lines = updateEnvLine(readEnvLines(envPath), key, value);
199
+ atomicWriteEnvFile(envPath, lines);
200
+ };
201
+ /**
202
+ * Update the embedding secret store. Validation runs before any mutation. The
203
+ * env file is replaced atomically; if the matching legacy JSON config cleanup
204
+ * fails, the previous env file bytes are restored.
205
+ */
206
+ export const writeEmbeddingSecrets = (request, opts) => {
207
+ const cwd = opts?.cwd ?? process.cwd();
208
+ const envPath = getTokenEnvPath(cwd);
209
+ const configPath = path.join(path.dirname(envPath), "vault-mind.config.json");
210
+ // Reject empty-string values before touching disk.
211
+ for (const [kind, value] of Object.entries(request)) {
212
+ if (isEmptyString(value)) {
213
+ return {
214
+ ok: false,
215
+ secrets: readEmbeddingSecretStatus(readVaultMindConfig(configPath), opts).secrets,
216
+ error: `Empty value rejected for ${kind}`,
217
+ };
218
+ }
219
+ }
220
+ const previousBytes = fs.existsSync(envPath) ? fs.readFileSync(envPath) : Buffer.from("");
221
+ let currentConfig = readVaultMindConfig(configPath);
222
+ // Apply each requested mutation while preserving unrelated lines.
223
+ let updatedLines = previousBytes.toString("utf-8").split(/\r?\n/);
224
+ for (const kind of Object.keys(DEFINITIONS)) {
225
+ const value = request[kind];
226
+ if (value !== undefined) {
227
+ updatedLines = updateEnvLine(updatedLines, DEFINITIONS[kind].env, value);
228
+ }
229
+ }
230
+ try {
231
+ atomicWriteEnvFile(envPath, updatedLines);
232
+ }
233
+ catch (err) {
234
+ return {
235
+ ok: false,
236
+ secrets: readEmbeddingSecretStatus(currentConfig, opts).secrets,
237
+ error: `Failed to write secret store: ${err}`,
238
+ };
239
+ }
240
+ // Clean matching legacy fields from the JSON config, if a config file exists.
241
+ try {
242
+ if (fs.existsSync(configPath)) {
243
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
244
+ let dirty = false;
245
+ for (const kind of Object.keys(DEFINITIONS)) {
246
+ const value = request[kind];
247
+ if (value === undefined)
248
+ continue;
249
+ const legacyKey = DEFINITIONS[kind].legacy;
250
+ if (raw.vaultMind?.embedding?.[legacyKey] !== undefined) {
251
+ delete raw.vaultMind.embedding[legacyKey];
252
+ dirty = true;
253
+ }
254
+ }
255
+ if (dirty) {
256
+ fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
257
+ }
258
+ if (raw.vaultMind) {
259
+ currentConfig = raw.vaultMind;
260
+ }
261
+ }
262
+ }
263
+ catch (err) {
264
+ restoreEnv(envPath, previousBytes);
265
+ return {
266
+ ok: false,
267
+ secrets: readEmbeddingSecretStatus(currentConfig, opts).secrets,
268
+ error: `Failed to clean legacy config: ${err}`,
269
+ };
270
+ }
271
+ return {
272
+ ok: true,
273
+ secrets: readEmbeddingSecretStatus(currentConfig, opts).secrets,
274
+ error: null,
275
+ };
276
+ };
277
+ /**
278
+ * Return a deep copy of `config` with all legacy provider credential fields
279
+ * replaced by the fixed mask. The original config is not mutated.
280
+ */
281
+ export const redactEmbeddingSecrets = (config) => {
282
+ const redacted = JSON.parse(JSON.stringify(config));
283
+ const emb = redacted.vaultMind.embedding;
284
+ for (const kind of Object.keys(DEFINITIONS)) {
285
+ const legacyKey = DEFINITIONS[kind].legacy;
286
+ if (emb[legacyKey] !== undefined) {
287
+ emb[legacyKey] = EMBEDDING_SECRET_MASK;
288
+ }
289
+ }
290
+ return redacted;
291
+ };
@@ -63,3 +63,9 @@ export declare function buildIdentityFromRole(role: string, id: string): AgentId
63
63
  * an agent.
64
64
  */
65
65
  export declare function mergeIdentityProfiles(base: AgentIdentity, defaultProfile: IdentityConfig | undefined, roleProfile: IdentityConfig | undefined): AgentIdentity;
66
+ /**
67
+ * Re-read identities config from disk, re-merge profiles, and re-register
68
+ * affected roles in the bridge's agentIdentities map. Call after config
69
+ * writes that touch identities.roles.
70
+ */
71
+ export declare function refreshAgentIdentities(rolesToReload?: string[]): void;
@@ -140,7 +140,13 @@ export function buildIdentityFromRole(role, id) {
140
140
  "ls",
141
141
  "bash",
142
142
  "vm_search",
143
- "vm_fts_search",
143
+ "vault_read",
144
+ "vault_search",
145
+ "vault_list",
146
+ "vault_tags",
147
+ "vm_backlinks",
148
+ "vm_related",
149
+ "vm_codegraph_impact",
144
150
  ],
145
151
  writeCollections: [],
146
152
  canPublish: false,
@@ -157,8 +163,13 @@ export function buildIdentityFromRole(role, id) {
157
163
  "find",
158
164
  "ls",
159
165
  "vm_search",
160
- "vm_fts_search",
161
166
  "vm_append",
167
+ "vault_read",
168
+ "vault_search",
169
+ "vault_list",
170
+ "vault_tags",
171
+ "vm_backlinks",
172
+ "vm_related",
162
173
  ],
163
174
  writeCollections: ["main", "research"],
164
175
  canPublish: false,
@@ -167,7 +178,7 @@ export function buildIdentityFromRole(role, id) {
167
178
  return {
168
179
  id,
169
180
  role,
170
- allowedTools: ["read", "write", "edit", "vm_search", "vm_fts_search"],
181
+ allowedTools: ["read", "write", "edit", "vm_search", "vault_read", "vault_search"],
171
182
  writeCollections: [],
172
183
  canPublish: false,
173
184
  };
@@ -183,7 +194,6 @@ export function buildIdentityFromRole(role, id) {
183
194
  "find",
184
195
  "ls",
185
196
  "vm_search",
186
- "vm_fts_search",
187
197
  "vm_append",
188
198
  "vm_sync",
189
199
  "vm_promote",
@@ -194,16 +204,58 @@ export function buildIdentityFromRole(role, id) {
194
204
  "vm_export",
195
205
  "vm_ingest",
196
206
  "vm_status",
197
- "vm_graph_query",
207
+ "vault_read",
208
+ "vault_search",
209
+ "vault_list",
210
+ "vault_tags",
211
+ "vm_backlinks",
212
+ "vm_related",
213
+ "vm_broken_links",
214
+ "vm_codegraph_impact",
215
+ "vault_write",
216
+ "vault_append",
198
217
  ],
199
218
  writeCollections: ["main", "research", "presentations"],
200
219
  canPublish: true,
201
220
  };
221
+ case "main":
222
+ return {
223
+ id,
224
+ role,
225
+ allowedTools: [
226
+ "read",
227
+ "write",
228
+ "edit",
229
+ "bash",
230
+ "grep",
231
+ "find",
232
+ "ls",
233
+ "vm_search",
234
+ "vm_query",
235
+ "vm_append",
236
+ "vm_stats",
237
+ "vm_status",
238
+ "vm_describe",
239
+ "vm_configure",
240
+ "vault_read",
241
+ "vault_search",
242
+ "vault_list",
243
+ "vault_tags",
244
+ "vm_backlinks",
245
+ "vm_related",
246
+ "vm_broken_links",
247
+ "vm_codegraph_impact",
248
+ ],
249
+ readCollections: ["main"],
250
+ writeCollections: [],
251
+ canPublish: false,
252
+ allowedVaultPaths: [],
253
+ };
202
254
  default:
203
255
  return {
204
256
  id,
205
257
  role,
206
- allowedTools: ["read", "vm_search", "vm_fts_search"],
258
+ allowedTools: ["read", "vm_search", "vault_search"],
207
259
  writeCollections: [],
208
260
  canPublish: false,
209
261
  };
@@ -242,3 +294,23 @@ export function mergeIdentityProfiles(base, defaultProfile, roleProfile) {
242
294
  }
243
295
  return merged;
244
296
  }
297
+ /**
298
+ * Re-read identities config from disk, re-merge profiles, and re-register
299
+ * affected roles in the bridge's agentIdentities map. Call after config
300
+ * writes that touch identities.roles.
301
+ */
302
+ export function refreshAgentIdentities(rolesToReload) {
303
+ const cfg = loadConfig(process.cwd());
304
+ const identitiesCfg = cfg.vaultMind.identities ?? {};
305
+ const roles = identitiesCfg.roles ?? {};
306
+ const defaultProfile = identitiesCfg.defaultProfile;
307
+ const targetRoles = rolesToReload ?? Object.keys(roles);
308
+ for (const role of targetRoles) {
309
+ const roleProfile = roles[role];
310
+ if (!roleProfile)
311
+ continue;
312
+ const base = buildIdentityFromRole(role, `agent-${role}`);
313
+ const identity = mergeIdentityProfiles(base, defaultProfile, roleProfile);
314
+ registerAgentIdentity(identity);
315
+ }
316
+ }
@@ -55,15 +55,12 @@ export const handleBeforeAgentStart = async (pi, event, ctx) => {
55
55
  // pi-context is disabled or unavailable — skip integration silently.
56
56
  }
57
57
  else {
58
- if (piContextCfg.autoEnableAcm) {
59
- pi.sendMessage({
60
- customType: "pi-context",
61
- content: "/acm",
62
- display: false,
63
- }, {
64
- deliverAs: "followUp",
65
- });
66
- }
58
+ // ACM auto-enable is handled exactly once at session_start (see index.ts)
59
+ // via pi.sendUserMessage("/acm"), gated on isVaultMindReady. A hidden
60
+ // sendMessage({ customType: "pi-context", content: "/acm" }) here does NOT
61
+ // enable compaction — pi-context only enables ACM from its own `/acm`
62
+ // command handler (which captures the command context), so this dead path
63
+ // was removed.
67
64
  if (piContextCfg.tagPatterns && piContextCfg.tagPatterns.length > 0) {
68
65
  try {
69
66
  const allTools = pi.getAllTools();
@@ -150,7 +147,7 @@ export const handleTurnEnd = async (pi, _event, ctx) => {
150
147
  }
151
148
  // Get current session history
152
149
  const logs = await contextLogTool.execute?.(null, { limit: 100, verbose: true }, null, null, ctx);
153
- if (!logs || !logs.content || !logs.content[0]?.text) {
150
+ if (!logs?.content?.[0]?.text) {
154
151
  return;
155
152
  }
156
153
  const logText = logs.content[0].text;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Canonical list of pi extensions required/optional for vault-mind.
3
+ *
4
+ * This is the SINGLE SOURCE OF TRUTH. Shell scripts and the Obsidian plugin
5
+ * consume generated JSON (see scripts/generate-extension-packages-json.mjs).
6
+ * When adding/removing a package, edit ONLY this file and regenerate.
7
+ */
8
+ export declare const PI_REQUIRED_EXTENSIONS: readonly ["npm:pi-vault-mind", "npm:pi-context", "npm:@kylebrodeur/pi-model-discovery", "npm:@kylebrodeur/pi-model-router"];
9
+ export declare const PI_OPTIONAL_EXTENSIONS: readonly ["npm:pi-1password"];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Canonical list of pi extensions required/optional for vault-mind.
3
+ *
4
+ * This is the SINGLE SOURCE OF TRUTH. Shell scripts and the Obsidian plugin
5
+ * consume generated JSON (see scripts/generate-extension-packages-json.mjs).
6
+ * When adding/removing a package, edit ONLY this file and regenerate.
7
+ */
8
+ export const PI_REQUIRED_EXTENSIONS = [
9
+ "npm:pi-vault-mind",
10
+ "npm:pi-context",
11
+ "npm:@kylebrodeur/pi-model-discovery",
12
+ "npm:@kylebrodeur/pi-model-router",
13
+ ];
14
+ export const PI_OPTIONAL_EXTENSIONS = ["npm:pi-1password"];
@@ -0,0 +1,19 @@
1
+ export interface GitStatus {
2
+ isRepo: boolean;
3
+ branch: string | null;
4
+ dirty: boolean;
5
+ ahead: number;
6
+ behind: number;
7
+ detached: boolean;
8
+ }
9
+ export interface GitBranches {
10
+ isRepo: boolean;
11
+ branches: string[];
12
+ current: string | null;
13
+ }
14
+ export declare function getGitStatus(cwd: string): Promise<GitStatus>;
15
+ export declare function getGitBranches(cwd: string): Promise<GitBranches>;
16
+ export declare function gitCheckout(cwd: string, branch: string): Promise<{
17
+ ok: boolean;
18
+ branch: string;
19
+ }>;
@@ -0,0 +1,78 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ async function git(cwd, args) {
5
+ const { stdout } = await execFileAsync("git", args, { cwd, timeout: 5000 });
6
+ return stdout.trim();
7
+ }
8
+ export async function getGitStatus(cwd) {
9
+ let isRepo = false;
10
+ try {
11
+ const out = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
12
+ isRepo = out === "true";
13
+ }
14
+ catch {
15
+ isRepo = false;
16
+ }
17
+ if (!isRepo) {
18
+ return { isRepo: false, branch: null, dirty: false, ahead: 0, behind: 0, detached: false };
19
+ }
20
+ const branch = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
21
+ const detached = branch === "HEAD";
22
+ let dirty = false;
23
+ try {
24
+ const status = await git(cwd, ["status", "--porcelain"]);
25
+ dirty = status.length > 0;
26
+ }
27
+ catch {
28
+ dirty = false;
29
+ }
30
+ let ahead = 0;
31
+ let behind = 0;
32
+ try {
33
+ const counts = await git(cwd, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
34
+ const parts = counts.split(/\s+/);
35
+ ahead = Number(parts[0]) || 0;
36
+ behind = Number(parts[1]) || 0;
37
+ }
38
+ catch {
39
+ ahead = 0;
40
+ behind = 0;
41
+ }
42
+ return {
43
+ isRepo: true,
44
+ branch: detached ? null : branch,
45
+ dirty,
46
+ ahead,
47
+ behind,
48
+ detached,
49
+ };
50
+ }
51
+ export async function getGitBranches(cwd) {
52
+ let isRepo = false;
53
+ try {
54
+ const out = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
55
+ isRepo = out === "true";
56
+ }
57
+ catch {
58
+ isRepo = false;
59
+ }
60
+ if (!isRepo) {
61
+ return { isRepo: false, branches: [], current: null };
62
+ }
63
+ const raw = await git(cwd, ["branch", "--format=%(refname:short)"]);
64
+ const branches = raw
65
+ .split("\n")
66
+ .map((b) => b.trim())
67
+ .filter((b) => b.length > 0);
68
+ const branch = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
69
+ const current = branch === "HEAD" ? null : branch;
70
+ return { isRepo: true, branches, current };
71
+ }
72
+ export async function gitCheckout(cwd, branch) {
73
+ if (!/^(?!-)[A-Za-z0-9._/-]+$/.test(branch)) {
74
+ throw new Error("Invalid branch name");
75
+ }
76
+ await git(cwd, ["checkout", branch]);
77
+ return { ok: true, branch };
78
+ }
@@ -1,3 +1,4 @@
1
+ import type { AgentIdentity } from "./bridge.js";
1
2
  import type { VaultMindConfig } from "./types.js";
2
3
  export declare const graphUpsert: (dataDir: string, cfg: VaultMindConfig, entry: Record<string, string>) => Promise<void>;
3
- export declare const queryGraph: (dataDir: string, cfg: VaultMindConfig, entityQuery: string, depth: number) => Promise<unknown>;
4
+ export declare const queryGraph: (dataDir: string, cfg: VaultMindConfig, entityQuery: string, depth: number, identity?: AgentIdentity) => Promise<unknown>;