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
@@ -9,16 +9,20 @@
9
9
  * No HTTP is reimplemented here — `ModalEmbeddingClient` owns that.
10
10
  */
11
11
  import * as fs from "node:fs";
12
- import * as path from "node:path";
12
+ import { EMBEDDING_FLAT_KEYS } from "./config-keys.js";
13
13
  import { ModalEmbeddingClient } from "./modal-client.js";
14
- /** Env var name for the Modal bearer token (preferred over config). */
15
- export const MODAL_TOKEN_ENV = "PVM_API_TOKEN";
14
+ import { deriveCollectionNameFromVaultPath, getTokenEnvPath, normalizeCollectionName, } from "./utils.js";
15
+ /** Env var name for the unscoped remote embedding API key (preferred over config). */
16
+ export const MODAL_TOKEN_ENV = "PVM_REMOTE_EMBEDDING_API_KEY";
16
17
  /** Env var name for the read-scoped Modal token (search/export). */
17
18
  export const MODAL_READ_TOKEN_ENV = "PVM_MODAL_READ_TOKEN";
18
19
  /** Env var name for the write-scoped Modal token (embed/reindex). */
19
20
  export const MODAL_WRITE_TOKEN_ENV = "PVM_MODAL_WRITE_TOKEN";
20
- /** Dotenv-style fallback path for the Modal token. */
21
- export const MODAL_TOKEN_ENV_PATH = path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent", "vault-mind.env");
21
+ /**
22
+ * Dotenv fallback path for the Modal token, resolved from the vault cwd:
23
+ * `<vault>/.vault-mind/vault-mind.env`. Lazy so tests can override cwd.
24
+ */
25
+ export const modalTokenEnvPath = (cwd = process.cwd()) => getTokenEnvPath(cwd);
22
26
  /** Canonical deployed Modal app name. */
23
27
  export const MODAL_APP_NAME = "pi-vault-mind-embed";
24
28
  /**
@@ -49,58 +53,63 @@ const readDotEnv = (filePath) => {
49
53
  /** Derive the Modal service URL from a workspace slug and canonical app name. */
50
54
  export const modalUrl = (workspace) => `https://${workspace}--${MODAL_APP_NAME}-embeddingservice-fastapi-app.modal.run`;
51
55
  /**
52
- * Resolve the Modal API token. Resolution order:
53
- * 1. `PVM_API_TOKEN` env var
54
- * 2. `~/.pi/agent/vault-mind.env`
55
- * 3. `vaultMind.embedding.modal.apiToken` in config
56
+ * Resolve the unscoped remote embedding API token. Resolution order:
57
+ * 1. `PVM_REMOTE_EMBEDDING_API_KEY` env var
58
+ * 2. `<vault>/.vault-mind/vault-mind.env`
59
+ * 3. `vaultMind.embedding.remoteApiKey` in config
60
+ *
61
+ * `PVM_API_TOKEN` is intentionally not consulted — it is reserved for the
62
+ * extension's bridge authentication.
56
63
  *
57
64
  * Never log the resolved token.
58
65
  */
59
66
  export const resolveModalToken = (cfg) => {
60
67
  if (process.env[MODAL_TOKEN_ENV])
61
68
  return process.env[MODAL_TOKEN_ENV];
62
- const dotenv = readDotEnv(MODAL_TOKEN_ENV_PATH);
69
+ const dotenv = readDotEnv(modalTokenEnvPath());
63
70
  if (dotenv[MODAL_TOKEN_ENV])
64
71
  return dotenv[MODAL_TOKEN_ENV];
65
- return cfg.embedding.modal?.apiToken;
72
+ return cfg.embedding[EMBEDDING_FLAT_KEYS[7]];
66
73
  };
67
74
  /**
68
75
  * Resolve a scoped Modal token for the requested operation.
69
76
  * Resolution order:
70
77
  * 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)
78
+ * 2. `<vault>/.vault-mind/vault-mind.env`
79
+ * 3. `vaultMind.embedding.remoteReadApiKey` / `remoteWriteApiKey` in config
80
+ * 4. Fallback to the unscoped `PVM_REMOTE_EMBEDDING_API_KEY` (env, dotenv, then config)
81
+ *
82
+ * `PVM_API_TOKEN` is intentionally not consulted — it is reserved for the
83
+ * extension's bridge authentication.
74
84
  *
75
85
  * Never log the resolved token.
76
86
  */
77
87
  export const getModalToken = (cfg, operation) => {
78
- const modal = cfg.embedding.modal;
79
- const dotenv = readDotEnv(MODAL_TOKEN_ENV_PATH);
88
+ const dotenv = readDotEnv(modalTokenEnvPath());
80
89
  if (operation === "read") {
81
90
  return (process.env[MODAL_READ_TOKEN_ENV] ||
82
91
  dotenv[MODAL_READ_TOKEN_ENV] ||
83
- modal?.readToken ||
92
+ cfg.embedding[EMBEDDING_FLAT_KEYS[8]] ||
84
93
  resolveModalToken(cfg));
85
94
  }
86
95
  return (process.env[MODAL_WRITE_TOKEN_ENV] ||
87
96
  dotenv[MODAL_WRITE_TOKEN_ENV] ||
88
- modal?.writeToken ||
97
+ cfg.embedding[EMBEDDING_FLAT_KEYS[9]] ||
89
98
  resolveModalToken(cfg));
90
99
  };
91
- /** Resolve the explicit baseUrl or derive it from workspace. */
100
+ /** Derive the remote URL from the top-level `remoteUrl` or the `workspace` shortcut. */
92
101
  export const resolveBaseUrl = (cfg) => {
93
- const modal = cfg.embedding.modal;
94
- if (modal?.baseUrl)
95
- return modal.baseUrl;
96
- if (modal?.workspace)
97
- return modalUrl(modal.workspace);
102
+ if (cfg.embedding[EMBEDDING_FLAT_KEYS[0]])
103
+ return cfg.embedding[EMBEDDING_FLAT_KEYS[0]];
104
+ if (cfg.embedding[EMBEDDING_FLAT_KEYS[6]])
105
+ return modalUrl(cfg.embedding[EMBEDDING_FLAT_KEYS[6]]);
98
106
  return undefined;
99
107
  };
100
108
  /** Resolve the configured workspace slug, if any. */
101
- export const resolveWorkspace = (cfg) => cfg.embedding.modal?.workspace;
109
+ export const resolveWorkspace = (cfg) => cfg.embedding[EMBEDDING_FLAT_KEYS[6]];
102
110
  /** 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"));
111
+ export const isModalConfigured = (cfg) => !!cfg.embedding[EMBEDDING_FLAT_KEYS[0]] &&
112
+ (!!getModalToken(cfg, "read") || !!getModalToken(cfg, "write"));
104
113
  /**
105
114
  * Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
106
115
  * configured (no base URL / token) so callers can degrade gracefully.
@@ -124,21 +133,39 @@ export const createModalClient = (cfg) => {
124
133
  * hard-coded dimension).
125
134
  */
126
135
  export const resolveModel = (cfg, collection) => {
127
- const override = collection ? cfg.embedding.collectionModels?.[collection] : undefined;
136
+ const override = collection ? cfg.embedding[EMBEDDING_FLAT_KEYS[12]]?.[collection] : undefined;
128
137
  if (override?.model)
129
138
  return override.model;
130
- return cfg.embedding.modal?.model || "embeddinggemma";
139
+ return cfg.embedding[EMBEDDING_FLAT_KEYS[2]] || "embeddinggemma";
131
140
  };
132
141
  /**
133
142
  * The configured output dimension for a collection, if known. Per-collection
134
- * overrides win; otherwise the global Modal config `dim`. Returns undefined
135
- * when unset — callers resolve the native dim lazily via `/models`.
143
+ * overrides win; otherwise the top-level `dim` on `EmbeddingConfig`.
144
+ * Returns undefined when unset — callers resolve the native dim lazily via `/models`.
136
145
  */
137
146
  export const resolveDim = (cfg, collection) => {
138
- const override = collection ? cfg.embedding.collectionModels?.[collection] : undefined;
147
+ const override = collection ? cfg.embedding[EMBEDDING_FLAT_KEYS[12]]?.[collection] : undefined;
139
148
  if (override?.dim != null)
140
149
  return override.dim;
141
- return cfg.embedding.modal?.dim;
150
+ return cfg.embedding[EMBEDDING_FLAT_KEYS[4]];
142
151
  };
143
152
  /** Namespaced local table name mirroring the server's ADR-3 scheme. */
144
153
  export const namespacedTableName = (collection, model, dim) => `col_${collection}__${model}__${dim}`;
154
+ /**
155
+ * Resolve the server-side (Modal) collection table prefix for cross-vault
156
+ * isolation on a shared Modal deployment. Unset or `"auto"` derives it from
157
+ * the vault slug. The result is normalized (no `__`, no leading/trailing `_`)
158
+ * so it is safe for the server's `col_(.+)__(.+)__(\d+)$` parse.
159
+ *
160
+ * Applies ONLY to remote table names — local LanceDB tables stay unprefixed.
161
+ */
162
+ export const resolveCollectionPrefix = (cfg, vaultPath) => {
163
+ const raw = cfg.vaults?.default?.collectionPrefix;
164
+ const base = raw && raw !== "auto" ? raw : deriveCollectionNameFromVaultPath(vaultPath);
165
+ return normalizeCollectionName(base) || undefined;
166
+ };
167
+ /**
168
+ * Prepend the collection prefix to a collection name for server-side table
169
+ * identity. When `prefix` is undefined, returns the collection name unchanged.
170
+ */
171
+ export const serverCollectionName = (prefix, collection) => prefix ? `${prefix}_${collection}` : collection;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Model-router config scaffolding — shared source of truth for the tuned
3
+ * default primary/fallback model sequence and the initial `model-router.json`
4
+ * shape.
5
+ *
6
+ * Extracted from the Obsidian plugin (`packages/obsidian/src/main.ts`
7
+ * `defaultModelRouterChoices`/`scaffoldModelRouterConfig`), which previously
8
+ * carried the only implementation — a CLI-only setup got no equivalent
9
+ * scaffold at all. The plugin is expected to switch to calling this (or an
10
+ * equivalent extension-exposed path) in a future refactor rather than
11
+ * maintaining its own copy.
12
+ */
13
+ /** Tuned default primary model + fallback sequence for the `auto` router profile. */
14
+ export declare const defaultModelRouterChoices: () => {
15
+ primary: string;
16
+ fallbackSequence: string[];
17
+ };
18
+ /**
19
+ * Scaffold `model-router.json` at the vault-scoped agent dir
20
+ * (`resolveVaultMindPaths(cwd).modelRouter`) with tuned defaults, if it
21
+ * doesn't already exist. No-op (returns `created: false`) when the file is
22
+ * already present — never overwrites a user's router config.
23
+ */
24
+ export declare const scaffoldModelRouterConfig: (cwd: string) => {
25
+ created: boolean;
26
+ path: string;
27
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Model-router config scaffolding — shared source of truth for the tuned
3
+ * default primary/fallback model sequence and the initial `model-router.json`
4
+ * shape.
5
+ *
6
+ * Extracted from the Obsidian plugin (`packages/obsidian/src/main.ts`
7
+ * `defaultModelRouterChoices`/`scaffoldModelRouterConfig`), which previously
8
+ * carried the only implementation — a CLI-only setup got no equivalent
9
+ * scaffold at all. The plugin is expected to switch to calling this (or an
10
+ * equivalent extension-exposed path) in a future refactor rather than
11
+ * maintaining its own copy.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import { ensureDir, resolveVaultMindPaths } from "./utils.js";
15
+ /** Tuned default primary model + fallback sequence for the `auto` router profile. */
16
+ export const defaultModelRouterChoices = () => {
17
+ const isMacOS = process.platform === "darwin";
18
+ return {
19
+ primary: "ollama/gemma4:31b-cloud",
20
+ fallbackSequence: isMacOS
21
+ ? ["ollama/gemma4:e4b", "ollama/gemma4:12b-mlx", "ollama/gemma3:4b", "ollama/*"]
22
+ : ["ollama/gemma4:e4b", "ollama/gemma4:12b", "ollama/gemma3:4b", "ollama/*"],
23
+ };
24
+ };
25
+ /**
26
+ * Scaffold `model-router.json` at the vault-scoped agent dir
27
+ * (`resolveVaultMindPaths(cwd).modelRouter`) with tuned defaults, if it
28
+ * doesn't already exist. No-op (returns `created: false`) when the file is
29
+ * already present — never overwrites a user's router config.
30
+ */
31
+ export const scaffoldModelRouterConfig = (cwd) => {
32
+ const { modelRouter: configPath } = resolveVaultMindPaths(cwd);
33
+ if (fs.existsSync(configPath))
34
+ return { created: false, path: configPath };
35
+ const { primary: mediumModel, fallbackSequence } = defaultModelRouterChoices();
36
+ const config = {
37
+ defaultProfile: "auto",
38
+ features: {
39
+ rateLimitFallback: true,
40
+ ollamaSync: false,
41
+ scopeShim: true,
42
+ perTurnRouting: false,
43
+ intentClassifier: false,
44
+ costBudgeting: false,
45
+ phaseMemory: false,
46
+ contextCompression: false,
47
+ },
48
+ rateLimitFallback: {
49
+ enabled: true,
50
+ shortDelayThreshold: 30,
51
+ autoFallback: true,
52
+ autoRestore: true,
53
+ restoreCheckInterval: 300,
54
+ fallbackSequence,
55
+ },
56
+ profiles: {
57
+ auto: {
58
+ high: { model: "ollama/gemma4:31b-cloud", thinking: "medium" },
59
+ medium: { model: mediumModel, thinking: "low" },
60
+ low: { model: "ollama/gemma4:e4b", thinking: "off" },
61
+ },
62
+ },
63
+ };
64
+ ensureDir(configPath);
65
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
66
+ return { created: true, path: configPath };
67
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Server-side reader for pi's `models.json` provider/model catalog.
3
+ *
4
+ * Mirrors the plugin's `packages/obsidian/src/chat/pi-config-reader.ts`
5
+ * (`readPiModelsConfig`) so the `GET /vault-mind/models` endpoint returns the
6
+ * same flattened `ProviderInfo[]` shape the plugin already consumes — letting
7
+ * it replace synchronous disk reads with a `VaultMindClient.getModels()` call.
8
+ *
9
+ * Reads two locations and merges (project overrides agent-dir):
10
+ * 1. Agent-dir: `resolveVaultMindPaths(vault).modelsJson`
11
+ * (= `<vault>/.vault-mind/.pi/agent/models.json`)
12
+ * 2. Project: `<vault>/.pi/models.json`
13
+ *
14
+ * Uses plain `JSON.parse` (consistent with the `/vm doctor` reader in
15
+ * commands.ts) — pi writes machine-generated strict JSON. A parse failure
16
+ * degrades gracefully to `{ providers: [], error }`.
17
+ */
18
+ /** A single model entry, flattened for UI/transport. */
19
+ export interface ModelInfo {
20
+ id: string;
21
+ name: string;
22
+ reasoning?: boolean;
23
+ contextWindow?: number;
24
+ }
25
+ /** A provider entry with its models and the env var it expects for auth. */
26
+ export interface ProviderInfo {
27
+ name: string;
28
+ /** Env var name the provider expects. Only exact $NAME or ${NAME} references are accepted; all others yield "". */
29
+ envVarName: string;
30
+ baseUrl?: string;
31
+ api?: string;
32
+ models: ModelInfo[];
33
+ }
34
+ /**
35
+ * Read + merge pi's `models.json` for a vault and flatten to `ProviderInfo[]`.
36
+ * Returns `{ providers, error? }`; `error` is set only when a config file
37
+ * exists but cannot be parsed (matching the plugin's contract).
38
+ */
39
+ export declare const readModelProviders: (vaultPath: string) => {
40
+ providers: ProviderInfo[];
41
+ error?: string;
42
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Server-side reader for pi's `models.json` provider/model catalog.
3
+ *
4
+ * Mirrors the plugin's `packages/obsidian/src/chat/pi-config-reader.ts`
5
+ * (`readPiModelsConfig`) so the `GET /vault-mind/models` endpoint returns the
6
+ * same flattened `ProviderInfo[]` shape the plugin already consumes — letting
7
+ * it replace synchronous disk reads with a `VaultMindClient.getModels()` call.
8
+ *
9
+ * Reads two locations and merges (project overrides agent-dir):
10
+ * 1. Agent-dir: `resolveVaultMindPaths(vault).modelsJson`
11
+ * (= `<vault>/.vault-mind/.pi/agent/models.json`)
12
+ * 2. Project: `<vault>/.pi/models.json`
13
+ *
14
+ * Uses plain `JSON.parse` (consistent with the `/vm doctor` reader in
15
+ * commands.ts) — pi writes machine-generated strict JSON. A parse failure
16
+ * degrades gracefully to `{ providers: [], error }`.
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { resolveVaultMindPaths } from "./utils.js";
21
+ /**
22
+ * Read + merge pi's `models.json` for a vault and flatten to `ProviderInfo[]`.
23
+ * Returns `{ providers, error? }`; `error` is set only when a config file
24
+ * exists but cannot be parsed (matching the plugin's contract).
25
+ */
26
+ export const readModelProviders = (vaultPath) => {
27
+ const merged = {};
28
+ let error;
29
+ let configExists = false;
30
+ const agentModelsPath = resolveVaultMindPaths(vaultPath).modelsJson;
31
+ const projectModelsPath = path.join(vaultPath, ".pi", "models.json");
32
+ for (const [label, file] of [
33
+ ["agent-dir", agentModelsPath],
34
+ ["project", projectModelsPath],
35
+ ]) {
36
+ try {
37
+ if (!fs.existsSync(file))
38
+ continue;
39
+ configExists = true;
40
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
41
+ if (parsed.providers) {
42
+ // Project-level overrides agent-dir for same provider names.
43
+ Object.assign(merged, parsed.providers);
44
+ }
45
+ }
46
+ catch (err) {
47
+ error = `Failed to read ${label} models.json: ${err instanceof Error ? err.message : String(err)}`;
48
+ }
49
+ }
50
+ if (configExists && Object.keys(merged).length === 0 && error) {
51
+ return { providers: [], error };
52
+ }
53
+ const providers = Object.entries(merged).map(([name, config]) => {
54
+ // Only exact shell-style references ($VAR_NAME or ${VAR_NAME}) are converted to raw
55
+ // env var names; all other apiKey values yield an empty string to prevent literal leakage.
56
+ let envVarName = "";
57
+ if (typeof config.apiKey === "string") {
58
+ const match = config.apiKey.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/) ||
59
+ config.apiKey.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/);
60
+ if (match)
61
+ envVarName = match[1];
62
+ }
63
+ return {
64
+ name,
65
+ envVarName,
66
+ baseUrl: config.baseUrl,
67
+ api: config.api,
68
+ models: (config.models ?? []).map((m) => ({
69
+ id: m.id,
70
+ name: m.name || m.id,
71
+ reasoning: m.reasoning,
72
+ contextWindow: m.contextWindow,
73
+ })),
74
+ };
75
+ });
76
+ providers.sort((a, b) => a.name.localeCompare(b.name));
77
+ return { providers, error };
78
+ };
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
4
4
  import { getStatus } from "./lance.js";
5
- import { expandHome, loadConfig } from "./utils.js";
5
+ import { expandHome, loadConfig, resolveAgentDir } from "./utils.js";
6
6
  /** Resolve the vault path: configured default vault wins, otherwise ctx.cwd. */
7
7
  export const resolveVaultPath = (ctx) => {
8
8
  const cfg = loadConfig(ctx.cwd);
@@ -11,7 +11,7 @@ export const resolveVaultPath = (ctx) => {
11
11
  };
12
12
  /** Load current agent configuration files if they exist. */
13
13
  export const loadCurrentConfig = (vaultPath) => {
14
- const systemMdPath = path.join(vaultPath, ".pi", "agent", "system.md");
14
+ const systemMdPath = path.join(resolveAgentDir(vaultPath), "system.md");
15
15
  const agentsMdPath = path.join(vaultPath, "AGENTS.md");
16
16
  return {
17
17
  systemMd: fs.existsSync(systemMdPath) ? fs.readFileSync(systemMdPath, "utf-8") : null,
@@ -305,6 +305,20 @@ const resolveCurrentContent = (filePath, currentConfig) => {
305
305
  }
306
306
  return null;
307
307
  };
308
+ /**
309
+ * Resolve a suggestion's relative path to its on-disk location. The framework
310
+ * `system.md` lives in the resolved agent dir (which may be outside the vault),
311
+ * while everything else (AGENTS.md, etc.) is vault-relative.
312
+ */
313
+ const resolveSuggestionPath = (filePath, vaultPath) => {
314
+ if (path.isAbsolute(filePath))
315
+ return filePath;
316
+ const normalized = filePath.replace(/\\/g, "/");
317
+ if (normalized === ".pi/agent/system.md" || normalized.endsWith("/.pi/agent/system.md")) {
318
+ return path.join(resolveAgentDir(vaultPath), "system.md");
319
+ }
320
+ return path.join(vaultPath, filePath);
321
+ };
308
322
  /** Present each suggestion diff to the user and write approved files. */
309
323
  export const presentAndApplyDiff = async (suggestions, ctx, currentConfig) => {
310
324
  const approved = [];
@@ -321,7 +335,7 @@ export const presentAndApplyDiff = async (suggestions, ctx, currentConfig) => {
321
335
  const message = [suggestions.commentary, diff].filter(Boolean).join("\n\n");
322
336
  const ok = await ctx.ui.confirm(title, message);
323
337
  if (ok) {
324
- const fullPath = path.isAbsolute(file.path) ? file.path : path.join(vaultPath, file.path);
338
+ const fullPath = resolveSuggestionPath(file.path, vaultPath);
325
339
  const dir = path.dirname(fullPath);
326
340
  if (!fs.existsSync(dir))
327
341
  fs.mkdirSync(dir, { recursive: true });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Vault config scaffolding — the single, UI-agnostic implementation of
3
+ * "create/merge-safe-update the config + collection files + injector
4
+ * artifacts + vault-root .gitignore." Shared by every entry point that needs
5
+ * to scaffold a vault: the `/vm setup` CLI command (`commands.ts`), the HTTP
6
+ * `/vm/setup` API route (`server.ts`), and any future consumer (e.g. an
7
+ * Obsidian plugin refactor) — none of them should carry their own copy of
8
+ * the collections/injectors template.
9
+ */
10
+ /**
11
+ * Vault-root `.gitignore` entries that keep pi-vault-mind compatible with
12
+ * `obsidian-git` (and, transitively, Obsidian Sync setups that also back up via
13
+ * git). The `.vault-mind/` directory holds local state (token, index, framework
14
+ * dir) that must never be committed, EXCEPT the hand-tuned config file, which is
15
+ * re-included via a `!` negation. The LanceDB index is a large, per-device,
16
+ * rebuildable binary; Obsidian's workspace UI state churns constantly. See
17
+ * docs/OBSIDIAN_SETUP.md §6.
18
+ */
19
+ export declare const GITIGNORE_ENTRIES: string[];
20
+ /**
21
+ * Decide what to do with the vault `.gitignore`, given its current contents
22
+ * (`null` if absent). Pure — does no I/O — so it is unit-testable. Creates the
23
+ * file when missing, appends only the entries that aren't already present
24
+ * (line-exact match, trimmed), and skips when everything is covered.
25
+ *
26
+ * A bare `.vault-mind/` directory exclude would block the per-file config
27
+ * negation (git cannot re-include a file inside an ignored directory), so when
28
+ * one is present it is rewritten to `.vault-mind/*` and the whole file returned.
29
+ */
30
+ export declare const planGitignore: (existing: string | null) => {
31
+ action: "create" | "append" | "rewrite" | "skip";
32
+ content: string;
33
+ };
34
+ export interface ScaffoldResult {
35
+ created: string[];
36
+ updated: string[];
37
+ skipped: string[];
38
+ /** The resolved default-collection file name (vault-derived unless overridden). */
39
+ collectionName: string;
40
+ }
41
+ /**
42
+ * Scaffold (or merge-safe-update) `.vault-mind/vault-mind.config.json`,
43
+ * the default collection files, injector artifacts, and vault-root
44
+ * `.gitignore` for `vaultPath`. UI-agnostic — returns what happened instead
45
+ * of notifying; callers (a CLI command, an HTTP handler, anything else)
46
+ * render the result however fits their surface.
47
+ */
48
+ export declare const scaffoldVaultConfig: (vaultPath: string, collectionOverride?: string) => ScaffoldResult;