pi-vault-mind 0.8.8 → 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.js +150 -49
- 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 +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 });
|
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 '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);
|
|
@@ -337,54 +409,83 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
337
409
|
// ── Step 1.5: Obsidian plugin readiness ────────────────────────────────
|
|
338
410
|
await runPluginOnboarding(ctx, vaultPath);
|
|
339
411
|
// ── Step 2: Embedding config ──────────────────────────────────────────
|
|
340
|
-
let remoteUrl =
|
|
341
|
-
|
|
412
|
+
let remoteUrl = "";
|
|
413
|
+
let localUrl = "";
|
|
342
414
|
let useTransformers = false;
|
|
343
415
|
let guidedModalWorkspace;
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
])
|
|
350
|
-
|
|
351
|
-
|
|
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");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
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");
|
|
352
445
|
return;
|
|
353
446
|
}
|
|
354
|
-
|
|
355
|
-
|
|
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;
|
|
458
|
+
}
|
|
356
459
|
}
|
|
357
460
|
else {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
guidedModalWorkspace = workspaceSlug.trim();
|
|
364
|
-
remoteUrl = modalUrl(guidedModalWorkspace);
|
|
365
|
-
ctx.ui.notify([
|
|
366
|
-
"Modal endpoint discovery:",
|
|
367
|
-
`- Derived remote URL: ${remoteUrl}`,
|
|
368
|
-
"- Set token now with one of:",
|
|
369
|
-
" /vm remote token",
|
|
370
|
-
" ./scripts/fetch-modal-token.sh --write",
|
|
371
|
-
"- Reference docs:",
|
|
372
|
-
" docs/getting-started/NEW_VAULT_WALKTHROUGH.md",
|
|
373
|
-
" docs/integrations/MODAL_EMBEDDING.md",
|
|
374
|
-
].join("\n"), "info");
|
|
461
|
+
ctx.ui.notify("Could not reach Modal /models — using default (embeddinggemma @ 768).", "warning");
|
|
462
|
+
model = "embeddinggemma";
|
|
463
|
+
dim = 768;
|
|
375
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");
|
|
376
470
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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;
|
|
381
481
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
482
|
+
else {
|
|
483
|
+
// Offline transformers
|
|
484
|
+
useTransformers = true;
|
|
485
|
+
model = "Xenova/all-MiniLM-L6-v2";
|
|
486
|
+
dim = 384;
|
|
387
487
|
}
|
|
488
|
+
const workspace = guidedModalWorkspace;
|
|
388
489
|
// ── Step 3: Deterministic runtime settings ──────────────────────────────
|
|
389
490
|
const enableContextAutomation = await ctx.ui.confirm("Context automation", "Enable pi-context integration with auto-/acm trigger and event indexing?");
|
|
390
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 { 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.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",
|