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.
- package/CHANGELOG.md +293 -0
- package/README.md +5 -5
- package/agents/broadcaster.agent.md +2 -2
- package/agents/heavy-lifter.agent.md +1 -1
- package/agents/main.agent.md +29 -0
- package/agents/manager.agent.md +1 -1
- package/agents/miner.agent.md +1 -1
- package/dist/packages/obsidian/src/client.js +331 -0
- package/dist/packages/obsidian/src/types.js +1 -0
- package/dist/src/activity.d.ts +49 -0
- package/dist/src/activity.js +175 -0
- package/dist/src/agent-bus/Agent.d.ts +9 -1
- package/dist/src/agent-bus/Agent.js +18 -2
- package/dist/src/agent-bus/index.d.ts +5 -5
- package/dist/src/agent-bus/index.js +1 -1
- package/dist/src/agent-queue.js +12 -3
- package/dist/src/agents/BroadcasterAgent.d.ts +1 -1
- package/dist/src/agents/BroadcasterAgent.js +2 -2
- package/dist/src/agents/HeavyLifterAgent.d.ts +1 -1
- package/dist/src/agents/HeavyLifterAgent.js +2 -2
- package/dist/src/agents/ManagerAgent.js +1 -1
- package/dist/src/agents/MinerAgent.d.ts +1 -1
- package/dist/src/agents/MinerAgent.js +2 -2
- package/dist/src/agents/index.d.ts +2 -2
- package/dist/src/agents/index.js +2 -2
- package/dist/src/auth.d.ts +17 -15
- package/dist/src/auth.js +26 -37
- package/dist/src/autosync.js +2 -2
- package/dist/src/commands.d.ts +2 -19
- package/dist/src/commands.js +301 -231
- package/dist/src/config-keys.d.ts +16 -0
- package/dist/src/config-keys.js +45 -0
- package/dist/src/discover-schema.js +4 -4
- package/dist/src/edits.d.ts +21 -0
- package/dist/src/edits.js +75 -0
- package/dist/src/embedding-probe.d.ts +34 -0
- package/dist/src/embedding-probe.js +142 -0
- package/dist/src/embedding-providers.d.ts +133 -0
- package/dist/src/embedding-providers.js +173 -0
- package/dist/src/embedding-secrets.d.ts +38 -0
- package/dist/src/embedding-secrets.js +291 -0
- package/dist/src/engine.d.ts +6 -0
- package/dist/src/engine.js +78 -6
- package/dist/src/events.js +7 -10
- package/dist/src/extension-packages.d.ts +9 -0
- package/dist/src/extension-packages.js +14 -0
- package/dist/src/git.d.ts +19 -0
- package/dist/src/git.js +78 -0
- package/dist/src/graph.d.ts +2 -1
- package/dist/src/graph.js +47 -13
- package/dist/src/identities-config.d.ts +6 -0
- package/dist/src/identities-config.js +39 -0
- package/dist/src/identity-injector.js +1 -1
- package/dist/src/index.js +9 -26
- package/dist/src/lance.d.ts +9 -0
- package/dist/src/lance.js +143 -203
- package/dist/src/modal-client.d.ts +16 -0
- package/dist/src/modal-client.js +16 -0
- package/dist/src/modal-config.d.ts +37 -14
- package/dist/src/modal-config.js +59 -32
- package/dist/src/model-router.d.ts +27 -0
- package/dist/src/model-router.js +67 -0
- package/dist/src/models.d.ts +42 -0
- package/dist/src/models.js +78 -0
- package/dist/src/personalize.js +17 -3
- package/dist/src/scaffold.d.ts +48 -0
- package/dist/src/scaffold.js +246 -0
- package/dist/src/server.d.ts +47 -4
- package/dist/src/server.js +988 -221
- package/dist/src/session-search.d.ts +63 -0
- package/dist/src/session-search.js +379 -0
- package/dist/src/settings-ui.d.ts +2 -2
- package/dist/src/settings-ui.js +76 -135
- package/dist/src/sync.d.ts +3 -3
- package/dist/src/sync.js +19 -15
- package/dist/src/tool-catalog.d.ts +18 -0
- package/dist/src/tool-catalog.js +232 -0
- package/dist/src/tools/marksman.js +1 -1
- package/dist/src/tools.js +111 -65
- package/dist/src/types.d.ts +127 -71
- package/dist/src/utils.d.ts +57 -6
- package/dist/src/utils.js +114 -28
- package/dist/src/vault-tools.d.ts +34 -0
- package/dist/src/vault-tools.js +99 -0
- package/dist/src/vault-writer.js +4 -1
- package/dist/src/vm-handlers.js +140 -43
- package/dist/src/watcher.js +9 -9
- package/dist/test/activity-tool.test.js +54 -0
- package/dist/test/agent-bus/Agent.test.js +174 -0
- package/dist/test/agent-bus/AgentLoader.test.js +180 -0
- package/dist/test/agent-bus/AgentRegistry.test.js +86 -0
- package/dist/test/agent-bus/LLMProvider.test.js +162 -0
- package/dist/test/agent-bus/TaskQueue.test.js +256 -0
- package/dist/test/agent-dispatch-context.test.js +73 -0
- package/dist/test/agent-queue.test.js +1 -1
- package/dist/test/agents/Agents.test.js +282 -0
- package/dist/test/auth.test.js +57 -56
- package/dist/test/bridge-identity.test.js +34 -3
- package/dist/test/classify-search-mode.test.js +51 -0
- package/dist/test/commands.test.js +410 -0
- package/dist/test/config-merge.test.js +98 -0
- package/dist/test/context-capture.test.js +56 -0
- package/dist/test/discover-schema.test.js +8 -5
- package/dist/test/dispatch.test.js +45 -91
- package/dist/test/edits.test.js +144 -0
- package/dist/test/embedding-probe.test.js +424 -0
- package/dist/test/embedding-providers.test.js +157 -0
- package/dist/test/embedding-secrets.test.js +210 -0
- package/dist/test/engine-identity.test.js +1 -1
- package/dist/test/events.test.js +675 -0
- package/dist/test/git-bridge.test.js +160 -0
- package/dist/test/graph.test.js +464 -0
- package/dist/test/helpers/server.js +81 -0
- package/dist/test/identity-injector.test.js +197 -0
- package/dist/test/index.test.js +5 -7
- package/dist/test/lance-modal.test.js +67 -2
- package/dist/test/lance.test.js +429 -0
- package/dist/test/modal-client.test.js +1 -1
- package/dist/test/modal-config.test.js +92 -79
- package/dist/test/model-readiness.test.js +122 -0
- package/dist/test/model-router.test.js +99 -0
- package/dist/test/obsidian-client.test.js +42 -0
- package/dist/test/personalize.test.js +1 -1
- package/dist/test/rest-models.test.js +153 -0
- package/dist/test/rest-queue.test.js +135 -87
- package/dist/test/rest-setup.test.js +625 -115
- package/dist/test/rest-vm.test.js +355 -94
- package/dist/test/server-cors.test.js +48 -0
- package/dist/test/server-ws.test.js +61 -34
- package/dist/test/session-crud.test.js +323 -0
- package/dist/test/session-search.test.js +139 -0
- package/dist/test/settings-ui.test.js +113 -4
- package/dist/test/sync.test.js +12 -12
- package/dist/test/tombstone.test.js +8 -10
- package/dist/test/tool-catalog.test.js +86 -0
- package/dist/test/utils.test.js +588 -0
- package/dist/test/vault-tools.test.js +103 -12
- package/dist/test/vault-writer.test.js +371 -0
- package/dist/test/vm-handlers.test.js +620 -0
- package/extension-packages.json +9 -0
- package/package.json +107 -90
- package/pi-vault-mind.config.example.json +24 -2
- package/scripts/check-deps.sh +35 -32
- package/scripts/cli-e2e.mjs +215 -0
- package/scripts/configuration-e2e.mjs +783 -0
- package/scripts/e2e-commands.mjs +391 -0
- package/scripts/e2e-ws.mjs +3 -2
- package/scripts/fetch-modal-token.sh +4 -3
- package/scripts/generate-config-keys-json.mjs +45 -0
- package/scripts/generate-extension-packages-json.mjs +55 -0
- package/scripts/pi-session-query +0 -0
- package/scripts/publish-obsidian-plugin.sh +0 -0
- package/scripts/reset-test-vault.sh +2 -2
- package/scripts/setup-test-config.sh +0 -0
- package/scripts/setup-vault-pi.sh +48 -9
- package/scripts/test-slash-commands.mjs +2 -2
- package/scripts/vault-pi.sh +12 -3
- package/skills/github-release-notes-file/SKILL.md +42 -0
- package/skills/obsidian-guided-test-session/SKILL.md +110 -0
- package/skills/obsidian-interactive-test/SKILL.md +88 -0
- package/skills/obsidian-plugin-publish/SKILL.md +58 -0
- package/skills/pi-vault-mind-release/SKILL.md +141 -0
- package/skills/pre-test-release/SKILL.md +116 -0
- package/dist/test/auto-indexer.test.js +0 -2
package/dist/src/commands.js
CHANGED
|
@@ -3,31 +3,34 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import * as lancedb from "@lancedb/lancedb";
|
|
6
|
-
import {
|
|
7
|
-
import { DEFAULT_CONFIG } from "./types.js";
|
|
8
|
-
import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, resolveInitCollectionName, } from "./utils.js";
|
|
9
|
-
import { updateActiveCollectionWidget } from "./widget.js";
|
|
10
|
-
import { cancelJobHelper, listJobs, retryJob, } from "./agent-queue.js";
|
|
6
|
+
import { cancelJobHelper, listJobs, retryJob } from "./agent-queue.js";
|
|
11
7
|
import { revokeToken, rotateToken } from "./auth.js";
|
|
8
|
+
import { EMBEDDING_FLAT_KEYS } from "./config-keys.js";
|
|
12
9
|
import { handleDiscoverSchema } from "./discover-schema.js";
|
|
10
|
+
import { writeEnvEntry } from "./embedding-secrets.js";
|
|
11
|
+
import { queryGraph } from "./graph.js";
|
|
13
12
|
import { runAskIntake } from "./intake.js";
|
|
14
|
-
import { connect, searchHybrid } from "./lance.js";
|
|
15
|
-
import {
|
|
13
|
+
import { connect, searchFts, searchHybrid, searchHybridRanked } from "./lance.js";
|
|
14
|
+
import { createModalClient, isModalConfigured, MODAL_TOKEN_ENV, modalTokenEnvPath, resolveBaseUrl, resolveModalToken, resolveWorkspace, } from "./modal-config.js";
|
|
15
|
+
import { scaffoldModelRouterConfig } from "./model-router.js";
|
|
16
16
|
import { runPersonalize } from "./personalize.js";
|
|
17
|
-
import {
|
|
17
|
+
import { scaffoldVaultConfig } from "./scaffold.js";
|
|
18
|
+
import { createServerState, startServer } from "./server.js";
|
|
18
19
|
import { createCollectionWizard, createInjectorWizard, openSettingsDashboard, setupWizard, } from "./settings-ui.js";
|
|
20
|
+
import { getActiveCollection, setActiveCollection } from "./state.js";
|
|
19
21
|
import { reindexRemote, syncAll, syncCollection } from "./sync.js";
|
|
20
22
|
import { tombstoneEntryById } from "./tombstone.js";
|
|
21
|
-
import {
|
|
23
|
+
import { DEFAULT_CONFIG } from "./types.js";
|
|
24
|
+
import { collectionNames, ensureDir, expandHome, findConfig, getConfigPath, getPiContextConfig, hasPiContextTools, isVaultMindReady, loadConfig, resolveVaultMindPaths, } from "./utils.js";
|
|
22
25
|
import { createWatcherState, getWatcherStatus, startWatcher, stopWatcher } from "./watcher.js";
|
|
26
|
+
import { updateActiveCollectionWidget } from "./widget.js";
|
|
23
27
|
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
24
28
|
const VM_USAGE = [
|
|
25
29
|
"**pi-vault-mind Commands**",
|
|
26
|
-
" /vm init [--collection <name>] Scaffold config + collections (default collection path uses vault name)",
|
|
27
30
|
" /vm validate Check LanceDB, config, and collection health",
|
|
28
31
|
" /vm validate --audit Audit config for missing defaults",
|
|
29
32
|
" /vm approve [collection] Batch-review pending entries",
|
|
30
|
-
" /vm
|
|
33
|
+
" /vm search [collection] <query> [--mode=hybrid|semantic|fts|graph|full] Unified search",
|
|
31
34
|
" /vm tombstone <collection> <entry-id> Redact an entry in place",
|
|
32
35
|
" /vm settings Open interactive settings dashboard",
|
|
33
36
|
" /vm collection select Select active collection",
|
|
@@ -51,22 +54,25 @@ const VM_USAGE = [
|
|
|
51
54
|
" /vm watcher retry <id> Retry a failed or cancelled agent job",
|
|
52
55
|
" /vm watcher cancel <id> Cancel a pending agent job",
|
|
53
56
|
" /vm server status Show HTTP server status + port",
|
|
57
|
+
" /vm server start Start the in-process HTTP server",
|
|
54
58
|
" /vm personalize Analyze vault and suggest personality tuning for system.md + AGENTS.md",
|
|
55
59
|
" /vm setup Onboarding flow: config + personalize (auto-uses cwd when it's a vault)",
|
|
56
60
|
" /vm setup --vault <path> CLI: set vault path",
|
|
57
61
|
" /vm setup --remoteUrl <url> CLI: set remote embedding URL",
|
|
58
62
|
" /vm setup --localUrl <url> CLI: set local embedding URL",
|
|
59
63
|
" /vm setup --workspace <slug> CLI: derive Modal URL + set workspace",
|
|
64
|
+
" /vm setup --remoteApiKey <key> CLI: set remote API key",
|
|
65
|
+
" /vm setup --collection <name> CLI: override default collection name (defaults to vault name)",
|
|
60
66
|
" /vm token Show API token source and masked value",
|
|
61
67
|
" /vm token show Show API token source and masked value",
|
|
62
68
|
" /vm token rotate Generate + persist a new API token (old token invalidated)",
|
|
63
|
-
" /vm token revoke
|
|
69
|
+
" /vm token revoke <identity> Remove PVM_TOKEN_<IDENTITY> from .vault-mind/vault-mind.env",
|
|
64
70
|
" /vm help Show this help",
|
|
65
71
|
].join("\n");
|
|
66
72
|
export const auditConfig = async (ctx) => {
|
|
67
73
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
68
74
|
if (!cfgPath) {
|
|
69
|
-
ctx.ui.notify("No project config file found to audit. Run /vm
|
|
75
|
+
ctx.ui.notify("No project config file found to audit. Run /vm setup first.", "info");
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
72
78
|
const projectCfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
@@ -162,44 +168,64 @@ export const selectActiveCollection = async (ctx) => {
|
|
|
162
168
|
ctx.ui.notify(`Active collection set to: ${result}`, "info");
|
|
163
169
|
}
|
|
164
170
|
};
|
|
165
|
-
// ── /vm
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
171
|
+
// ── /vm doctor ──────────────────────────────────────────────────────────────
|
|
172
|
+
const handleDoctor = (ctx) => {
|
|
173
|
+
const paths = resolveVaultMindPaths(ctx.cwd);
|
|
174
|
+
const ready = isVaultMindReady(ctx.cwd);
|
|
175
|
+
const piAgentDirSet = !!process.env.PI_CODING_AGENT_DIR;
|
|
176
|
+
const stalePiAgent = fs.existsSync(path.join(ctx.cwd, ".pi", "agent"));
|
|
177
|
+
const hasGlobalPi = paths.agentDir.startsWith(path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi"));
|
|
178
|
+
// Count discovered providers/models from models.json
|
|
179
|
+
let providerCount = 0;
|
|
180
|
+
let modelCount = 0;
|
|
181
|
+
if (paths.exists.modelsJson) {
|
|
182
|
+
try {
|
|
183
|
+
const modelsJson = JSON.parse(fs.readFileSync(paths.modelsJson, "utf-8"));
|
|
184
|
+
const providers = modelsJson?.providers ?? {};
|
|
185
|
+
providerCount = Object.keys(providers).length;
|
|
186
|
+
for (const p of Object.values(providers)) {
|
|
187
|
+
modelCount += p.models?.length ?? 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* ignore parse errors */
|
|
192
|
+
}
|
|
187
193
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
194
|
+
const lines = [
|
|
195
|
+
"**Vault Mind Doctor**",
|
|
196
|
+
"",
|
|
197
|
+
`Vault: ${paths.vaultPath}`,
|
|
198
|
+
`Agent dir: ${paths.agentDir}`,
|
|
199
|
+
`Vault-mind dir: ${paths.vaultMindDir}`,
|
|
200
|
+
"",
|
|
201
|
+
"── Paths ──",
|
|
202
|
+
`Config: ${paths.config} ${paths.exists.config ? "✅" : "❌ missing"}`,
|
|
203
|
+
`Token env: ${paths.tokenEnv} ${paths.exists.tokenEnv ? "✅" : "❌ missing"}`,
|
|
204
|
+
`Queue: ${paths.queue}`,
|
|
205
|
+
`Server state: ${paths.serverState}`,
|
|
206
|
+
`Manual dispatch: ${paths.manualDispatch}`,
|
|
207
|
+
`Canvas: ${paths.canvasPath}`,
|
|
208
|
+
`Capture note: ${paths.captureNote ?? "(not configured)"}`,
|
|
209
|
+
"",
|
|
210
|
+
"── Framework ──",
|
|
211
|
+
`models.json: ${paths.modelsJson} ${paths.exists.modelsJson ? `✅ (${providerCount} provider(s), ${modelCount} model(s))` : "❌ missing"}`,
|
|
212
|
+
`model-router.json: ${paths.modelRouter} ${paths.exists.modelRouter ? "✅" : "❌ missing"}`,
|
|
213
|
+
"",
|
|
214
|
+
"── Status ──",
|
|
215
|
+
`Ready: ${ready ? "✅ yes" : "❌ no (config missing or embedding not configured)"}`,
|
|
216
|
+
`PI_CODING_AGENT_DIR: ${piAgentDirSet ? "✅ set" : "⚠️ not set"}`,
|
|
217
|
+
`Stale <vault>/.pi/agent: ${stalePiAgent ? "⚠️ exists (may be legacy)" : "✅ absent"}`,
|
|
218
|
+
"",
|
|
219
|
+
"── Auth ──",
|
|
220
|
+
"Provider auth is managed by pi (`pi` login). No new auth machinery here.",
|
|
221
|
+
"",
|
|
222
|
+
hasGlobalPi
|
|
223
|
+
? "⚠️ WARN: agentDir resolves under ~/.pi (global pi). Vault isolation may be incomplete."
|
|
224
|
+
: "✅ PASS: all paths are vault-local (no ~/.pi dependency).",
|
|
225
|
+
];
|
|
226
|
+
ctx.ui.notify(lines.join("\n"), ready ? "info" : "warning");
|
|
197
227
|
};
|
|
198
228
|
const handleInit = async (_args, ctx, pi) => {
|
|
199
|
-
const cfg = loadConfig(ctx.cwd);
|
|
200
|
-
const created = [];
|
|
201
|
-
const updated = [];
|
|
202
|
-
const skipped = [];
|
|
203
229
|
const initTokens = _args.trim().split(/\s+/).filter(Boolean);
|
|
204
230
|
let requestedCollection;
|
|
205
231
|
for (let i = 0; i < initTokens.length; i++) {
|
|
@@ -208,79 +234,7 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
208
234
|
i++;
|
|
209
235
|
}
|
|
210
236
|
}
|
|
211
|
-
const
|
|
212
|
-
const ensureFile = (dest, tmpl) => {
|
|
213
|
-
if (fs.existsSync(dest)) {
|
|
214
|
-
skipped.push(dest);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
ensureDir(dest);
|
|
218
|
-
const src = path.join(EXT_ROOT, "templates", tmpl);
|
|
219
|
-
fs.writeFileSync(dest, fs.existsSync(src) ? fs.readFileSync(src) : "", "utf-8");
|
|
220
|
-
created.push(dest);
|
|
221
|
-
};
|
|
222
|
-
const cfgDest = path.join(ctx.cwd, CONFIG_FILES[0]);
|
|
223
|
-
if (!fs.existsSync(cfgDest)) {
|
|
224
|
-
const tmpl = {
|
|
225
|
-
version: 2,
|
|
226
|
-
collections: {
|
|
227
|
-
main: {
|
|
228
|
-
path: `collections/${initCollection}.jsonl`,
|
|
229
|
-
schema: ["id", "domain", "source", "fact", "tag", "artifact"],
|
|
230
|
-
dedupField: "fact",
|
|
231
|
-
},
|
|
232
|
-
pending: { path: "collections/pending.jsonl", schema: "main" },
|
|
233
|
-
context_events: {
|
|
234
|
-
path: "collections/context_events.jsonl",
|
|
235
|
-
schema: ["id", "type", "session_entry_id", "content", "timestamp", "tags"],
|
|
236
|
-
dedupField: "id",
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
injectors: [
|
|
240
|
-
{
|
|
241
|
-
name: "draft-context",
|
|
242
|
-
regex: "draft\\s+(\\S+)",
|
|
243
|
-
collection: "main",
|
|
244
|
-
filterField: "tag",
|
|
245
|
-
artifactPath: "collections/artifact.md",
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
vaultMind: {
|
|
249
|
-
dataDir: ".lancedb",
|
|
250
|
-
ftsEnabled: true,
|
|
251
|
-
graph: { enabled: true, canvasSync: false },
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
fs.writeFileSync(cfgDest, `${JSON.stringify(tmpl, null, 2)}\n`, "utf-8");
|
|
255
|
-
created.push(cfgDest);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
skipped.push(cfgDest);
|
|
259
|
-
}
|
|
260
|
-
// Reload the config we just wrote so we scaffold the collections it defines.
|
|
261
|
-
const effectiveCfg = fs.existsSync(cfgDest) ? loadConfig(ctx.cwd) : cfg;
|
|
262
|
-
for (const [name, def] of Object.entries(effectiveCfg.collections)) {
|
|
263
|
-
ensureFile(def.path, name === "pending" ? "PENDING_LEDGER.jsonl" : "UCL_LEDGER.jsonl");
|
|
264
|
-
}
|
|
265
|
-
for (const ij of effectiveCfg.injectors) {
|
|
266
|
-
if (ij.artifactPath)
|
|
267
|
-
ensureFile(ij.artifactPath, "ARTIFACT.md");
|
|
268
|
-
}
|
|
269
|
-
// Keep the rebuildable binary index out of git so obsidian-git (and git-backed
|
|
270
|
-
// Obsidian Sync setups) never commit it. See docs/OBSIDIAN_SETUP.md §6.
|
|
271
|
-
const gitignoreDest = path.join(ctx.cwd, ".gitignore");
|
|
272
|
-
const plan = planGitignore(fs.existsSync(gitignoreDest) ? fs.readFileSync(gitignoreDest, "utf-8") : null);
|
|
273
|
-
if (plan.action === "create") {
|
|
274
|
-
fs.writeFileSync(gitignoreDest, plan.content, "utf-8");
|
|
275
|
-
created.push(gitignoreDest);
|
|
276
|
-
}
|
|
277
|
-
else if (plan.action === "append") {
|
|
278
|
-
fs.appendFileSync(gitignoreDest, plan.content);
|
|
279
|
-
updated.push(gitignoreDest);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
skipped.push(gitignoreDest);
|
|
283
|
-
}
|
|
237
|
+
const { created, updated, skipped, collectionName } = scaffoldVaultConfig(ctx.cwd, requestedCollection);
|
|
284
238
|
const msg = [
|
|
285
239
|
"Vault-mind scaffolding complete.",
|
|
286
240
|
"",
|
|
@@ -293,11 +247,12 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
293
247
|
if (skipped.length) {
|
|
294
248
|
msg.push("", "Skipped (already exist):", ...skipped.map((s) => ` • ${path.relative(ctx.cwd, s)}`));
|
|
295
249
|
}
|
|
296
|
-
msg.push("", `💡 Tip: default collection file is collections/${
|
|
250
|
+
msg.push("", `💡 Tip: default collection file is collections/${collectionName}.jsonl (override with /vm setup --collection <name>).`);
|
|
297
251
|
if (ctx.hasUI) {
|
|
298
252
|
ctx.ui.notify(msg.join("\n"), "info");
|
|
299
253
|
}
|
|
300
254
|
if (hasPiContextTools(pi)) {
|
|
255
|
+
const cfgDest = getConfigPath(ctx.cwd);
|
|
301
256
|
const existing = JSON.parse(fs.readFileSync(cfgDest, "utf-8"));
|
|
302
257
|
existing.extensionCompatibility = existing.extensionCompatibility || {};
|
|
303
258
|
existing.extensionCompatibility["pi-context"] = {
|
|
@@ -341,7 +296,7 @@ const handleValidate = async (_args, ctx, pi) => {
|
|
|
341
296
|
}
|
|
342
297
|
catch { }
|
|
343
298
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
344
|
-
lines.push(cfgPath ? `✅ Config: ${cfgPath}` : `⚠️ No config file (checked ${
|
|
299
|
+
lines.push(cfgPath ? `✅ Config: ${cfgPath}` : `⚠️ No config file (checked ${getConfigPath(ctx.cwd)}).`);
|
|
345
300
|
for (const [name, def] of Object.entries(cfg.collections)) {
|
|
346
301
|
const exists = fs.existsSync(def.path);
|
|
347
302
|
lines.push(`${exists ? "✅" : "⚠️"} Collection "${name}": ${def.path} ${exists ? "" : "(missing)"}`);
|
|
@@ -413,54 +368,105 @@ const handleApprove = async (args, ctx) => {
|
|
|
413
368
|
fs.writeFileSync(pending.path, rejected.join("\n") + (rejected.length > 0 ? "\n" : ""));
|
|
414
369
|
ctx.ui.notify(`Approved ${approved} → "${targetName}". Rejected ${rejected.length}.`, "info");
|
|
415
370
|
};
|
|
416
|
-
// ── /vm
|
|
417
|
-
const
|
|
418
|
-
const
|
|
371
|
+
// ── /vm search ─────────────────────────────────────────────────────────────
|
|
372
|
+
const handleSearch = async (args, ctx, defaultMode = "hybrid") => {
|
|
373
|
+
const modeMatch = args.match(/--mode=(\w+)/);
|
|
374
|
+
const mode = modeMatch ? modeMatch[1] : defaultMode;
|
|
375
|
+
const cleanArgs = args.replace(/--mode=\w+/, "").trim();
|
|
376
|
+
const parts = cleanArgs.split(/\s+/).filter(Boolean);
|
|
419
377
|
if (parts.length === 0) {
|
|
420
|
-
ctx.ui.notify("Usage: /vm
|
|
378
|
+
ctx.ui.notify("Usage: /vm search [collection] <query> [--mode=hybrid|semantic|fts|graph|full]", "error");
|
|
421
379
|
return;
|
|
422
380
|
}
|
|
423
381
|
const cfg = loadConfig(ctx.cwd);
|
|
424
382
|
let collection = getActiveCollection(ctx.cwd) || "main";
|
|
425
|
-
let query =
|
|
383
|
+
let query = cleanArgs;
|
|
426
384
|
if (parts.length > 1 && cfg.collections[parts[0]]) {
|
|
427
385
|
collection = parts[0];
|
|
428
386
|
query = parts.slice(1).join(" ");
|
|
429
387
|
}
|
|
430
388
|
const def = cfg.collections[collection];
|
|
431
389
|
if (!def) {
|
|
432
|
-
ctx.ui.notify(`Unknown collection "${collection}". Run /vm
|
|
390
|
+
ctx.ui.notify(`Unknown collection "${collection}". Run /vm setup first.`, "error");
|
|
433
391
|
return;
|
|
434
392
|
}
|
|
393
|
+
const limit = 20;
|
|
394
|
+
const dataDir = cfg.vaultMind.dataDir;
|
|
395
|
+
const vmCfg = cfg.vaultMind;
|
|
435
396
|
try {
|
|
436
|
-
|
|
397
|
+
let results = [];
|
|
398
|
+
switch (mode) {
|
|
399
|
+
case "hybrid":
|
|
400
|
+
results = await searchHybridRanked(dataDir, collection, query, limit, vmCfg);
|
|
401
|
+
break;
|
|
402
|
+
case "semantic":
|
|
403
|
+
results = await searchHybrid(dataDir, collection, query, limit, vmCfg);
|
|
404
|
+
break;
|
|
405
|
+
case "fts":
|
|
406
|
+
results = await searchFts(dataDir, collection, query, limit, vmCfg);
|
|
407
|
+
break;
|
|
408
|
+
case "graph":
|
|
409
|
+
results = [await queryGraph(dataDir, vmCfg, query, 1, undefined)];
|
|
410
|
+
break;
|
|
411
|
+
case "full": {
|
|
412
|
+
const [hybrid, semantic, fts, graph] = await Promise.all([
|
|
413
|
+
searchHybridRanked(dataDir, collection, query, limit, vmCfg).catch(() => []),
|
|
414
|
+
searchHybrid(dataDir, collection, query, limit, vmCfg).catch(() => []),
|
|
415
|
+
searchFts(dataDir, collection, query, limit, vmCfg).catch(() => []),
|
|
416
|
+
queryGraph(dataDir, vmCfg, query, 1, undefined).catch(() => []),
|
|
417
|
+
]);
|
|
418
|
+
const graphResult = Array.isArray(graph) ? graph : graph ? [graph] : [];
|
|
419
|
+
results = mergeSearchResults([hybrid, semantic, fts, graphResult]);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
default:
|
|
423
|
+
ctx.ui.notify(`Unknown search mode "${mode}". Use hybrid, semantic, fts, graph, or full.`, "error");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
437
426
|
if (results.length === 0) {
|
|
438
|
-
ctx.ui.notify(`No results for "${query}" in "${collection}".`, "info");
|
|
427
|
+
ctx.ui.notify(`No results for "${query}" in "${collection}" (mode: ${mode}).`, "info");
|
|
439
428
|
return;
|
|
440
429
|
}
|
|
441
430
|
// Rich TUI table when UI is available; plain text fallback for RPC
|
|
442
431
|
if (ctx.hasUI) {
|
|
443
|
-
await
|
|
432
|
+
await renderSearchResults(ctx, collection, query, results);
|
|
444
433
|
}
|
|
445
434
|
else {
|
|
446
435
|
const lines = [
|
|
447
|
-
`**Search results (${results.length}) for "${query}" in "${collection}":**`,
|
|
436
|
+
`**Search results (${results.length}) for "${query}" in "${collection}" (mode: ${mode}):**`,
|
|
448
437
|
"",
|
|
449
438
|
];
|
|
450
439
|
for (const r of results) {
|
|
451
440
|
const row = r;
|
|
452
|
-
const fact = String(row.fact ??
|
|
441
|
+
const fact = String(row.fact ??
|
|
442
|
+
row.text ??
|
|
443
|
+
(typeof row === "object" && row !== null ? JSON.stringify(row) : "(no text)"));
|
|
453
444
|
lines.push(`- ${fact.slice(0, 120)}${fact.length > 120 ? "..." : ""}`);
|
|
454
445
|
}
|
|
455
446
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
456
447
|
}
|
|
457
448
|
}
|
|
458
449
|
catch (err) {
|
|
459
|
-
ctx.ui.notify(`Search failed: ${err.message}`, "error");
|
|
450
|
+
ctx.ui.notify(`Search failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
460
451
|
}
|
|
461
452
|
};
|
|
453
|
+
function mergeSearchResults(sources) {
|
|
454
|
+
const seen = new Set();
|
|
455
|
+
const merged = [];
|
|
456
|
+
for (const source of sources) {
|
|
457
|
+
for (const r of source) {
|
|
458
|
+
const row = r;
|
|
459
|
+
const key = String(row.id ?? row.fact ?? JSON.stringify(row));
|
|
460
|
+
if (!seen.has(key)) {
|
|
461
|
+
seen.add(key);
|
|
462
|
+
merged.push(row);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return merged.slice(0, 20);
|
|
467
|
+
}
|
|
462
468
|
/** Render search results as a scrollable TUI table with column headers. */
|
|
463
|
-
async function
|
|
469
|
+
async function renderSearchResults(ctx, collection, query, results) {
|
|
464
470
|
// Infer columns from the first result
|
|
465
471
|
const first = results[0];
|
|
466
472
|
const keys = first ? Object.keys(first).filter((k) => !k.startsWith("_")) : [];
|
|
@@ -487,7 +493,7 @@ async function renderQueryResults(ctx, collection, query, results) {
|
|
|
487
493
|
divider,
|
|
488
494
|
...rows.map((r) => r.join(" ")),
|
|
489
495
|
];
|
|
490
|
-
await ctx.ui.custom((
|
|
496
|
+
await ctx.ui.custom((_tui, theme, _keybindings, done) => {
|
|
491
497
|
const container = new Container();
|
|
492
498
|
let y = 0;
|
|
493
499
|
container.addChild(new Text(theme.fg("accent", lines[0]), 1, ++y));
|
|
@@ -527,7 +533,7 @@ const handleTombstone = async (args, ctx) => {
|
|
|
527
533
|
ctx.ui.notify(result.message, result.success ? "info" : "error");
|
|
528
534
|
};
|
|
529
535
|
// ── /vm reindex ────────────────────────────────────────────────────────────
|
|
530
|
-
const handleReindex = async (args, ctx,
|
|
536
|
+
const handleReindex = async (args, ctx, _pi) => {
|
|
531
537
|
const cfg = loadConfig(ctx.cwd);
|
|
532
538
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
533
539
|
const flags = new Set(tokens.map((t) => t.toLowerCase()));
|
|
@@ -537,7 +543,7 @@ const handleReindex = async (args, ctx, pi) => {
|
|
|
537
543
|
const collectionFilter = reindexAll || rebuildEmbeddings ? null : (tokens.find((t) => !t.startsWith("--")) ?? null);
|
|
538
544
|
// Remote bulk re-index: read JSONL → submit Modal bulk job → poll → sync down.
|
|
539
545
|
if (remote) {
|
|
540
|
-
if (!cfg.vaultMind.embedding
|
|
546
|
+
if (!cfg.vaultMind.embedding[EMBEDDING_FLAT_KEYS[0]]) {
|
|
541
547
|
ctx.ui.notify("--remote requires remoteUrl. Set it up via /vm setup.", "error");
|
|
542
548
|
return;
|
|
543
549
|
}
|
|
@@ -552,7 +558,7 @@ const handleReindex = async (args, ctx, pi) => {
|
|
|
552
558
|
: Object.keys(cfg.collections);
|
|
553
559
|
ctx.ui.notify(`Remote re-index: submitting Modal bulk job for ${names.join(", ")}...`, "info");
|
|
554
560
|
try {
|
|
555
|
-
const results = await reindexRemote(cfg, names, {
|
|
561
|
+
const results = await reindexRemote(cfg, names, ctx.cwd, {
|
|
556
562
|
onStatus: (s) => ctx.ui.notify(` ${s.collection || ""}: ${s.status} ${s.processed}/${s.total}`, "info"),
|
|
557
563
|
});
|
|
558
564
|
const lines = ["**Remote Re-index Report:**", ""];
|
|
@@ -652,14 +658,14 @@ const handleReindex = async (args, ctx, pi) => {
|
|
|
652
658
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
653
659
|
};
|
|
654
660
|
// ── /vm embedding ──────────────────────────────────────────────────────────
|
|
655
|
-
const handleEmbedding = async (args, ctx,
|
|
661
|
+
const handleEmbedding = async (args, ctx, _pi) => {
|
|
656
662
|
const cfg = loadConfig(ctx.cwd);
|
|
657
663
|
const parts = args.trim().split(/\s+/g);
|
|
658
664
|
const subcommand = parts[0]?.toLowerCase() || "status";
|
|
659
|
-
const
|
|
665
|
+
const _value = parts.slice(1).join(" ");
|
|
660
666
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
661
667
|
if (!cfgPath) {
|
|
662
|
-
ctx.ui.notify("No config found. Run /vm
|
|
668
|
+
ctx.ui.notify("No config found. Run /vm setup first.", "error");
|
|
663
669
|
return;
|
|
664
670
|
}
|
|
665
671
|
switch (subcommand) {
|
|
@@ -677,9 +683,8 @@ const handleEmbedding = async (args, ctx, pi) => {
|
|
|
677
683
|
`Graph: ${cfg.vaultMind.graph?.enabled !== false ? "enabled" : "disabled"}`,
|
|
678
684
|
`Data Dir: ${cfg.vaultMind.dataDir}`,
|
|
679
685
|
];
|
|
680
|
-
if (emb.
|
|
681
|
-
|
|
682
|
-
lines.push("", "**Remote Provider (Modal):**", ` Workspace: ${modal.workspace || "(default)"}`, ` Fallback: ${modal.fallback?.enabled === false ? "disabled" : modal.fallback?.provider || "(none — degrade to FTS)"}`, ` Sync: auto=${modal.sync?.autoSync ? "on" : "off"}, interval=${modal.sync?.autoSyncIntervalMs ?? 300000}ms`);
|
|
686
|
+
if (emb.remoteUrl) {
|
|
687
|
+
lines.push("", "**Remote Provider (Modal):**", ` Workspace: ${emb.workspace || "(default)"}`, ` Fallback: ${emb.fallback?.enabled === false ? "disabled" : "(on — same-space local when unreachable)"}`, ` Sync: auto=${emb.sync?.autoSync ? "on" : "off"}, interval=${emb.sync?.autoSyncIntervalMs ?? 300000}ms`);
|
|
683
688
|
const co = emb.coalesce;
|
|
684
689
|
if (co) {
|
|
685
690
|
lines.push(` Coalesce: debounce=${co.debounceMs ?? 1000}ms, batch=${co.maxBatchSize ?? 64}, concurrency=${co.maxConcurrentFlushes ?? 2}, dedupe=${co.dedupe ?? true}, searchBypass=${co.searchBypass ?? true}`);
|
|
@@ -702,13 +707,13 @@ const MODAL_CONFIG_USAGE = [
|
|
|
702
707
|
" /vm remote config workspace <name> Derive the Modal URL from workspace slug",
|
|
703
708
|
" /vm remote config model <name> Set the canonical embedder (default embeddinggemma)",
|
|
704
709
|
" /vm remote config dim <n> Set output dimension (omit for native)",
|
|
705
|
-
" /vm remote config fallback ollama|none Set offline fallback provider",
|
|
710
|
+
" /vm remote config fallback <openai-compatible|ollama|none> Set offline fallback provider",
|
|
706
711
|
" /vm remote config sync auto on|off Toggle auto-sync",
|
|
707
712
|
" /vm remote config sync interval <ms> Auto-sync interval",
|
|
708
713
|
" /vm remote config pageSize <n> Sync page size",
|
|
709
714
|
" /vm remote config coalesce debounce <ms> Coalescer debounce window",
|
|
710
715
|
" /vm remote config coalesce batch <n> Coalescer max batch size",
|
|
711
|
-
" /vm remote config token Show token guidance (use
|
|
716
|
+
" /vm remote config token Show token guidance (use PVM_REMOTE_EMBEDDING_API_KEY env)",
|
|
712
717
|
"",
|
|
713
718
|
" (no args) Show current remote config",
|
|
714
719
|
].join("\n");
|
|
@@ -735,31 +740,31 @@ const modalSection = (obj) => {
|
|
|
735
740
|
const handleModalConfig = async (args, ctx) => {
|
|
736
741
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
737
742
|
if (!cfgPath) {
|
|
738
|
-
ctx.ui.notify("No config found. Run /vm
|
|
743
|
+
ctx.ui.notify("No config found. Run /vm setup first.", "error");
|
|
739
744
|
return;
|
|
740
745
|
}
|
|
741
746
|
const cfg = loadConfig(ctx.cwd);
|
|
742
747
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
743
748
|
const key = parts[0]?.toLowerCase();
|
|
744
|
-
const
|
|
749
|
+
const emb = cfg.vaultMind.embedding;
|
|
745
750
|
if (!key) {
|
|
746
751
|
const tokenSrc = resolveModalToken(cfg.vaultMind)
|
|
747
|
-
? "env or
|
|
748
|
-
:
|
|
749
|
-
? "config (set
|
|
750
|
-
: "❌ none (set
|
|
752
|
+
? "env or .vault-mind/vault-mind.env ✅"
|
|
753
|
+
: emb.remoteApiKey
|
|
754
|
+
? "config (set PVM_REMOTE_EMBEDDING_API_KEY env/dotenv to override)"
|
|
755
|
+
: "❌ none (set PVM_REMOTE_EMBEDDING_API_KEY env or .vault-mind/vault-mind.env)";
|
|
751
756
|
const derivedUrl = resolveBaseUrl(cfg.vaultMind);
|
|
752
757
|
const lines = [
|
|
753
758
|
"**Modal Config:**",
|
|
754
759
|
"",
|
|
755
|
-
` workspace: ${
|
|
756
|
-
` baseUrl: ${
|
|
757
|
-
` model: ${
|
|
758
|
-
` dim: ${
|
|
760
|
+
` workspace: ${emb.workspace || "(not set)"}`,
|
|
761
|
+
` baseUrl: ${emb.remoteUrl || (derivedUrl ? `${derivedUrl} (derived)` : "❌ not set")}`,
|
|
762
|
+
` model: ${emb.model || "(default embeddinggemma)"}`,
|
|
763
|
+
` dim: ${emb.dim ?? "(native)"}`,
|
|
759
764
|
` token: ${tokenSrc}`,
|
|
760
|
-
` fallback: ${
|
|
761
|
-
` sync: ${JSON.stringify(
|
|
762
|
-
` coalesce: ${JSON.stringify(
|
|
765
|
+
` fallback: ${emb.fallback?.enabled === false ? "disabled" : "(on — same-space local when unreachable)"}`,
|
|
766
|
+
` sync: ${JSON.stringify(emb.sync ?? {})}`,
|
|
767
|
+
` coalesce: ${JSON.stringify(emb.coalesce ?? {})}`,
|
|
763
768
|
];
|
|
764
769
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
765
770
|
return;
|
|
@@ -810,11 +815,13 @@ const handleModalConfig = async (args, ctx) => {
|
|
|
810
815
|
if (v === "none") {
|
|
811
816
|
m.fallback = { enabled: false };
|
|
812
817
|
}
|
|
813
|
-
else if (v === "
|
|
814
|
-
|
|
818
|
+
else if (v === "openai-compatible" || v === "ollama") {
|
|
819
|
+
// Accept "ollama" as a friendlier alias for "openai-compatible" —
|
|
820
|
+
// they're functionally identical (any local /v1/embeddings endpoint).
|
|
821
|
+
m.fallback = { enabled: true, provider: "openai-compatible" };
|
|
815
822
|
}
|
|
816
823
|
else {
|
|
817
|
-
ctx.ui.notify("/vm remote config fallback <ollama|
|
|
824
|
+
ctx.ui.notify("/vm remote config fallback <openai-compatible|ollama|none>", "error");
|
|
818
825
|
return;
|
|
819
826
|
}
|
|
820
827
|
writeProjectConfig(cfgPath, obj);
|
|
@@ -882,17 +889,17 @@ const handleModalConfig = async (args, ctx) => {
|
|
|
882
889
|
ctx.ui.notify(MODAL_CONFIG_USAGE, "info");
|
|
883
890
|
}
|
|
884
891
|
};
|
|
885
|
-
const handleModal = async (args, ctx,
|
|
892
|
+
const handleModal = async (args, ctx, _pi) => {
|
|
886
893
|
const cfg = loadConfig(ctx.cwd);
|
|
887
894
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
888
895
|
const sub = parts[0]?.toLowerCase() || "status";
|
|
889
896
|
const rest = parts.slice(1).join(" ");
|
|
890
897
|
switch (sub) {
|
|
891
898
|
case "status": {
|
|
892
|
-
const
|
|
899
|
+
const emb = cfg.vaultMind.embedding;
|
|
893
900
|
const tokenSrc = resolveModalToken(cfg.vaultMind)
|
|
894
|
-
? "env or
|
|
895
|
-
:
|
|
901
|
+
? "env or .vault-mind/vault-mind.env ✅"
|
|
902
|
+
: emb.remoteApiKey
|
|
896
903
|
? "config"
|
|
897
904
|
: "❌ none";
|
|
898
905
|
const derivedUrl = resolveBaseUrl(cfg.vaultMind);
|
|
@@ -901,9 +908,9 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
901
908
|
"",
|
|
902
909
|
`Configured: ${isModalConfigured(cfg.vaultMind) ? "✅" : "❌"}`,
|
|
903
910
|
` workspace: ${resolveWorkspace(cfg.vaultMind) || "(not set)"}`,
|
|
904
|
-
` baseUrl: ${
|
|
905
|
-
` model: ${
|
|
906
|
-
` dim: ${
|
|
911
|
+
` baseUrl: ${emb.remoteUrl || (derivedUrl ? `${derivedUrl} (derived)` : "(not set)")}`,
|
|
912
|
+
` model: ${emb.model || "(default embeddinggemma)"}`,
|
|
913
|
+
` dim: ${emb.dim ?? "(native)"}`,
|
|
907
914
|
` token: ${tokenSrc}`,
|
|
908
915
|
];
|
|
909
916
|
const client = createModalClient(cfg.vaultMind);
|
|
@@ -943,8 +950,8 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
943
950
|
ctx.ui.notify(`Syncing ${oneCollection ? `"${oneCollection}"` : "all collections"}${full ? " (full)" : ""}...`, "info");
|
|
944
951
|
try {
|
|
945
952
|
const results = oneCollection
|
|
946
|
-
? [await syncCollection(cfg, oneCollection, { full })]
|
|
947
|
-
: await syncAll(cfg, undefined, { full });
|
|
953
|
+
? [await syncCollection(cfg, oneCollection, ctx.cwd, { full })]
|
|
954
|
+
: await syncAll(cfg, ctx.cwd, undefined, { full });
|
|
948
955
|
const lines = ["**Sync Report:**", ""];
|
|
949
956
|
for (const r of results)
|
|
950
957
|
lines.push(` ${r.rows > 0 ? "✅" : "•"} ${r.collection} / ${r.model} / ${r.dim}: ${r.rows} rows, watermark=${r.watermark}${r.full ? " (full)" : ""}`);
|
|
@@ -986,16 +993,16 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
986
993
|
case "auto": {
|
|
987
994
|
const cfgPath = findConfig(ctx.cwd).project;
|
|
988
995
|
if (!cfgPath) {
|
|
989
|
-
ctx.ui.notify("No project config found. Run /vm
|
|
996
|
+
ctx.ui.notify("No project config found. Run /vm setup first.", "error");
|
|
990
997
|
return;
|
|
991
998
|
}
|
|
992
999
|
const workspace = resolveWorkspace(cfg.vaultMind);
|
|
993
|
-
if (!workspace && !cfg.vaultMind.embedding
|
|
1000
|
+
if (!workspace && !cfg.vaultMind.embedding[EMBEDDING_FLAT_KEYS[0]]) {
|
|
994
1001
|
ctx.ui.notify("Modal workspace or baseUrl not configured. Run:\n /vm remote config workspace <name>", "error");
|
|
995
1002
|
}
|
|
996
1003
|
const token = resolveModalToken(cfg.vaultMind);
|
|
997
1004
|
if (!token) {
|
|
998
|
-
ctx.ui.notify("Modal token not found. Set
|
|
1005
|
+
ctx.ui.notify("Modal token not found. Set PVM_REMOTE_EMBEDDING_API_KEY env or write it to .vault-mind/vault-mind.env", "error");
|
|
999
1006
|
return;
|
|
1000
1007
|
}
|
|
1001
1008
|
ctx.ui.notify("🔄 Auto-configuring Modal...", "info");
|
|
@@ -1082,7 +1089,7 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
1082
1089
|
ctx.ui.notify(`Canonical model → ${newModel}${newDim ? `@${newDim}` : ""} (was ${oldModel}${oldDim ? `@${oldDim}` : ""}). Old namespace kept. Starting remote re-embed...`, "info");
|
|
1083
1090
|
const fresh = loadConfig(ctx.cwd);
|
|
1084
1091
|
try {
|
|
1085
|
-
const results = await reindexRemote(fresh, Object.keys(fresh.collections));
|
|
1092
|
+
const results = await reindexRemote(fresh, Object.keys(fresh.collections), ctx.cwd);
|
|
1086
1093
|
const lines = [`**Migration → ${newModel}:**`, ""];
|
|
1087
1094
|
for (const r of results)
|
|
1088
1095
|
if (r.error)
|
|
@@ -1105,10 +1112,10 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
1105
1112
|
const handleContext = async (args, ctx, pi) => {
|
|
1106
1113
|
const parts = args.trim().split(/\s+/g);
|
|
1107
1114
|
const subcommand = parts[0]?.toLowerCase() || "status";
|
|
1108
|
-
const
|
|
1115
|
+
const _value = parts[1]?.toLowerCase();
|
|
1109
1116
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
1110
1117
|
if (!cfgPath) {
|
|
1111
|
-
ctx.ui.notify("No config found. Run /vm
|
|
1118
|
+
ctx.ui.notify("No config found. Run /vm setup first.", "error");
|
|
1112
1119
|
return;
|
|
1113
1120
|
}
|
|
1114
1121
|
switch (subcommand) {
|
|
@@ -1226,7 +1233,65 @@ export const watcherState = createWatcherState();
|
|
|
1226
1233
|
/** Module-level server state, referenced by the /vm server command.
|
|
1227
1234
|
* Port is set when the server starts in index.ts. */
|
|
1228
1235
|
export const serverState = createServerState();
|
|
1229
|
-
const handleServer = (ctx) => {
|
|
1236
|
+
const handleServer = (args, ctx, pi) => {
|
|
1237
|
+
const subcommand = args.trim().split(/\s+/g)[0]?.toLowerCase() || "status";
|
|
1238
|
+
if (subcommand === "start") {
|
|
1239
|
+
const cfg = loadConfig(ctx.cwd);
|
|
1240
|
+
serverState.port = cfg.vaultMind.httpPort || 11435;
|
|
1241
|
+
startServer(pi, serverState, watcherState, async () => {
|
|
1242
|
+
const registry = ctx.modelRegistry;
|
|
1243
|
+
await registry.refresh();
|
|
1244
|
+
const error = registry.getError();
|
|
1245
|
+
if (error)
|
|
1246
|
+
return { status: "model-registry-error" };
|
|
1247
|
+
const all = registry.getAll();
|
|
1248
|
+
if (all.length === 0)
|
|
1249
|
+
return { status: "model-registry-empty" };
|
|
1250
|
+
const available = registry.getAvailable();
|
|
1251
|
+
if (available.length === 0) {
|
|
1252
|
+
const providers = [...new Set(all.map((m) => m.provider))];
|
|
1253
|
+
const statuses = providers.map((p) => {
|
|
1254
|
+
const status = registry.getProviderAuthStatus(p);
|
|
1255
|
+
return {
|
|
1256
|
+
providerId: p,
|
|
1257
|
+
configured: status?.configured,
|
|
1258
|
+
source: status?.source,
|
|
1259
|
+
};
|
|
1260
|
+
});
|
|
1261
|
+
const configured = statuses.filter((s) => s.configured);
|
|
1262
|
+
if (configured.length === 0) {
|
|
1263
|
+
const firstSource = statuses[0]?.source;
|
|
1264
|
+
return {
|
|
1265
|
+
status: "provider-authentication-required",
|
|
1266
|
+
authSource: firstSource,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
const firstConfigured = configured[0];
|
|
1270
|
+
return {
|
|
1271
|
+
status: "configured-provider-unavailable",
|
|
1272
|
+
providerId: firstConfigured.providerId,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const currentModel = ctx.model;
|
|
1276
|
+
if (!currentModel) {
|
|
1277
|
+
if (available.length > 0)
|
|
1278
|
+
return { status: "model-selection-required" };
|
|
1279
|
+
}
|
|
1280
|
+
else if (!available.some((m) => m.id === currentModel.id && m.provider === currentModel.provider)) {
|
|
1281
|
+
return { status: "current-model-unavailable", modelId: currentModel.id };
|
|
1282
|
+
}
|
|
1283
|
+
return {
|
|
1284
|
+
status: "ready",
|
|
1285
|
+
model: {
|
|
1286
|
+
id: currentModel.id,
|
|
1287
|
+
name: currentModel.name,
|
|
1288
|
+
provider: currentModel.provider,
|
|
1289
|
+
},
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
ctx.ui.notify(`Started HTTP server on http://127.0.0.1:${serverState.port}/vault-mind/status`, "info");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1230
1295
|
const lines = [
|
|
1231
1296
|
`HTTP server: ${serverState.server ? "RUNNING" : "STOPPED"}`,
|
|
1232
1297
|
`Port: ${serverState.port}`,
|
|
@@ -1255,7 +1320,7 @@ const handleWatcher = async (args, ctx, pi) => {
|
|
|
1255
1320
|
case "start": {
|
|
1256
1321
|
const cfg = loadConfig(ctx.cwd);
|
|
1257
1322
|
if (!cfg.vaultMind.vaults || Object.keys(cfg.vaultMind.vaults).length === 0) {
|
|
1258
|
-
ctx.ui.notify("No vaults configured. Add vaults to vaultMind.vaults in
|
|
1323
|
+
ctx.ui.notify("No vaults configured. Add vaults to vaultMind.vaults in .vault-mind/vault-mind.config.json first.", "error");
|
|
1259
1324
|
return;
|
|
1260
1325
|
}
|
|
1261
1326
|
if (watcherState.running) {
|
|
@@ -1369,7 +1434,7 @@ const handleWatcher = async (args, ctx, pi) => {
|
|
|
1369
1434
|
const handleSetup = async (args, ctx, pi) => {
|
|
1370
1435
|
// Parse optional CLI-style args: --vault <path> --remoteUrl <url> --localUrl <url> --model <name> --workspace <name>
|
|
1371
1436
|
const cliArgs = {};
|
|
1372
|
-
const parts = args.trim().split(/\s+--/);
|
|
1437
|
+
const parts = args.trim().replace(/^--/, "").split(/\s+--/);
|
|
1373
1438
|
for (const part of parts) {
|
|
1374
1439
|
const trimmed = part.trim();
|
|
1375
1440
|
if (trimmed.startsWith("vault "))
|
|
@@ -1385,11 +1450,28 @@ const handleSetup = async (args, ctx, pi) => {
|
|
|
1385
1450
|
cliArgs.model = trimmed.slice(6).trim();
|
|
1386
1451
|
else if (trimmed.startsWith("workspace "))
|
|
1387
1452
|
cliArgs.workspace = trimmed.slice(10).trim();
|
|
1388
|
-
|
|
1389
|
-
|
|
1453
|
+
else if (trimmed.startsWith("remoteApiKey "))
|
|
1454
|
+
cliArgs.remoteApiKey = trimmed.slice(13).trim();
|
|
1455
|
+
else if (trimmed.startsWith("collection "))
|
|
1456
|
+
cliArgs.collection = trimmed.slice(11).trim();
|
|
1457
|
+
}
|
|
1458
|
+
const hasCliArgs = cliArgs.vault ||
|
|
1459
|
+
cliArgs.remoteUrl ||
|
|
1460
|
+
cliArgs.localUrl ||
|
|
1461
|
+
cliArgs.model ||
|
|
1462
|
+
cliArgs.workspace ||
|
|
1463
|
+
cliArgs.collection ||
|
|
1464
|
+
cliArgs.remoteApiKey;
|
|
1465
|
+
// Merge-safe scaffold FIRST: on a truly fresh vault this populates the
|
|
1466
|
+
// default collections/injectors (with vault-derived naming) before
|
|
1467
|
+
// setupWizard ever writes the file — setupWizard writing first would
|
|
1468
|
+
// permanently skip this branch (it always sees an existing file).
|
|
1469
|
+
await handleInit(cliArgs.collection ? `--collection ${cliArgs.collection}` : "", ctx, pi);
|
|
1390
1470
|
await setupWizard(ctx, hasCliArgs ? cliArgs : undefined);
|
|
1391
|
-
// Scaffold
|
|
1392
|
-
|
|
1471
|
+
// Scaffold model-router.json with tuned defaults if this vault doesn't
|
|
1472
|
+
// have one yet — mirrors what the Obsidian plugin does today, so a
|
|
1473
|
+
// CLI-only setup gets the same primary/fallback model choices.
|
|
1474
|
+
scaffoldModelRouterConfig(ctx.cwd);
|
|
1393
1475
|
if (ctx.hasUI) {
|
|
1394
1476
|
ctx.ui.notify("Running personalization (system.md + AGENTS.md)...", "info");
|
|
1395
1477
|
await runPersonalize(ctx, pi);
|
|
@@ -1399,7 +1481,7 @@ const handleSetup = async (args, ctx, pi) => {
|
|
|
1399
1481
|
}
|
|
1400
1482
|
};
|
|
1401
1483
|
// ── /vm intake ─────────────────────────────────────────────────────────────
|
|
1402
|
-
const
|
|
1484
|
+
const _handleIntake = async (args, ctx) => {
|
|
1403
1485
|
const filePath = args.trim();
|
|
1404
1486
|
if (!filePath) {
|
|
1405
1487
|
ctx.ui.notify("Usage: /vm intake <path-to-json-spec>", "error");
|
|
@@ -1430,15 +1512,8 @@ const handleIntake = async (args, ctx) => {
|
|
|
1430
1512
|
}
|
|
1431
1513
|
};
|
|
1432
1514
|
// ── /vm remote token ────────────────────────────────────────────────────────
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const writeTokenEnv = (token) => {
|
|
1436
|
-
const dir = path.dirname(MODAL_TOKEN_ENV_PATH);
|
|
1437
|
-
if (!fs.existsSync(dir))
|
|
1438
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1439
|
-
// fs mode bits are unsupported on Windows; skip them to avoid EINVAL.
|
|
1440
|
-
const options = process.platform === "win32" ? {} : { mode: 0o600 };
|
|
1441
|
-
fs.writeFileSync(MODAL_TOKEN_ENV_PATH, `PVM_API_TOKEN="${token}"\n`, options);
|
|
1515
|
+
export const writeTokenEnv = (token, cwd, varName = "PVM_API_TOKEN") => {
|
|
1516
|
+
writeEnvEntry(varName, token, { cwd });
|
|
1442
1517
|
};
|
|
1443
1518
|
const fetchTokenFrom1Password = () => {
|
|
1444
1519
|
try {
|
|
@@ -1453,32 +1528,32 @@ const fetchTokenFrom1Password = () => {
|
|
|
1453
1528
|
};
|
|
1454
1529
|
const handleModalToken = async (ctx, cliToken) => {
|
|
1455
1530
|
if (cliToken) {
|
|
1456
|
-
writeTokenEnv(cliToken);
|
|
1457
|
-
ctx.ui.notify(`✅ Wrote token to ${
|
|
1531
|
+
writeTokenEnv(cliToken, ctx.cwd, MODAL_TOKEN_ENV);
|
|
1532
|
+
ctx.ui.notify(`✅ Wrote token to ${modalTokenEnvPath(ctx.cwd)}`, "info");
|
|
1458
1533
|
return;
|
|
1459
1534
|
}
|
|
1460
1535
|
const envToken = process.env[MODAL_TOKEN_ENV];
|
|
1461
1536
|
if (envToken) {
|
|
1462
|
-
ctx.ui.notify("
|
|
1537
|
+
ctx.ui.notify("PVM_REMOTE_EMBEDDING_API_KEY is already exported in env. Skipping write so we don't clobber it.", "warning");
|
|
1463
1538
|
return;
|
|
1464
1539
|
}
|
|
1465
1540
|
const tokenFrom1Pass = fetchTokenFrom1Password();
|
|
1466
1541
|
if (tokenFrom1Pass) {
|
|
1467
|
-
writeTokenEnv(tokenFrom1Pass);
|
|
1468
|
-
ctx.ui.notify(`✅ Fetched token from 1Password item "pi-vault-mind-auth" and wrote it to ${
|
|
1542
|
+
writeTokenEnv(tokenFrom1Pass, ctx.cwd, MODAL_TOKEN_ENV);
|
|
1543
|
+
ctx.ui.notify(`✅ Fetched token from 1Password item "pi-vault-mind-auth" and wrote it to ${modalTokenEnvPath(ctx.cwd)}`, "info");
|
|
1469
1544
|
return;
|
|
1470
1545
|
}
|
|
1471
1546
|
if (!ctx.hasUI) {
|
|
1472
1547
|
ctx.ui.notify("No 1Password token found and no TUI available. Use:\n /vm remote token <token>", "error");
|
|
1473
1548
|
return;
|
|
1474
1549
|
}
|
|
1475
|
-
const token = await ctx.ui.input("Modal API token:", "paste bearer token here (will be written to
|
|
1550
|
+
const token = await ctx.ui.input("Modal API token:", "paste bearer token here (will be written to .vault-mind/vault-mind.env as PVM_REMOTE_EMBEDDING_API_KEY)");
|
|
1476
1551
|
if (!token) {
|
|
1477
1552
|
ctx.ui.notify("Token setup cancelled.", "warning");
|
|
1478
1553
|
return;
|
|
1479
1554
|
}
|
|
1480
|
-
writeTokenEnv(token);
|
|
1481
|
-
ctx.ui.notify(`✅ Wrote token to ${
|
|
1555
|
+
writeTokenEnv(token, ctx.cwd, MODAL_TOKEN_ENV);
|
|
1556
|
+
ctx.ui.notify(`✅ Wrote token to ${modalTokenEnvPath(ctx.cwd)}`, "info");
|
|
1482
1557
|
};
|
|
1483
1558
|
// ── /vm token ──────────────────────────────────────────────────────────────
|
|
1484
1559
|
const maskToken = (token) => {
|
|
@@ -1500,15 +1575,15 @@ const handleToken = (args, ctx) => {
|
|
|
1500
1575
|
else {
|
|
1501
1576
|
source = "none";
|
|
1502
1577
|
try {
|
|
1503
|
-
const text = fs.readFileSync(
|
|
1578
|
+
const text = fs.readFileSync(modalTokenEnvPath(ctx.cwd), "utf-8");
|
|
1504
1579
|
for (const line of text.split(/\r?\n/)) {
|
|
1505
1580
|
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s#]+)/);
|
|
1506
|
-
if (
|
|
1581
|
+
if (m?.[1] !== "PVM_API_TOKEN")
|
|
1507
1582
|
continue;
|
|
1508
1583
|
const val = m[3] ?? m[4] ?? m[2];
|
|
1509
1584
|
if (val) {
|
|
1510
1585
|
token = val;
|
|
1511
|
-
source = "
|
|
1586
|
+
source = ".vault-mind/vault-mind.env";
|
|
1512
1587
|
break;
|
|
1513
1588
|
}
|
|
1514
1589
|
}
|
|
@@ -1516,14 +1591,6 @@ const handleToken = (args, ctx) => {
|
|
|
1516
1591
|
catch {
|
|
1517
1592
|
// file missing or unreadable
|
|
1518
1593
|
}
|
|
1519
|
-
if (!token) {
|
|
1520
|
-
const cfg = loadConfig(ctx.cwd);
|
|
1521
|
-
const cfgToken = cfg?.vaultMind?.embedding?.modal?.apiToken;
|
|
1522
|
-
if (cfgToken) {
|
|
1523
|
-
token = cfgToken;
|
|
1524
|
-
source = "config (vaultMind.embedding.modal.apiToken)";
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
1594
|
}
|
|
1528
1595
|
const masked = token ? maskToken(token) : "(not set)";
|
|
1529
1596
|
ctx.ui.notify(["**API Token**", `Source: ${source}`, `Value: ${masked}`].join("\n"), token ? "info" : "warning");
|
|
@@ -1537,7 +1604,7 @@ const handleToken = (args, ctx) => {
|
|
|
1537
1604
|
: "";
|
|
1538
1605
|
ctx.ui.notify([
|
|
1539
1606
|
`\u2705 New token: ${masked}`,
|
|
1540
|
-
"Written to
|
|
1607
|
+
"Written to .vault-mind/vault-mind.env.",
|
|
1541
1608
|
`\u26a0\ufe0f All clients (Obsidian plugin, scripts) must update to the new token — the old token is now invalid.${envNote}`,
|
|
1542
1609
|
].join("\n"), "warning");
|
|
1543
1610
|
return;
|
|
@@ -1568,15 +1635,15 @@ export const registerCommands = (pi) => {
|
|
|
1568
1635
|
getArgumentCompletions: (_prefix) => {
|
|
1569
1636
|
const words = _prefix.trim().split(/\s+/);
|
|
1570
1637
|
const top = [
|
|
1571
|
-
"init",
|
|
1572
1638
|
"validate",
|
|
1573
1639
|
"approve",
|
|
1574
|
-
"
|
|
1640
|
+
"search",
|
|
1575
1641
|
"tombstone",
|
|
1576
1642
|
"settings",
|
|
1577
1643
|
"help",
|
|
1578
1644
|
"collection",
|
|
1579
1645
|
"discover-schema",
|
|
1646
|
+
"doctor",
|
|
1580
1647
|
"injector",
|
|
1581
1648
|
"context",
|
|
1582
1649
|
"embedding",
|
|
@@ -1597,11 +1664,6 @@ export const registerCommands = (pi) => {
|
|
|
1597
1664
|
const subcommand = words[0];
|
|
1598
1665
|
if (words.length >= 2) {
|
|
1599
1666
|
const prefix = words[1] || "";
|
|
1600
|
-
if (subcommand === "init") {
|
|
1601
|
-
return ["--collection"]
|
|
1602
|
-
.filter((c) => c.startsWith(prefix))
|
|
1603
|
-
.map((c) => ({ label: c, value: c, description: `init ${c}` }));
|
|
1604
|
-
}
|
|
1605
1667
|
if (subcommand === "embedding") {
|
|
1606
1668
|
return ["status", "use", "model", "models", "pull", "cloud"]
|
|
1607
1669
|
.filter((c) => c.startsWith(prefix))
|
|
@@ -1617,11 +1679,11 @@ export const registerCommands = (pi) => {
|
|
|
1617
1679
|
.filter((c) => c.startsWith(prefix))
|
|
1618
1680
|
.map((c) => ({ label: c, value: c, description: `discover-schema ${c}` }));
|
|
1619
1681
|
}
|
|
1620
|
-
if (subcommand === "
|
|
1682
|
+
if (subcommand === "search") {
|
|
1621
1683
|
const names = collectionNames(loadConfig(process.cwd()));
|
|
1622
1684
|
return names
|
|
1623
1685
|
.filter((c) => c.startsWith(prefix))
|
|
1624
|
-
.map((c) => ({ label: c, value: c, description: `
|
|
1686
|
+
.map((c) => ({ label: c, value: c, description: `search ${c}` }));
|
|
1625
1687
|
}
|
|
1626
1688
|
if (subcommand === "collection") {
|
|
1627
1689
|
return ["select", "list", "create", "reindex"]
|
|
@@ -1639,7 +1701,15 @@ export const registerCommands = (pi) => {
|
|
|
1639
1701
|
.map((c) => ({ label: c, value: c, description: `watcher ${c}` }));
|
|
1640
1702
|
}
|
|
1641
1703
|
if (subcommand === "setup") {
|
|
1642
|
-
return [
|
|
1704
|
+
return [
|
|
1705
|
+
"--vault",
|
|
1706
|
+
"--remoteUrl",
|
|
1707
|
+
"--localUrl",
|
|
1708
|
+
"--model",
|
|
1709
|
+
"--workspace",
|
|
1710
|
+
"--collection",
|
|
1711
|
+
"--remoteApiKey",
|
|
1712
|
+
]
|
|
1643
1713
|
.filter((c) => c.startsWith(prefix))
|
|
1644
1714
|
.map((c) => ({ label: c, value: c, description: `setup ${c}` }));
|
|
1645
1715
|
}
|
|
@@ -1661,14 +1731,14 @@ export const registerCommands = (pi) => {
|
|
|
1661
1731
|
const subcommand = parts[0]?.toLowerCase() || "help";
|
|
1662
1732
|
const rest = parts.slice(1).join(" ");
|
|
1663
1733
|
switch (subcommand) {
|
|
1664
|
-
case "
|
|
1665
|
-
return
|
|
1734
|
+
case "doctor":
|
|
1735
|
+
return handleDoctor(ctx);
|
|
1666
1736
|
case "validate":
|
|
1667
1737
|
return handleValidate(rest, ctx, pi);
|
|
1668
1738
|
case "approve":
|
|
1669
1739
|
return handleApprove(rest, ctx);
|
|
1670
|
-
case "
|
|
1671
|
-
return
|
|
1740
|
+
case "search":
|
|
1741
|
+
return handleSearch(rest, ctx);
|
|
1672
1742
|
case "tombstone":
|
|
1673
1743
|
return handleTombstone(rest, ctx);
|
|
1674
1744
|
case "settings":
|
|
@@ -1684,7 +1754,7 @@ export const registerCommands = (pi) => {
|
|
|
1684
1754
|
case "embedding":
|
|
1685
1755
|
return handleEmbedding(rest, ctx, pi);
|
|
1686
1756
|
case "server":
|
|
1687
|
-
return handleServer(ctx);
|
|
1757
|
+
return handleServer(rest, ctx, pi);
|
|
1688
1758
|
case "watcher":
|
|
1689
1759
|
return handleWatcher(rest, ctx, pi);
|
|
1690
1760
|
case "remote":
|