pi-vault-mind 0.8.7 → 0.9.0
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/dist/src/commands.js +18 -5
- package/dist/src/server.js +35 -14
- package/dist/src/settings-ui.d.ts +8 -0
- package/dist/src/settings-ui.js +289 -38
- package/dist/src/utils.d.ts +3 -0
- package/dist/src/utils.js +19 -0
- package/dist/test/rest-setup.test.js +17 -3
- package/dist/test/settings-ui.test.js +22 -1
- package/package.json +1 -1
package/dist/src/commands.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Container, SelectList, Text } from "@earendil-works/pi-tui";
|
|
|
5
5
|
import * as lancedb from "@lancedb/lancedb";
|
|
6
6
|
import { getActiveCollection, setActiveCollection } from "./state.js";
|
|
7
7
|
import { DEFAULT_CONFIG } from "./types.js";
|
|
8
|
-
import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, } from "./utils.js";
|
|
8
|
+
import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, resolveInitCollectionName, } from "./utils.js";
|
|
9
9
|
import { updateActiveCollectionWidget } from "./widget.js";
|
|
10
10
|
import { cancelJobHelper, listJobs, retryJob, } from "./agent-queue.js";
|
|
11
11
|
import { revokeToken, rotateToken } from "./auth.js";
|
|
@@ -23,8 +23,7 @@ import { createWatcherState, getWatcherStatus, startWatcher, stopWatcher } from
|
|
|
23
23
|
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
24
24
|
const VM_USAGE = [
|
|
25
25
|
"**pi-vault-mind Commands**",
|
|
26
|
-
"",
|
|
27
|
-
" /vm init Scaffold config + collections",
|
|
26
|
+
" /vm init [--collection <name>] Scaffold config + collections (default collection path uses vault name)",
|
|
28
27
|
" /vm validate Check LanceDB, config, and collection health",
|
|
29
28
|
" /vm validate --audit Audit config for missing defaults",
|
|
30
29
|
" /vm approve [collection] Batch-review pending entries",
|
|
@@ -200,6 +199,15 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
200
199
|
const created = [];
|
|
201
200
|
const updated = [];
|
|
202
201
|
const skipped = [];
|
|
202
|
+
const initTokens = _args.trim().split(/\s+/).filter(Boolean);
|
|
203
|
+
let requestedCollection;
|
|
204
|
+
for (let i = 0; i < initTokens.length; i++) {
|
|
205
|
+
if (initTokens[i] === "--collection") {
|
|
206
|
+
requestedCollection = initTokens[i + 1];
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const initCollection = resolveInitCollectionName(ctx.cwd, requestedCollection);
|
|
203
211
|
const ensureFile = (dest, tmpl) => {
|
|
204
212
|
if (fs.existsSync(dest)) {
|
|
205
213
|
skipped.push(dest);
|
|
@@ -216,7 +224,7 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
216
224
|
version: 2,
|
|
217
225
|
collections: {
|
|
218
226
|
main: {
|
|
219
|
-
path:
|
|
227
|
+
path: `collections/${initCollection}.jsonl`,
|
|
220
228
|
schema: ["id", "domain", "source", "fact", "tag", "artifact"],
|
|
221
229
|
dedupField: "fact",
|
|
222
230
|
},
|
|
@@ -284,7 +292,7 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
284
292
|
if (skipped.length) {
|
|
285
293
|
msg.push("", "Skipped (already exist):", ...skipped.map((s) => ` • ${path.relative(ctx.cwd, s)}`));
|
|
286
294
|
}
|
|
287
|
-
msg.push("",
|
|
295
|
+
msg.push("", `💡 Tip: default collection file is collections/${initCollection}.jsonl (override with /vm init --collection <name>).`);
|
|
288
296
|
if (ctx.hasUI) {
|
|
289
297
|
ctx.ui.notify(msg.join("\n"), "info");
|
|
290
298
|
}
|
|
@@ -1584,6 +1592,11 @@ export const registerCommands = (pi) => {
|
|
|
1584
1592
|
const subcommand = words[0];
|
|
1585
1593
|
if (words.length >= 2) {
|
|
1586
1594
|
const prefix = words[1] || "";
|
|
1595
|
+
if (subcommand === "init") {
|
|
1596
|
+
return ["--collection"]
|
|
1597
|
+
.filter((c) => c.startsWith(prefix))
|
|
1598
|
+
.map((c) => ({ label: c, value: c, description: `init ${c}` }));
|
|
1599
|
+
}
|
|
1587
1600
|
if (subcommand === "embedding") {
|
|
1588
1601
|
return ["status", "use", "model", "models", "pull", "cloud"]
|
|
1589
1602
|
.filter((c) => c.startsWith(prefix))
|
package/dist/src/server.js
CHANGED
|
@@ -30,7 +30,7 @@ import { authoriseRequest, resolveAuthToken } from "./auth.js";
|
|
|
30
30
|
import { GITIGNORE_ENTRIES } from "./commands.js";
|
|
31
31
|
import { searchHybrid, upsertEntry } from "./lance.js";
|
|
32
32
|
import { DEFAULT_CONFIG } from "./types.js";
|
|
33
|
-
import {
|
|
33
|
+
import { ensureDir, expandHome, findConfig, loadConfig, resolveInitCollectionName, shrinkHome, } from "./utils.js";
|
|
34
34
|
import { processQueue, scanFile, startWatcher, stopWatcher } from "./watcher.js";
|
|
35
35
|
export function createServerState(port = 11435) {
|
|
36
36
|
return { server: null, wss: null, port, startTime: 0 };
|
|
@@ -493,22 +493,43 @@ function handleVmInit(req, res) {
|
|
|
493
493
|
res.end(JSON.stringify({ error: `Vault path does not exist: ${vaultPath}` }));
|
|
494
494
|
return;
|
|
495
495
|
}
|
|
496
|
+
const initCollection = resolveInitCollectionName(vaultPath, parsed.collection);
|
|
496
497
|
// Scaffold project config
|
|
497
498
|
const cfgPath = path.join(vaultPath, "pi-vault-mind.config.json");
|
|
498
499
|
if (!fs.existsSync(cfgPath)) {
|
|
499
|
-
const tmpl =
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
500
|
+
const tmpl = {
|
|
501
|
+
version: 2,
|
|
502
|
+
collections: {
|
|
503
|
+
main: {
|
|
504
|
+
path: `collections/${initCollection}.jsonl`,
|
|
505
|
+
schema: ["id", "domain", "source", "fact", "tag", "artifact"],
|
|
506
|
+
dedupField: "fact",
|
|
507
|
+
},
|
|
508
|
+
pending: { path: "collections/pending.jsonl", schema: "main" },
|
|
509
|
+
context_events: {
|
|
510
|
+
path: "collections/context_events.jsonl",
|
|
511
|
+
schema: ["id", "type", "session_entry_id", "content", "timestamp", "tags"],
|
|
512
|
+
dedupField: "id",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
injectors: [
|
|
516
|
+
{
|
|
517
|
+
name: "draft-context",
|
|
518
|
+
regex: "draft\\s+(\\S+)",
|
|
519
|
+
collection: "main",
|
|
520
|
+
filterField: "tag",
|
|
521
|
+
artifactPath: "collections/artifact.md",
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
vaultMind: DEFAULT_CONFIG.vaultMind,
|
|
525
|
+
};
|
|
526
|
+
fs.writeFileSync(cfgPath, `${JSON.stringify(tmpl, null, 2)}\n`, "utf-8");
|
|
527
|
+
}
|
|
528
|
+
// Scaffold collections dir from config
|
|
529
|
+
const projectCfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
530
|
+
for (const def of Object.values(projectCfg.collections || {})) {
|
|
531
|
+
if (!def.path)
|
|
532
|
+
continue;
|
|
512
533
|
const p = path.join(vaultPath, def.path);
|
|
513
534
|
if (!fs.existsSync(p)) {
|
|
514
535
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface ObsidianPluginRequirement {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const getMissingOnboardingPlugins: (vaultPath: string) => ObsidianPluginRequirement[];
|
|
9
|
+
export declare const buildPluginInstallGuidance: (plugins: ObsidianPluginRequirement[]) => string;
|
|
2
10
|
export declare const detectVaultFromCwd: (cwd: string) => string | null;
|
|
3
11
|
export declare const createCollectionWizard: (ctx: ExtensionContext) => Promise<void>;
|
|
4
12
|
export declare const createInjectorWizard: (ctx: ExtensionContext) => Promise<void>;
|
package/dist/src/settings-ui.js
CHANGED
|
@@ -1,7 +1,158 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { MODAL_TOKEN_ENV, createModalClient, modalUrl, resolveModalToken } from "./modal-config.js";
|
|
4
5
|
import { collectionNames, findConfig, getGlobalConfigPath, loadConfig, shrinkHome, } from "./utils.js";
|
|
6
|
+
const ONBOARDING_PLUGIN_REQUIREMENTS = [
|
|
7
|
+
{
|
|
8
|
+
id: "obsidian42-brat",
|
|
9
|
+
label: "BRAT (Beta Reviewer's Auto-update Tester)",
|
|
10
|
+
required: true,
|
|
11
|
+
reason: "required to install obsidian-pi-vault-mind (not yet in community registry)",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "obsidian-pi-vault-mind",
|
|
15
|
+
label: "Vault Mind plugin",
|
|
16
|
+
required: true,
|
|
17
|
+
reason: "native setup/status/chat UI and local bridge controls",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "actions-uri",
|
|
21
|
+
label: "Actions URI",
|
|
22
|
+
required: false,
|
|
23
|
+
reason: "deep-link automation and capture shortcuts",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "obsidian-git",
|
|
27
|
+
label: "Obsidian Git",
|
|
28
|
+
required: false,
|
|
29
|
+
reason: "vault backup and change history safety net",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
const readEnabledCommunityPlugins = (vaultPath) => {
|
|
33
|
+
const communityPluginsPath = path.join(vaultPath, ".obsidian", "community-plugins.json");
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(communityPluginsPath, "utf-8"));
|
|
36
|
+
if (!Array.isArray(parsed))
|
|
37
|
+
return new Set();
|
|
38
|
+
return new Set(parsed.filter((value) => typeof value === "string"));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return new Set();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export const getMissingOnboardingPlugins = (vaultPath) => {
|
|
45
|
+
const enabled = readEnabledCommunityPlugins(vaultPath);
|
|
46
|
+
return ONBOARDING_PLUGIN_REQUIREMENTS.filter((plugin) => !enabled.has(plugin.id));
|
|
47
|
+
};
|
|
48
|
+
const hasObsidianCli = () => {
|
|
49
|
+
try {
|
|
50
|
+
execFileSync("obsidian", ["help"], {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const formatExecError = (err) => {
|
|
61
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
62
|
+
const stderr = err.stderr;
|
|
63
|
+
if (typeof stderr === "string" && stderr.trim())
|
|
64
|
+
return stderr.trim();
|
|
65
|
+
if (stderr instanceof Buffer && stderr.length > 0)
|
|
66
|
+
return stderr.toString("utf-8").trim();
|
|
67
|
+
}
|
|
68
|
+
return err instanceof Error ? err.message : String(err);
|
|
69
|
+
};
|
|
70
|
+
const BRAT_REPO = "kylebrodeur/obsidian-pi-vault-mind";
|
|
71
|
+
const installViaBrat = () => {
|
|
72
|
+
try {
|
|
73
|
+
execFileSync("obsidian", ["eval", `app.plugins.getPlugin("obsidian42-brat").addPlugin("${BRAT_REPO}")`], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return { ok: false, error: formatExecError(err) };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const installObsidianPlugin = (pluginId) => {
|
|
81
|
+
// vault-mind plugin must be installed via BRAT (not in community registry)
|
|
82
|
+
if (pluginId === "obsidian-pi-vault-mind") {
|
|
83
|
+
return installViaBrat();
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
execFileSync("obsidian", ["plugin:install", `id=${pluginId}`, "enable"], {
|
|
87
|
+
encoding: "utf-8",
|
|
88
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
+
});
|
|
90
|
+
return { ok: true };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return { ok: false, error: formatExecError(err) };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
export const buildPluginInstallGuidance = (plugins) => {
|
|
97
|
+
const bratPlugins = plugins.filter((p) => p.id === "obsidian-pi-vault-mind");
|
|
98
|
+
const registryPlugins = plugins.filter((p) => p.id !== "obsidian-pi-vault-mind");
|
|
99
|
+
const lines = ["Install missing Obsidian plugins:", ""];
|
|
100
|
+
if (registryPlugins.length > 0) {
|
|
101
|
+
lines.push("CLI (when Obsidian CLI is available):", ...registryPlugins.map((plugin) => ` obsidian plugin:install id=${plugin.id} enable`), "", "Deep links (open each URI):", ...registryPlugins.map((plugin) => ` obsidian://show-plugin?id=${encodeURIComponent(plugin.id)}`), "", "GUI fallback: Obsidian → Settings → Community plugins → Browse");
|
|
102
|
+
}
|
|
103
|
+
if (bratPlugins.length > 0) {
|
|
104
|
+
lines.push("", "Via BRAT (not yet in community registry):", ` Settings → BRAT → "Add Beta plugin" → ${BRAT_REPO}`, ` Or CLI: obsidian eval 'app.plugins.getPlugin("obsidian42-brat").addPlugin("${BRAT_REPO}")'`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
};
|
|
108
|
+
const runPluginOnboarding = async (ctx, vaultPath) => {
|
|
109
|
+
const missingPlugins = getMissingOnboardingPlugins(vaultPath);
|
|
110
|
+
if (missingPlugins.length === 0) {
|
|
111
|
+
ctx.ui.notify("Obsidian plugin check: required integration plugins already enabled.", "info");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
ctx.ui.notify([
|
|
115
|
+
"Missing Obsidian plugins detected:",
|
|
116
|
+
...missingPlugins.map((plugin) => `- ${plugin.label} (${plugin.id})${plugin.required ? " [required]" : " [recommended]"} — ${plugin.reason}`),
|
|
117
|
+
].join("\n"), "warning");
|
|
118
|
+
const cliAvailable = hasObsidianCli();
|
|
119
|
+
const options = cliAvailable
|
|
120
|
+
? [
|
|
121
|
+
"Install missing plugins now (Obsidian CLI)",
|
|
122
|
+
"I'll install manually (show commands + URIs)",
|
|
123
|
+
"Skip plugin setup for now",
|
|
124
|
+
]
|
|
125
|
+
: ["I'll install manually (show commands + URIs)", "Skip plugin setup for now"];
|
|
126
|
+
const choice = await ctx.ui.select("Obsidian plugin onboarding", options);
|
|
127
|
+
if (!choice || choice === "Skip plugin setup for now")
|
|
128
|
+
return;
|
|
129
|
+
if (choice === "Install missing plugins now (Obsidian CLI)") {
|
|
130
|
+
const installed = [];
|
|
131
|
+
const failed = [];
|
|
132
|
+
for (const plugin of missingPlugins) {
|
|
133
|
+
const result = installObsidianPlugin(plugin.id);
|
|
134
|
+
if (result.ok) {
|
|
135
|
+
installed.push(plugin.id);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
failed.push({ plugin, error: result.error || "Unknown error" });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (installed.length > 0) {
|
|
142
|
+
ctx.ui.notify([`Installed plugins (${installed.length}):`, ...installed.map((id) => `- ${id}`)].join("\n"), "info");
|
|
143
|
+
}
|
|
144
|
+
if (failed.length > 0) {
|
|
145
|
+
ctx.ui.notify([
|
|
146
|
+
"Some plugin installs failed:",
|
|
147
|
+
...failed.map(({ plugin, error }) => `- ${plugin.id}: ${error}`),
|
|
148
|
+
"",
|
|
149
|
+
buildPluginInstallGuidance(failed.map(({ plugin }) => plugin)),
|
|
150
|
+
].join("\n"), "warning");
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
ctx.ui.notify(buildPluginInstallGuidance(missingPlugins), "info");
|
|
155
|
+
};
|
|
5
156
|
export const detectVaultFromCwd = (cwd) => {
|
|
6
157
|
const obsidianDir = path.join(cwd, ".obsidian");
|
|
7
158
|
try {
|
|
@@ -91,6 +242,60 @@ export const createInjectorWizard = async (ctx) => {
|
|
|
91
242
|
fs.writeFileSync(cfgPath, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
|
|
92
243
|
ctx.ui.notify(`✅ Injector "${name}" created and configured!`, "info");
|
|
93
244
|
};
|
|
245
|
+
/** Probe Ollama at localhost for available embedding models. */
|
|
246
|
+
const probeOllamaModels = async () => {
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch("http://127.0.0.1:11434/api/tags", {
|
|
249
|
+
signal: AbortSignal.timeout(3000),
|
|
250
|
+
});
|
|
251
|
+
if (!res.ok)
|
|
252
|
+
return [];
|
|
253
|
+
const data = (await res.json());
|
|
254
|
+
if (!Array.isArray(data.models))
|
|
255
|
+
return [];
|
|
256
|
+
// Filter to embedding-capable models (heuristic: name contains "embed" or known embedding models)
|
|
257
|
+
const embeddingHints = ["embed", "minilm", "nomic", "bge", "gte", "e5", "mxbai"];
|
|
258
|
+
const all = data.models
|
|
259
|
+
.filter((m) => embeddingHints.some((h) => m.name.toLowerCase().includes(h)))
|
|
260
|
+
.map((m) => ({
|
|
261
|
+
name: m.name,
|
|
262
|
+
size: m.size > 1e9 ? `${(m.size / 1e9).toFixed(1)}GB` : `${(m.size / 1e6).toFixed(0)}MB`,
|
|
263
|
+
}));
|
|
264
|
+
// Also include all models as secondary options (user might know what they want)
|
|
265
|
+
const nonEmbedding = data.models
|
|
266
|
+
.filter((m) => !embeddingHints.some((h) => m.name.toLowerCase().includes(h)))
|
|
267
|
+
.map((m) => ({
|
|
268
|
+
name: m.name,
|
|
269
|
+
size: m.size > 1e9 ? `${(m.size / 1e9).toFixed(1)}GB` : `${(m.size / 1e6).toFixed(0)}MB`,
|
|
270
|
+
}));
|
|
271
|
+
return [...all, ...nonEmbedding];
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
/** Probe a Modal deployment for available embedding models via GET /models. */
|
|
278
|
+
const probeModalModels = async (baseUrl, cfg) => {
|
|
279
|
+
try {
|
|
280
|
+
const token = resolveModalToken(cfg);
|
|
281
|
+
const headers = {};
|
|
282
|
+
if (token)
|
|
283
|
+
headers.Authorization = `Bearer ${token}`;
|
|
284
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
|
|
285
|
+
headers,
|
|
286
|
+
signal: AbortSignal.timeout(8000),
|
|
287
|
+
});
|
|
288
|
+
if (!res.ok)
|
|
289
|
+
return [];
|
|
290
|
+
const data = (await res.json());
|
|
291
|
+
if (!Array.isArray(data.models))
|
|
292
|
+
return [];
|
|
293
|
+
return data.models.filter((m) => m.enabled);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
};
|
|
94
299
|
export const setupWizard = async (ctx, cliArgs) => {
|
|
95
300
|
const existingGlobal = fs.existsSync(getGlobalConfigPath());
|
|
96
301
|
const detectedVaultPath = detectVaultFromCwd(ctx.cwd);
|
|
@@ -142,6 +347,21 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
142
347
|
lines.push(` Dim: ${cliArgs.dim}`);
|
|
143
348
|
if (cliArgs.workspace)
|
|
144
349
|
lines.push(` Modal workspace: ${cliArgs.workspace}`);
|
|
350
|
+
if (effectiveVaultPath) {
|
|
351
|
+
const obsidianDir = path.join(effectiveVaultPath, ".obsidian");
|
|
352
|
+
if (fs.existsSync(obsidianDir)) {
|
|
353
|
+
const missingPlugins = getMissingOnboardingPlugins(effectiveVaultPath);
|
|
354
|
+
if (missingPlugins.length === 0) {
|
|
355
|
+
lines.push("", "Obsidian plugin check: required integration plugins already enabled.");
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
lines.push("", "Missing Obsidian plugins:", ...missingPlugins.map((plugin) => `- ${plugin.label} (${plugin.id})`), "", buildPluginInstallGuidance(missingPlugins));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
lines.push("", "Obsidian plugin check skipped: .obsidian/ not found.", "Open the vault in Obsidian once, then re-run /vm setup for plugin guidance.");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
145
365
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
146
366
|
return;
|
|
147
367
|
}
|
|
@@ -186,55 +406,86 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
186
406
|
return;
|
|
187
407
|
}
|
|
188
408
|
}
|
|
409
|
+
// ── Step 1.5: Obsidian plugin readiness ────────────────────────────────
|
|
410
|
+
await runPluginOnboarding(ctx, vaultPath);
|
|
189
411
|
// ── Step 2: Embedding config ──────────────────────────────────────────
|
|
190
|
-
let remoteUrl =
|
|
191
|
-
|
|
412
|
+
let remoteUrl = "";
|
|
413
|
+
let localUrl = "";
|
|
192
414
|
let useTransformers = false;
|
|
193
415
|
let guidedModalWorkspace;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
])
|
|
200
|
-
|
|
201
|
-
|
|
416
|
+
let model = "";
|
|
417
|
+
let dim;
|
|
418
|
+
// Probe Ollama for local embedding models
|
|
419
|
+
const ollamaModels = await probeOllamaModels();
|
|
420
|
+
const providerOptions = [
|
|
421
|
+
...(ollamaModels.length > 0 ? ["Local (Ollama — models detected)"] : []),
|
|
422
|
+
"Cloud (Modal workspace)",
|
|
423
|
+
"Custom URL (any /v1/embeddings endpoint)",
|
|
424
|
+
"Offline (transformers.js — no external service)",
|
|
425
|
+
];
|
|
426
|
+
const provider = await ctx.ui.select("Embedding provider:", providerOptions);
|
|
427
|
+
if (!provider) {
|
|
428
|
+
ctx.ui.notify("Setup cancelled.", "warning");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (provider.startsWith("Local")) {
|
|
432
|
+
localUrl = "http://127.0.0.1:11434/v1";
|
|
433
|
+
// Let user pick from detected Ollama embedding models
|
|
434
|
+
const modelChoice = await ctx.ui.select("Select embedding model:", ollamaModels.map((m) => `${m.name} (${m.size})`));
|
|
435
|
+
if (!modelChoice) {
|
|
436
|
+
ctx.ui.notify("Setup cancelled.", "warning");
|
|
202
437
|
return;
|
|
203
438
|
}
|
|
204
|
-
|
|
205
|
-
|
|
439
|
+
model = modelChoice.replace(/\s*\(.*\)$/, "");
|
|
440
|
+
}
|
|
441
|
+
else if (provider.startsWith("Cloud")) {
|
|
442
|
+
const workspaceSlug = await ctx.ui.input("Modal workspace slug:", "");
|
|
443
|
+
if (!workspaceSlug) {
|
|
444
|
+
ctx.ui.notify("Setup cancelled — workspace is required.", "warning");
|
|
445
|
+
return;
|
|
206
446
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
447
|
+
guidedModalWorkspace = workspaceSlug.trim();
|
|
448
|
+
remoteUrl = modalUrl(guidedModalWorkspace);
|
|
449
|
+
// Probe Modal /models for available embedders
|
|
450
|
+
const modalModels = await probeModalModels(remoteUrl, loadConfig(ctx.cwd).vaultMind);
|
|
451
|
+
if (modalModels.length > 0) {
|
|
452
|
+
const modalChoice = await ctx.ui.select("Select cloud embedding model:", modalModels.map((m) => `${m.key} (dim ${m.native_dim}${m.gated ? ", gated" : ""})`));
|
|
453
|
+
if (modalChoice) {
|
|
454
|
+
model = modalChoice.replace(/\s*\(.*\)$/, "");
|
|
455
|
+
const matched = modalModels.find((m) => m.key === model);
|
|
456
|
+
if (matched)
|
|
457
|
+
dim = matched.native_dim;
|
|
212
458
|
}
|
|
213
|
-
guidedModalWorkspace = workspaceSlug.trim();
|
|
214
|
-
remoteUrl = modalUrl(guidedModalWorkspace);
|
|
215
|
-
ctx.ui.notify([
|
|
216
|
-
"Modal endpoint discovery:",
|
|
217
|
-
`- Derived remote URL: ${remoteUrl}`,
|
|
218
|
-
"- Set token now with one of:",
|
|
219
|
-
" /vm remote token",
|
|
220
|
-
" ./scripts/fetch-modal-token.sh --write",
|
|
221
|
-
"- Reference docs:",
|
|
222
|
-
" docs/getting-started/NEW_VAULT_WALKTHROUGH.md",
|
|
223
|
-
" docs/integrations/MODAL_EMBEDDING.md",
|
|
224
|
-
].join("\n"), "info");
|
|
225
459
|
}
|
|
460
|
+
else {
|
|
461
|
+
ctx.ui.notify("Could not reach Modal /models — using default (embeddinggemma @ 768).", "warning");
|
|
462
|
+
model = "embeddinggemma";
|
|
463
|
+
dim = 768;
|
|
464
|
+
}
|
|
465
|
+
ctx.ui.notify([
|
|
466
|
+
"Modal endpoint discovery:",
|
|
467
|
+
`- Remote URL: ${remoteUrl}`,
|
|
468
|
+
"- Set token with: /vm remote token or ./scripts/fetch-modal-token.sh --write",
|
|
469
|
+
].join("\n"), "info");
|
|
226
470
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
471
|
+
else if (provider.startsWith("Custom")) {
|
|
472
|
+
const url = await ctx.ui.input("Embedding URL (e.g. https://my-server.com/v1):", "");
|
|
473
|
+
if (!url) {
|
|
474
|
+
ctx.ui.notify("Setup cancelled — URL required.", "warning");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
remoteUrl = url;
|
|
478
|
+
model = (await ctx.ui.input("Model name (optional):", ""))?.trim() || "";
|
|
479
|
+
const dimStr = await ctx.ui.input("Dimensions (optional):", "");
|
|
480
|
+
dim = dimStr ? Number.parseInt(dimStr, 10) : undefined;
|
|
231
481
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
482
|
+
else {
|
|
483
|
+
// Offline transformers
|
|
484
|
+
useTransformers = true;
|
|
485
|
+
model = "Xenova/all-MiniLM-L6-v2";
|
|
486
|
+
dim = 384;
|
|
237
487
|
}
|
|
488
|
+
const workspace = guidedModalWorkspace;
|
|
238
489
|
// ── Step 3: Deterministic runtime settings ──────────────────────────────
|
|
239
490
|
const enableContextAutomation = await ctx.ui.confirm("Context automation", "Enable pi-context integration with auto-/acm trigger and event indexing?");
|
|
240
491
|
const enableAutoStart = await ctx.ui.confirm("Vault auto-start", "Auto-start watcher/server for this vault when pi session starts?");
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare const hasPiContextTools: (pi: ExtensionAPI) => boolean;
|
|
|
21
21
|
export declare const isPiContextEnabled: (cfg: UniversalConfig) => boolean;
|
|
22
22
|
export declare const getPiContextConfig: (cfg: UniversalConfig) => PiContextDef;
|
|
23
23
|
export declare const collectionNames: (cfg: UniversalConfig) => string[];
|
|
24
|
+
export declare const normalizeCollectionName: (value: string) => string;
|
|
25
|
+
export declare const deriveCollectionNameFromVaultPath: (vaultPath: string) => string;
|
|
26
|
+
export declare const resolveInitCollectionName: (vaultPath: string, override?: string) => string;
|
|
24
27
|
/** Resolve a configured vault folder, falling back to the default layout. */
|
|
25
28
|
export declare function resolveVaultFolder(cfg: VaultMindConfig, key: "inbox" | "library" | "presentations" | "journal"): string;
|
|
26
29
|
/** Resolve the configured default vault path, or null if none is configured. */
|
package/dist/src/utils.js
CHANGED
|
@@ -132,6 +132,25 @@ export const getPiContextConfig = (cfg) => ({
|
|
|
132
132
|
});
|
|
133
133
|
// Backward compat alias
|
|
134
134
|
export const collectionNames = (cfg) => Object.keys(cfg.collections);
|
|
135
|
+
export const normalizeCollectionName = (value) => value
|
|
136
|
+
.trim()
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
139
|
+
.replace(/^_+|_+$/g, "")
|
|
140
|
+
.slice(0, 64);
|
|
141
|
+
export const deriveCollectionNameFromVaultPath = (vaultPath) => {
|
|
142
|
+
const base = path.basename(path.resolve(vaultPath));
|
|
143
|
+
const normalized = normalizeCollectionName(base);
|
|
144
|
+
return normalized || "main";
|
|
145
|
+
};
|
|
146
|
+
export const resolveInitCollectionName = (vaultPath, override) => {
|
|
147
|
+
if (override && override.trim().length > 0) {
|
|
148
|
+
const normalized = normalizeCollectionName(override);
|
|
149
|
+
if (normalized)
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
return deriveCollectionNameFromVaultPath(vaultPath);
|
|
153
|
+
};
|
|
135
154
|
/** Default vault folder layout. Paths are relative to the vault root. */
|
|
136
155
|
const DEFAULT_VAULT_FOLDERS = {
|
|
137
156
|
inbox: "Agent/Inbox",
|
|
@@ -114,16 +114,30 @@ describe("REST setup routes", () => {
|
|
|
114
114
|
const { status } = await fetchJson("127.0.0.1", port, "GET", "/vm/status");
|
|
115
115
|
assert.equal(status, 401);
|
|
116
116
|
});
|
|
117
|
-
it("POST /vm/init scaffolds config and
|
|
117
|
+
it("POST /vm/init scaffolds config and vault-named collection file", async () => {
|
|
118
118
|
const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath }, "test-token");
|
|
119
119
|
assert.equal(status, 200);
|
|
120
120
|
const b = body;
|
|
121
121
|
assert.equal(b.ok, true);
|
|
122
122
|
assert.equal(b.path, vaultPath);
|
|
123
|
-
|
|
124
|
-
assert.ok(fs.existsSync(
|
|
123
|
+
const configPath = path.join(vaultPath, "pi-vault-mind.config.json");
|
|
124
|
+
assert.ok(fs.existsSync(configPath));
|
|
125
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
126
|
+
const mainPath = String(cfg.collections?.main?.path || "");
|
|
127
|
+
assert.notEqual(mainPath, "collections/main.jsonl");
|
|
128
|
+
assert.match(mainPath, /^collections\/.+\.jsonl$/);
|
|
129
|
+
assert.ok(fs.existsSync(path.join(vaultPath, mainPath)));
|
|
125
130
|
assert.ok(fs.existsSync(path.join(vaultPath, ".gitignore")));
|
|
126
131
|
});
|
|
132
|
+
it("POST /vm/init accepts collection override", async () => {
|
|
133
|
+
const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath, collection: "returnvape" }, "test-token");
|
|
134
|
+
assert.equal(status, 200);
|
|
135
|
+
const b = body;
|
|
136
|
+
assert.equal(b.ok, true);
|
|
137
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(vaultPath, "pi-vault-mind.config.json"), "utf-8"));
|
|
138
|
+
assert.equal(cfg.collections?.main?.path, "collections/returnvape.jsonl");
|
|
139
|
+
assert.ok(fs.existsSync(path.join(vaultPath, "collections", "returnvape.jsonl")));
|
|
140
|
+
});
|
|
127
141
|
it("POST /vm/init is idempotent", async () => {
|
|
128
142
|
await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath }, "test-token");
|
|
129
143
|
const { status, body } = await fetchJson("127.0.0.1", port, "POST", "/vm/init", { vaultPath }, "test-token");
|
|
@@ -3,8 +3,8 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { afterEach, describe, it } from "node:test";
|
|
6
|
-
import { detectVaultFromCwd, setupWizard } from "../src/settings-ui.js";
|
|
7
6
|
import { modalUrl } from "../src/modal-config.js";
|
|
7
|
+
import { buildPluginInstallGuidance, detectVaultFromCwd, getMissingOnboardingPlugins, setupWizard, } from "../src/settings-ui.js";
|
|
8
8
|
import { getGlobalConfigPath } from "../src/utils.js";
|
|
9
9
|
const savedHome = process.env.HOME;
|
|
10
10
|
const mkTmpDir = (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
@@ -37,6 +37,27 @@ describe("settings-ui vault detection", () => {
|
|
|
37
37
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
|
+
describe("setupWizard plugin onboarding helpers", () => {
|
|
41
|
+
it("reports only missing community plugins", () => {
|
|
42
|
+
const vault = mkTmpDir("pvm-plugin-check-");
|
|
43
|
+
const obsidianDir = path.join(vault, ".obsidian");
|
|
44
|
+
fs.mkdirSync(obsidianDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(path.join(obsidianDir, "community-plugins.json"), JSON.stringify(["obsidian-pi-vault-mind"], null, 2), "utf-8");
|
|
46
|
+
const missing = getMissingOnboardingPlugins(vault).map((plugin) => plugin.id);
|
|
47
|
+
assert.equal(missing.includes("obsidian-pi-vault-mind"), false);
|
|
48
|
+
assert.equal(missing.includes("actions-uri"), true);
|
|
49
|
+
assert.equal(missing.includes("obsidian-git"), true);
|
|
50
|
+
fs.rmSync(vault, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
it("builds manual guidance with CLI commands and Obsidian URIs", () => {
|
|
53
|
+
const vault = mkTmpDir("pvm-plugin-guide-");
|
|
54
|
+
const guidance = buildPluginInstallGuidance(getMissingOnboardingPlugins(vault));
|
|
55
|
+
assert.match(guidance, /obsidian plugin:install id=obsidian42-brat enable/);
|
|
56
|
+
assert.match(guidance, /obsidian plugin:install id=actions-uri enable/);
|
|
57
|
+
assert.match(guidance, /BRAT.*kylebrodeur\/obsidian-pi-vault-mind/);
|
|
58
|
+
fs.rmSync(vault, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
40
61
|
describe("setupWizard CLI defaults", () => {
|
|
41
62
|
it("auto-uses cwd as vault when cwd is an Obsidian vault", async () => {
|
|
42
63
|
const home = mkTmpDir("pvm-home-");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vault-mind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Passive Obsidian vault extension for pi. Watches @agent markers, dispatches forked subagents (Miner, Broadcaster, Heavy-Lifter), stores in LanceDB with vector + FTS + graph. Multi-agent 'Drop & Forget' workflow for the pi agent ecosystem.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|