pi-vault-mind 0.8.8 → 0.9.1
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 +27 -8
- package/dist/src/index.js +6 -1
- package/dist/src/server.js +35 -14
- package/dist/src/settings-ui.js +204 -67
- 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 +3 -2
- package/package.json +2 -1
- package/pi-vault-mind.config.example.json +43 -0
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",
|
|
@@ -34,7 +33,8 @@ const VM_USAGE = [
|
|
|
34
33
|
" /vm collection select Select active collection",
|
|
35
34
|
" /vm collection list List configured collections",
|
|
36
35
|
" /vm collection create Create a new collection (wizard)",
|
|
37
|
-
" /vm collection reindex Rebuild FTS + vector indexes
|
|
36
|
+
" /vm collection reindex Rebuild FTS + vector indexes",
|
|
37
|
+
" /vm index [--all] [--reembed] [--remote] Index collections (alias: reindex)",
|
|
38
38
|
" /vm discover-schema Infer schema from a JSONL file",
|
|
39
39
|
" /vm injector create Create a new injector (wizard)",
|
|
40
40
|
" /vm context enable|disable Enable/disable pi-context integration",
|
|
@@ -200,6 +200,15 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
200
200
|
const created = [];
|
|
201
201
|
const updated = [];
|
|
202
202
|
const skipped = [];
|
|
203
|
+
const initTokens = _args.trim().split(/\s+/).filter(Boolean);
|
|
204
|
+
let requestedCollection;
|
|
205
|
+
for (let i = 0; i < initTokens.length; i++) {
|
|
206
|
+
if (initTokens[i] === "--collection") {
|
|
207
|
+
requestedCollection = initTokens[i + 1];
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const initCollection = resolveInitCollectionName(ctx.cwd, requestedCollection);
|
|
203
212
|
const ensureFile = (dest, tmpl) => {
|
|
204
213
|
if (fs.existsSync(dest)) {
|
|
205
214
|
skipped.push(dest);
|
|
@@ -216,7 +225,7 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
216
225
|
version: 2,
|
|
217
226
|
collections: {
|
|
218
227
|
main: {
|
|
219
|
-
path:
|
|
228
|
+
path: `collections/${initCollection}.jsonl`,
|
|
220
229
|
schema: ["id", "domain", "source", "fact", "tag", "artifact"],
|
|
221
230
|
dedupField: "fact",
|
|
222
231
|
},
|
|
@@ -284,7 +293,7 @@ const handleInit = async (_args, ctx, pi) => {
|
|
|
284
293
|
if (skipped.length) {
|
|
285
294
|
msg.push("", "Skipped (already exist):", ...skipped.map((s) => ` • ${path.relative(ctx.cwd, s)}`));
|
|
286
295
|
}
|
|
287
|
-
msg.push("",
|
|
296
|
+
msg.push("", `💡 Tip: default collection file is collections/${initCollection}.jsonl (override with /vm init --collection <name>).`);
|
|
288
297
|
if (ctx.hasUI) {
|
|
289
298
|
ctx.ui.notify(msg.join("\n"), "info");
|
|
290
299
|
}
|
|
@@ -639,7 +648,7 @@ const handleReindex = async (args, ctx, pi) => {
|
|
|
639
648
|
lines.unshift("**Reindex Report:**", "");
|
|
640
649
|
lines.push("", rebuildEmbeddings
|
|
641
650
|
? "Entries will be re-embedded with current embedding model on next append."
|
|
642
|
-
: "Use /vm
|
|
651
|
+
: "Use /vm index --reembed to regenerate embeddings after model switch.");
|
|
643
652
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
644
653
|
};
|
|
645
654
|
// ── /vm embedding ──────────────────────────────────────────────────────────
|
|
@@ -1553,7 +1562,7 @@ const handleToken = (args, ctx) => {
|
|
|
1553
1562
|
// ── Main /vm command ───────────────────────────────────────────────────────
|
|
1554
1563
|
export const registerCommands = (pi) => {
|
|
1555
1564
|
pi.registerCommand("vm", {
|
|
1556
|
-
description: "pi-vault-mind: manage collections, embedding,
|
|
1565
|
+
description: "pi-vault-mind: manage collections, embedding, indexing, and config.",
|
|
1557
1566
|
getArgumentCompletions: (_prefix) => {
|
|
1558
1567
|
const words = _prefix.trim().split(/\s+/);
|
|
1559
1568
|
const top = [
|
|
@@ -1574,6 +1583,8 @@ export const registerCommands = (pi) => {
|
|
|
1574
1583
|
"personalize",
|
|
1575
1584
|
"setup",
|
|
1576
1585
|
"token",
|
|
1586
|
+
"index",
|
|
1587
|
+
"reindex",
|
|
1577
1588
|
];
|
|
1578
1589
|
if (words.length === 1 || (words.length === 2 && words[1] === "")) {
|
|
1579
1590
|
return top
|
|
@@ -1584,6 +1595,11 @@ export const registerCommands = (pi) => {
|
|
|
1584
1595
|
const subcommand = words[0];
|
|
1585
1596
|
if (words.length >= 2) {
|
|
1586
1597
|
const prefix = words[1] || "";
|
|
1598
|
+
if (subcommand === "init") {
|
|
1599
|
+
return ["--collection"]
|
|
1600
|
+
.filter((c) => c.startsWith(prefix))
|
|
1601
|
+
.map((c) => ({ label: c, value: c, description: `init ${c}` }));
|
|
1602
|
+
}
|
|
1587
1603
|
if (subcommand === "embedding") {
|
|
1588
1604
|
return ["status", "use", "model", "models", "pull", "cloud"]
|
|
1589
1605
|
.filter((c) => c.startsWith(prefix))
|
|
@@ -1677,6 +1693,9 @@ export const registerCommands = (pi) => {
|
|
|
1677
1693
|
return handleSetup(rest, ctx, pi);
|
|
1678
1694
|
case "token":
|
|
1679
1695
|
return handleToken(rest, ctx);
|
|
1696
|
+
case "index":
|
|
1697
|
+
case "reindex":
|
|
1698
|
+
return handleReindex(rest, ctx, pi);
|
|
1680
1699
|
default:
|
|
1681
1700
|
ctx.ui.notify(VM_USAGE, "info");
|
|
1682
1701
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -37,7 +37,12 @@ export default function (pi) {
|
|
|
37
37
|
* during extension loading — the runtime must be initialized first. */
|
|
38
38
|
pi.on("session_start", async (_event, ctx) => {
|
|
39
39
|
updateActiveCollectionWidget(ctx);
|
|
40
|
-
|
|
40
|
+
/* Only enable ACM when this session has a vault configured */
|
|
41
|
+
const sessionCfg = loadConfig(ctx.cwd);
|
|
42
|
+
const piCtxCfg = sessionCfg.extensionCompatibility?.["pi-context"];
|
|
43
|
+
if (piCtxCfg?.enabled && piCtxCfg?.autoEnableAcm !== false) {
|
|
44
|
+
enableACM(pi);
|
|
45
|
+
}
|
|
41
46
|
/* register shortcut */
|
|
42
47
|
pi.registerShortcut("ctrl+alt+l", {
|
|
43
48
|
description: "Select Active Collection",
|
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 });
|
package/dist/src/settings-ui.js
CHANGED
|
@@ -4,6 +4,12 @@ import * as path from "node:path";
|
|
|
4
4
|
import { MODAL_TOKEN_ENV, createModalClient, modalUrl, resolveModalToken } from "./modal-config.js";
|
|
5
5
|
import { collectionNames, findConfig, getGlobalConfigPath, loadConfig, shrinkHome, } from "./utils.js";
|
|
6
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
|
+
},
|
|
7
13
|
{
|
|
8
14
|
id: "obsidian-pi-vault-mind",
|
|
9
15
|
label: "Vault Mind plugin",
|
|
@@ -61,7 +67,21 @@ const formatExecError = (err) => {
|
|
|
61
67
|
}
|
|
62
68
|
return err instanceof Error ? err.message : String(err);
|
|
63
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
|
+
};
|
|
64
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
|
+
}
|
|
65
85
|
try {
|
|
66
86
|
execFileSync("obsidian", ["plugin:install", `id=${pluginId}`, "enable"], {
|
|
67
87
|
encoding: "utf-8",
|
|
@@ -74,17 +94,15 @@ const installObsidianPlugin = (pluginId) => {
|
|
|
74
94
|
}
|
|
75
95
|
};
|
|
76
96
|
export const buildPluginInstallGuidance = (plugins) => {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
...
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"GUI fallback: Obsidian → Settings → Community plugins → Browse",
|
|
87
|
-
];
|
|
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 code='await app.plugins.getPlugin("obsidian42-brat").addPlugin("${BRAT_REPO}")'`);
|
|
105
|
+
}
|
|
88
106
|
return lines.join("\n");
|
|
89
107
|
};
|
|
90
108
|
const runPluginOnboarding = async (ctx, vaultPath) => {
|
|
@@ -224,6 +242,60 @@ export const createInjectorWizard = async (ctx) => {
|
|
|
224
242
|
fs.writeFileSync(cfgPath, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
|
|
225
243
|
ctx.ui.notify(`✅ Injector "${name}" created and configured!`, "info");
|
|
226
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
|
+
};
|
|
227
299
|
export const setupWizard = async (ctx, cliArgs) => {
|
|
228
300
|
const existingGlobal = fs.existsSync(getGlobalConfigPath());
|
|
229
301
|
const detectedVaultPath = detectVaultFromCwd(ctx.cwd);
|
|
@@ -336,55 +408,119 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
336
408
|
}
|
|
337
409
|
// ── Step 1.5: Obsidian plugin readiness ────────────────────────────────
|
|
338
410
|
await runPluginOnboarding(ctx, vaultPath);
|
|
411
|
+
// ── Step 1.6: Pi skills ────────────────────────────────────────────────
|
|
412
|
+
const installSkills = await ctx.ui.confirm("Pi skills", "Install recommended Obsidian skills for agents? (obsidian-markdown, obsidian-bases, json-canvas, obsidian-cli, defuddle)");
|
|
413
|
+
if (installSkills) {
|
|
414
|
+
try {
|
|
415
|
+
execFileSync("npx", [
|
|
416
|
+
"-y",
|
|
417
|
+
"skills",
|
|
418
|
+
"add",
|
|
419
|
+
"https://github.com/kepano/obsidian-skills",
|
|
420
|
+
"--skill",
|
|
421
|
+
"obsidian-markdown",
|
|
422
|
+
"--skill",
|
|
423
|
+
"obsidian-bases",
|
|
424
|
+
"--skill",
|
|
425
|
+
"json-canvas",
|
|
426
|
+
"--skill",
|
|
427
|
+
"obsidian-cli",
|
|
428
|
+
"--skill",
|
|
429
|
+
"defuddle",
|
|
430
|
+
"-g",
|
|
431
|
+
"-a",
|
|
432
|
+
"pi",
|
|
433
|
+
"--copy",
|
|
434
|
+
"-y",
|
|
435
|
+
], {
|
|
436
|
+
encoding: "utf-8",
|
|
437
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
438
|
+
env: { ...process.env, DISABLE_TELEMETRY: "1" },
|
|
439
|
+
});
|
|
440
|
+
ctx.ui.notify("✅ Pi skills installed.", "info");
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
ctx.ui.notify(`⚠️ Skill install failed: ${formatExecError(err)}\nInstall manually: see INSTALL.md §Layer 2`, "warning");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
339
446
|
// ── Step 2: Embedding config ──────────────────────────────────────────
|
|
340
|
-
let remoteUrl =
|
|
341
|
-
|
|
447
|
+
let remoteUrl = "";
|
|
448
|
+
let localUrl = "";
|
|
342
449
|
let useTransformers = false;
|
|
343
450
|
let guidedModalWorkspace;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
])
|
|
350
|
-
|
|
351
|
-
|
|
451
|
+
let model = "";
|
|
452
|
+
let dim;
|
|
453
|
+
// Probe Ollama for local embedding models
|
|
454
|
+
const ollamaModels = await probeOllamaModels();
|
|
455
|
+
const providerOptions = [
|
|
456
|
+
...(ollamaModels.length > 0 ? ["Local (Ollama — models detected)"] : []),
|
|
457
|
+
"Cloud (Modal workspace)",
|
|
458
|
+
"Custom URL (any /v1/embeddings endpoint)",
|
|
459
|
+
"Offline (transformers.js — no external service)",
|
|
460
|
+
];
|
|
461
|
+
const provider = await ctx.ui.select("Embedding provider:", providerOptions);
|
|
462
|
+
if (!provider) {
|
|
463
|
+
ctx.ui.notify("Setup cancelled.", "warning");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (provider.startsWith("Local")) {
|
|
467
|
+
localUrl = "http://127.0.0.1:11434/v1";
|
|
468
|
+
// Let user pick from detected Ollama embedding models
|
|
469
|
+
const modelChoice = await ctx.ui.select("Select embedding model:", ollamaModels.map((m) => `${m.name} (${m.size})`));
|
|
470
|
+
if (!modelChoice) {
|
|
471
|
+
ctx.ui.notify("Setup cancelled.", "warning");
|
|
352
472
|
return;
|
|
353
473
|
}
|
|
354
|
-
|
|
355
|
-
|
|
474
|
+
model = modelChoice.replace(/\s*\(.*\)$/, "");
|
|
475
|
+
}
|
|
476
|
+
else if (provider.startsWith("Cloud")) {
|
|
477
|
+
const workspaceSlug = await ctx.ui.input("Modal workspace slug:", "");
|
|
478
|
+
if (!workspaceSlug) {
|
|
479
|
+
ctx.ui.notify("Setup cancelled — workspace is required.", "warning");
|
|
480
|
+
return;
|
|
356
481
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
482
|
+
guidedModalWorkspace = workspaceSlug.trim();
|
|
483
|
+
remoteUrl = modalUrl(guidedModalWorkspace);
|
|
484
|
+
// Probe Modal /models for available embedders
|
|
485
|
+
const modalModels = await probeModalModels(remoteUrl, loadConfig(ctx.cwd).vaultMind);
|
|
486
|
+
if (modalModels.length > 0) {
|
|
487
|
+
const modalChoice = await ctx.ui.select("Select cloud embedding model:", modalModels.map((m) => `${m.key} (dim ${m.native_dim}${m.gated ? ", gated" : ""})`));
|
|
488
|
+
if (modalChoice) {
|
|
489
|
+
model = modalChoice.replace(/\s*\(.*\)$/, "");
|
|
490
|
+
const matched = modalModels.find((m) => m.key === model);
|
|
491
|
+
if (matched)
|
|
492
|
+
dim = matched.native_dim;
|
|
362
493
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
ctx.ui.notify(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
].join("\n"), "info");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
const model = (await ctx.ui.input("Embedding model key (optional):", ""))?.trim();
|
|
378
|
-
if (model === undefined) {
|
|
379
|
-
ctx.ui.notify("Setup cancelled.", "warning");
|
|
380
|
-
return;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
ctx.ui.notify("Could not reach Modal /models — using default (embeddinggemma @ 768).", "warning");
|
|
497
|
+
model = "embeddinggemma";
|
|
498
|
+
dim = 768;
|
|
499
|
+
}
|
|
500
|
+
ctx.ui.notify([
|
|
501
|
+
"Modal endpoint discovery:",
|
|
502
|
+
`- Remote URL: ${remoteUrl}`,
|
|
503
|
+
"- Set token with: /vm remote token or ./scripts/fetch-modal-token.sh --write",
|
|
504
|
+
].join("\n"), "info");
|
|
381
505
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
506
|
+
else if (provider.startsWith("Custom")) {
|
|
507
|
+
const url = await ctx.ui.input("Embedding URL (e.g. https://my-server.com/v1):", "");
|
|
508
|
+
if (!url) {
|
|
509
|
+
ctx.ui.notify("Setup cancelled — URL required.", "warning");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
remoteUrl = url;
|
|
513
|
+
model = (await ctx.ui.input("Model name (optional):", ""))?.trim() || "";
|
|
514
|
+
const dimStr = await ctx.ui.input("Dimensions (optional):", "");
|
|
515
|
+
dim = dimStr ? Number.parseInt(dimStr, 10) : undefined;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
// Offline transformers
|
|
519
|
+
useTransformers = true;
|
|
520
|
+
model = "Xenova/all-MiniLM-L6-v2";
|
|
521
|
+
dim = 384;
|
|
387
522
|
}
|
|
523
|
+
const workspace = guidedModalWorkspace;
|
|
388
524
|
// ── Step 3: Deterministic runtime settings ──────────────────────────────
|
|
389
525
|
const enableContextAutomation = await ctx.ui.confirm("Context automation", "Enable pi-context integration with auto-/acm trigger and event indexing?");
|
|
390
526
|
const enableAutoStart = await ctx.ui.confirm("Vault auto-start", "Auto-start watcher/server for this vault when pi session starts?");
|
|
@@ -428,22 +564,30 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
428
564
|
};
|
|
429
565
|
config.vaultMind.graph = config.vaultMind.graph || { enabled: true, canvasSync: true };
|
|
430
566
|
config.vaultMind.ftsEnabled = config.vaultMind.ftsEnabled !== false;
|
|
431
|
-
|
|
432
|
-
|
|
567
|
+
const dir = path.dirname(getGlobalConfigPath());
|
|
568
|
+
if (!fs.existsSync(dir))
|
|
569
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
570
|
+
fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
571
|
+
// Write extension compatibility to the PROJECT config (not global)
|
|
572
|
+
// so pi-context settings are scoped to this vault, not every session.
|
|
573
|
+
const projectCfgPath = path.join(vaultPath, "pi-vault-mind.config.json");
|
|
574
|
+
const projectCfg = fs.existsSync(projectCfgPath)
|
|
575
|
+
? JSON.parse(fs.readFileSync(projectCfgPath, "utf-8"))
|
|
576
|
+
: {};
|
|
577
|
+
projectCfg.extensionCompatibility = projectCfg.extensionCompatibility || {};
|
|
578
|
+
projectCfg.extensionCompatibility["pi-context"] = {
|
|
433
579
|
tagPatterns: [],
|
|
434
580
|
enhanceInjectors: false,
|
|
435
581
|
autoEnableAcm: true,
|
|
436
582
|
indexContextEvents: true,
|
|
437
|
-
...(
|
|
583
|
+
...(projectCfg.extensionCompatibility["pi-context"] || {}),
|
|
438
584
|
enabled: enableContextAutomation,
|
|
439
585
|
};
|
|
440
|
-
|
|
441
|
-
if (!fs.existsSync(dir))
|
|
442
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
443
|
-
fs.writeFileSync(getGlobalConfigPath(), `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
586
|
+
fs.writeFileSync(projectCfgPath, `${JSON.stringify(projectCfg, null, 2)}\n`, "utf-8");
|
|
444
587
|
ctx.ui.notify([
|
|
445
|
-
"✅
|
|
446
|
-
` ${getGlobalConfigPath()}`,
|
|
588
|
+
"✅ Config written!",
|
|
589
|
+
` Global: ${getGlobalConfigPath()}`,
|
|
590
|
+
` Project: ${projectCfgPath}`,
|
|
447
591
|
"",
|
|
448
592
|
` Vault: ${vaultPath}`,
|
|
449
593
|
` Remote: ${remoteUrl || "none"}`,
|
|
@@ -454,13 +598,6 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
454
598
|
"",
|
|
455
599
|
"Next: /vm watcher start (or restart pi for auto-start)",
|
|
456
600
|
].join("\n"), "info");
|
|
457
|
-
if (remoteUrl?.includes("modal.run")) {
|
|
458
|
-
ctx.ui.notify([
|
|
459
|
-
"Modal token reminder:",
|
|
460
|
-
"- /vm remote token",
|
|
461
|
-
"- ./scripts/fetch-modal-token.sh --write",
|
|
462
|
-
].join("\n"), "info");
|
|
463
|
-
}
|
|
464
601
|
};
|
|
465
602
|
/**
|
|
466
603
|
* Interactive Modal embedding configuration + "Test connection" action.
|
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 { buildPluginInstallGuidance, detectVaultFromCwd, getMissingOnboardingPlugins, 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));
|
|
@@ -52,8 +52,9 @@ describe("setupWizard plugin onboarding helpers", () => {
|
|
|
52
52
|
it("builds manual guidance with CLI commands and Obsidian URIs", () => {
|
|
53
53
|
const vault = mkTmpDir("pvm-plugin-guide-");
|
|
54
54
|
const guidance = buildPluginInstallGuidance(getMissingOnboardingPlugins(vault));
|
|
55
|
+
assert.match(guidance, /obsidian plugin:install id=obsidian42-brat enable/);
|
|
55
56
|
assert.match(guidance, /obsidian plugin:install id=actions-uri enable/);
|
|
56
|
-
assert.match(guidance, /obsidian
|
|
57
|
+
assert.match(guidance, /BRAT.*kylebrodeur\/obsidian-pi-vault-mind/);
|
|
57
58
|
fs.rmSync(vault, { recursive: true, force: true });
|
|
58
59
|
});
|
|
59
60
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vault-mind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"agents",
|
|
11
11
|
"skills",
|
|
12
12
|
"scripts",
|
|
13
|
+
"pi-vault-mind.config.example.json",
|
|
13
14
|
"CHANGELOG.md",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"collections": {
|
|
4
|
+
"main": {
|
|
5
|
+
"path": "collections/main.jsonl",
|
|
6
|
+
"schema": ["id", "domain", "source", "fact", "tag", "artifact"],
|
|
7
|
+
"dedupField": "fact"
|
|
8
|
+
},
|
|
9
|
+
"pending": {
|
|
10
|
+
"path": "collections/pending.jsonl",
|
|
11
|
+
"schema": "main"
|
|
12
|
+
},
|
|
13
|
+
"context_events": {
|
|
14
|
+
"path": "collections/context_events.jsonl",
|
|
15
|
+
"schema": ["id", "type", "session_entry_id", "content", "timestamp", "tags"],
|
|
16
|
+
"dedupField": "id"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"injectors": [],
|
|
20
|
+
"vaultMind": {
|
|
21
|
+
"dataDir": ".lancedb",
|
|
22
|
+
"embedding": {
|
|
23
|
+
"localUrl": "http://127.0.0.1:11434",
|
|
24
|
+
"useTransformers": true
|
|
25
|
+
},
|
|
26
|
+
"graph": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"canvasSync": false
|
|
29
|
+
},
|
|
30
|
+
"ftsEnabled": true,
|
|
31
|
+
"httpPort": 11435,
|
|
32
|
+
"autoIndex": false
|
|
33
|
+
},
|
|
34
|
+
"extensionCompatibility": {
|
|
35
|
+
"pi-context": {
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"tagPatterns": [],
|
|
38
|
+
"enhanceInjectors": false,
|
|
39
|
+
"autoEnableAcm": true,
|
|
40
|
+
"indexContextEvents": true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|