pi-vault-mind 0.7.2 → 0.7.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/dist/src/commands.js +90 -11
- package/dist/src/modal-config.d.ts +16 -4
- package/dist/src/modal-config.js +62 -9
- package/dist/src/settings-ui.d.ts +5 -3
- package/dist/src/settings-ui.js +36 -18
- package/dist/src/types.d.ts +6 -1
- package/dist/src/utils.d.ts +4 -0
- package/dist/src/utils.js +27 -0
- package/dist/src/watcher.js +2 -1
- package/dist/test/modal-config.test.js +54 -1
- package/package.json +1 -1
package/dist/src/commands.js
CHANGED
|
@@ -7,7 +7,7 @@ import { DEFAULT_CONFIG } from "./types.js";
|
|
|
7
7
|
import { CONFIG_FILES, EXT_ROOT, collectionNames, ensureDir, findConfig, getPiContextConfig, hasPiContextTools, loadConfig, } from "./utils.js";
|
|
8
8
|
import { updateActiveCollectionWidget } from "./widget.js";
|
|
9
9
|
import { connect, pullOllamaModel, testOllamaConnection } from "./lance.js";
|
|
10
|
-
import { MODAL_TOKEN_ENV, createModalClient, isModalConfigured } from "./modal-config.js";
|
|
10
|
+
import { MODAL_TOKEN_ENV, createModalClient, isModalConfigured, resolveBaseUrl, resolveModalToken, resolveWorkspace, } from "./modal-config.js";
|
|
11
11
|
import { createServerState } from "./server.js";
|
|
12
12
|
import { createCollectionWizard, createInjectorWizard, openSettingsDashboard, setupWizard, } from "./settings-ui.js";
|
|
13
13
|
import { reindexRemote, syncAll, syncCollection } from "./sync.js";
|
|
@@ -649,6 +649,7 @@ const MODAL_CONFIG_USAGE = [
|
|
|
649
649
|
"**/wiki modal config**",
|
|
650
650
|
"",
|
|
651
651
|
" /wiki modal config baseUrl <url> Set the Modal ASGI base URL",
|
|
652
|
+
" /wiki modal config workspace <name> Derive the Modal URL from workspace slug",
|
|
652
653
|
" /wiki modal config model <name> Set the canonical embedder (default embeddinggemma)",
|
|
653
654
|
" /wiki modal config dim <n> Set output dimension (omit for native)",
|
|
654
655
|
" /wiki modal config fallback ollama|none Set offline fallback provider",
|
|
@@ -692,15 +693,17 @@ const handleModalConfig = async (args, ctx) => {
|
|
|
692
693
|
const key = parts[0]?.toLowerCase();
|
|
693
694
|
const modal = cfg.wiki.embedding.modal ?? {};
|
|
694
695
|
if (!key) {
|
|
695
|
-
const tokenSrc =
|
|
696
|
-
?
|
|
696
|
+
const tokenSrc = resolveModalToken(cfg.wiki)
|
|
697
|
+
? "env or ~/.pi/agent/vault-mind.env ✅"
|
|
697
698
|
: modal.apiToken
|
|
698
|
-
? "config (set PVM_API_TOKEN env to override)"
|
|
699
|
-
: "❌ none (set PVM_API_TOKEN env)";
|
|
699
|
+
? "config (set PVM_API_TOKEN env/dotenv to override)"
|
|
700
|
+
: "❌ none (set PVM_API_TOKEN env or ~/.pi/agent/vault-mind.env)";
|
|
701
|
+
const derivedUrl = resolveBaseUrl(cfg.wiki);
|
|
700
702
|
const lines = [
|
|
701
703
|
"**Modal Config:**",
|
|
702
704
|
"",
|
|
703
|
-
`
|
|
705
|
+
` workspace: ${resolveWorkspace(cfg.wiki) || "(not set)"}`,
|
|
706
|
+
` baseUrl: ${modal.baseUrl || (derivedUrl ? `${derivedUrl} (derived)` : "❌ not set")}`,
|
|
704
707
|
` model: ${modal.model || "(default embeddinggemma)"}`,
|
|
705
708
|
` dim: ${modal.dim ?? "(native)"}`,
|
|
706
709
|
` token: ${tokenSrc}`,
|
|
@@ -837,16 +840,18 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
837
840
|
switch (sub) {
|
|
838
841
|
case "status": {
|
|
839
842
|
const modal = cfg.wiki.embedding.modal;
|
|
840
|
-
const tokenSrc =
|
|
841
|
-
?
|
|
843
|
+
const tokenSrc = resolveModalToken(cfg.wiki)
|
|
844
|
+
? "env or ~/.pi/agent/vault-mind.env ✅"
|
|
842
845
|
: modal?.apiToken
|
|
843
846
|
? "config"
|
|
844
847
|
: "❌ none";
|
|
848
|
+
const derivedUrl = resolveBaseUrl(cfg.wiki);
|
|
845
849
|
const lines = [
|
|
846
850
|
"**Modal Status**",
|
|
847
851
|
"",
|
|
848
852
|
`Configured: ${isModalConfigured(cfg.wiki) ? "✅" : "❌"}`,
|
|
849
|
-
`
|
|
853
|
+
` workspace: ${resolveWorkspace(cfg.wiki) || "(not set)"}`,
|
|
854
|
+
` baseUrl: ${modal?.baseUrl || (derivedUrl ? `${derivedUrl} (derived)` : "(not set)")}`,
|
|
850
855
|
` model: ${modal?.model || "(default embeddinggemma)"}`,
|
|
851
856
|
` dim: ${modal?.dim ?? "(native)"}`,
|
|
852
857
|
` token: ${tokenSrc}`,
|
|
@@ -928,6 +933,78 @@ const handleModal = async (args, ctx, pi) => {
|
|
|
928
933
|
}
|
|
929
934
|
return;
|
|
930
935
|
}
|
|
936
|
+
case "auto": {
|
|
937
|
+
const cfgPath = findConfig(ctx.cwd).project;
|
|
938
|
+
if (!cfgPath) {
|
|
939
|
+
ctx.ui.notify("No project config found. Run /wiki init first.", "error");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const workspace = resolveWorkspace(cfg.wiki);
|
|
943
|
+
if (!workspace && !cfg.wiki.embedding.modal?.baseUrl) {
|
|
944
|
+
ctx.ui.notify("Modal workspace or baseUrl not configured. Run:\n /wiki modal config workspace <name>", "error");
|
|
945
|
+
}
|
|
946
|
+
const token = resolveModalToken(cfg.wiki);
|
|
947
|
+
if (!token) {
|
|
948
|
+
ctx.ui.notify("Modal token not found. Set PVM_API_TOKEN env or write it to ~/.pi/agent/vault-mind.env", "error");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
ctx.ui.notify("🔄 Auto-configuring Modal...", "info");
|
|
952
|
+
const client = createModalClient(cfg.wiki);
|
|
953
|
+
if (!client) {
|
|
954
|
+
ctx.ui.notify("Could not create Modal client (missing workspace/baseUrl or token).", "error");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
let health;
|
|
958
|
+
let models;
|
|
959
|
+
try {
|
|
960
|
+
health = await client.health();
|
|
961
|
+
ctx.ui.notify(`✅ Modal reachable (default model: ${health.default_model})`, "info");
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
ctx.ui.notify(`❌ Modal health check failed: ${err.message}`, "error");
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
models = await client.models();
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
ctx.ui.notify(`❌ Could not fetch model registry: ${err.message}`, "error");
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const defaultModel = models.default || health.default_model || "embeddinggemma";
|
|
975
|
+
const obj = readProjectConfig(cfgPath);
|
|
976
|
+
const wiki = obj.wiki || {};
|
|
977
|
+
const emb = wiki.embedding || {};
|
|
978
|
+
emb.provider = "modal";
|
|
979
|
+
const modalSec = emb.modal || {};
|
|
980
|
+
if (workspace)
|
|
981
|
+
modalSec.workspace = workspace;
|
|
982
|
+
modalSec.model = defaultModel;
|
|
983
|
+
if (models.default_dim != null)
|
|
984
|
+
modalSec.dim = models.default_dim;
|
|
985
|
+
modalSec.fallback = modalSec.fallback || {
|
|
986
|
+
enabled: true,
|
|
987
|
+
provider: "ollama",
|
|
988
|
+
};
|
|
989
|
+
modalSec.sync = modalSec.sync || {
|
|
990
|
+
autoSync: false,
|
|
991
|
+
autoSyncIntervalMs: 300000,
|
|
992
|
+
};
|
|
993
|
+
emb.modal = modalSec;
|
|
994
|
+
wiki.embedding = emb;
|
|
995
|
+
obj.wiki = wiki;
|
|
996
|
+
writeProjectConfig(cfgPath, obj);
|
|
997
|
+
const lines = [
|
|
998
|
+
"✅ Modal auto-configured:",
|
|
999
|
+
` baseUrl: ${resolveBaseUrl(cfg.wiki)}`,
|
|
1000
|
+
` model: ${defaultModel}`,
|
|
1001
|
+
` dim: ${models.default_dim ?? "native"}`,
|
|
1002
|
+
"",
|
|
1003
|
+
"Run /wiki modal status to verify.",
|
|
1004
|
+
];
|
|
1005
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
931
1008
|
case "migrate": {
|
|
932
1009
|
const newModel = parts[1];
|
|
933
1010
|
if (!newModel) {
|
|
@@ -1133,7 +1210,7 @@ const handleWatcher = async (args, ctx, pi) => {
|
|
|
1133
1210
|
};
|
|
1134
1211
|
// ── Setup ────────────────────────────────────────────────────────────────────
|
|
1135
1212
|
const handleSetup = async (args, ctx) => {
|
|
1136
|
-
// Parse optional CLI-style args: --vault <path> --provider <name> --model <name>
|
|
1213
|
+
// Parse optional CLI-style args: --vault <path> --provider <name> --model <name> --workspace <name>
|
|
1137
1214
|
const cliArgs = {};
|
|
1138
1215
|
const parts = args.trim().split(/\s+--/);
|
|
1139
1216
|
for (const part of parts) {
|
|
@@ -1147,8 +1224,10 @@ const handleSetup = async (args, ctx) => {
|
|
|
1147
1224
|
cliArgs.provider = trimmed.slice(9).trim();
|
|
1148
1225
|
else if (trimmed.startsWith("model "))
|
|
1149
1226
|
cliArgs.model = trimmed.slice(6).trim();
|
|
1227
|
+
else if (trimmed.startsWith("workspace "))
|
|
1228
|
+
cliArgs.workspace = trimmed.slice(10).trim();
|
|
1150
1229
|
}
|
|
1151
|
-
const hasCliArgs = cliArgs.vault || cliArgs.provider || cliArgs.model;
|
|
1230
|
+
const hasCliArgs = cliArgs.vault || cliArgs.provider || cliArgs.model || cliArgs.workspace;
|
|
1152
1231
|
await setupWizard(ctx, hasCliArgs ? cliArgs : undefined);
|
|
1153
1232
|
};
|
|
1154
1233
|
// ── Main /wiki command ───────────────────────────────────────────────────────
|
|
@@ -12,13 +12,25 @@ import { ModalEmbeddingClient } from "./modal-client.js";
|
|
|
12
12
|
import type { WikiConfig } from "./types.js";
|
|
13
13
|
/** Env var name for the Modal bearer token (preferred over config). */
|
|
14
14
|
export declare const MODAL_TOKEN_ENV = "PVM_API_TOKEN";
|
|
15
|
+
/** Dotenv-style fallback path for the Modal token. */
|
|
16
|
+
export declare const MODAL_TOKEN_ENV_PATH: string;
|
|
17
|
+
/** Canonical deployed Modal app name. */
|
|
18
|
+
export declare const MODAL_APP_NAME = "pi-vault-mind-embed";
|
|
19
|
+
/** Derive the Modal service URL from a workspace slug and canonical app name. */
|
|
20
|
+
export declare const modalUrl: (workspace: string) => string;
|
|
15
21
|
/**
|
|
16
|
-
* Resolve the Modal API token.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* Resolve the Modal API token. Resolution order:
|
|
23
|
+
* 1. `PVM_API_TOKEN` env var
|
|
24
|
+
* 2. `~/.pi/agent/vault-mind.env`
|
|
25
|
+
* 3. `wiki.embedding.modal.apiToken` in config
|
|
26
|
+
* Never log the resolved token.
|
|
19
27
|
*/
|
|
20
28
|
export declare const resolveModalToken: (cfg: WikiConfig) => string | undefined;
|
|
21
|
-
/**
|
|
29
|
+
/** Resolve the explicit baseUrl or derive it from workspace. */
|
|
30
|
+
export declare const resolveBaseUrl: (cfg: WikiConfig) => string | undefined;
|
|
31
|
+
/** Resolve the configured workspace slug, if any. */
|
|
32
|
+
export declare const resolveWorkspace: (cfg: WikiConfig) => string | undefined;
|
|
33
|
+
/** True when Modal is usable: a base URL (explicit or derived) and a resolvable token are present. */
|
|
22
34
|
export declare const isModalConfigured: (cfg: WikiConfig) => boolean;
|
|
23
35
|
/**
|
|
24
36
|
* Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
|
package/dist/src/modal-config.js
CHANGED
|
@@ -8,29 +8,82 @@
|
|
|
8
8
|
*
|
|
9
9
|
* No HTTP is reimplemented here — `ModalEmbeddingClient` owns that.
|
|
10
10
|
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
11
13
|
import { ModalEmbeddingClient } from "./modal-client.js";
|
|
12
14
|
/** Env var name for the Modal bearer token (preferred over config). */
|
|
13
15
|
export const MODAL_TOKEN_ENV = "PVM_API_TOKEN";
|
|
16
|
+
/** Dotenv-style fallback path for the Modal token. */
|
|
17
|
+
export const MODAL_TOKEN_ENV_PATH = path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent", "vault-mind.env");
|
|
18
|
+
/** Canonical deployed Modal app name. */
|
|
19
|
+
export const MODAL_APP_NAME = "pi-vault-mind-embed";
|
|
14
20
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the resolved token.
|
|
21
|
+
* Read a dotenv-style file and return a record of KEY=VALUE pairs.
|
|
22
|
+
* Ignores comments, blank lines, and unquoted values.
|
|
18
23
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const readDotEnv = (filePath) => {
|
|
25
|
+
if (!fs.existsSync(filePath))
|
|
26
|
+
return {};
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const line of fs.readFileSync(filePath, "utf-8").split(/\r?\n/)) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
31
|
+
continue;
|
|
32
|
+
const eq = trimmed.indexOf("=");
|
|
33
|
+
if (eq === -1)
|
|
34
|
+
continue;
|
|
35
|
+
const key = trimmed.slice(0, eq).trim();
|
|
36
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
37
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
38
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
39
|
+
value = value.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
out[key] = value;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
};
|
|
45
|
+
/** Derive the Modal service URL from a workspace slug and canonical app name. */
|
|
46
|
+
export const modalUrl = (workspace) => `https://${workspace}--${MODAL_APP_NAME}-embeddingservice-fastapi-app.modal.run`;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the Modal API token. Resolution order:
|
|
49
|
+
* 1. `PVM_API_TOKEN` env var
|
|
50
|
+
* 2. `~/.pi/agent/vault-mind.env`
|
|
51
|
+
* 3. `wiki.embedding.modal.apiToken` in config
|
|
52
|
+
* Never log the resolved token.
|
|
53
|
+
*/
|
|
54
|
+
export const resolveModalToken = (cfg) => {
|
|
55
|
+
if (process.env[MODAL_TOKEN_ENV])
|
|
56
|
+
return process.env[MODAL_TOKEN_ENV];
|
|
57
|
+
const dotenv = readDotEnv(MODAL_TOKEN_ENV_PATH);
|
|
58
|
+
if (dotenv[MODAL_TOKEN_ENV])
|
|
59
|
+
return dotenv[MODAL_TOKEN_ENV];
|
|
60
|
+
return cfg.embedding.modal?.apiToken;
|
|
61
|
+
};
|
|
62
|
+
/** Resolve the explicit baseUrl or derive it from workspace. */
|
|
63
|
+
export const resolveBaseUrl = (cfg) => {
|
|
64
|
+
const modal = cfg.embedding.modal;
|
|
65
|
+
if (modal?.baseUrl)
|
|
66
|
+
return modal.baseUrl;
|
|
67
|
+
if (modal?.workspace)
|
|
68
|
+
return modalUrl(modal.workspace);
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
/** Resolve the configured workspace slug, if any. */
|
|
72
|
+
export const resolveWorkspace = (cfg) => cfg.embedding.modal?.workspace;
|
|
73
|
+
/** True when Modal is usable: a base URL (explicit or derived) and a resolvable token are present. */
|
|
74
|
+
export const isModalConfigured = (cfg) => cfg.embedding.provider === "modal" && !!resolveBaseUrl(cfg) && !!resolveModalToken(cfg);
|
|
22
75
|
/**
|
|
23
76
|
* Build a `ModalEmbeddingClient` from config. Returns null when Modal is not
|
|
24
77
|
* configured (no base URL / token) so callers can degrade gracefully.
|
|
25
78
|
*/
|
|
26
79
|
export const createModalClient = (cfg) => {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
80
|
+
const baseUrl = resolveBaseUrl(cfg);
|
|
81
|
+
if (!baseUrl)
|
|
29
82
|
return null;
|
|
30
83
|
const apiToken = resolveModalToken(cfg);
|
|
31
84
|
if (!apiToken)
|
|
32
85
|
return null;
|
|
33
|
-
const clientCfg = { baseUrl
|
|
86
|
+
const clientCfg = { baseUrl, apiToken };
|
|
34
87
|
return new ModalEmbeddingClient(clientCfg);
|
|
35
88
|
};
|
|
36
89
|
/**
|
|
@@ -5,12 +5,14 @@ export declare const setupWizard: (ctx: ExtensionContext, cliArgs?: {
|
|
|
5
5
|
vault?: string;
|
|
6
6
|
provider?: string;
|
|
7
7
|
model?: string;
|
|
8
|
+
workspace?: string;
|
|
8
9
|
}) => Promise<void>;
|
|
9
10
|
/**
|
|
10
11
|
* Interactive Modal embedding configuration + "Test connection" action.
|
|
11
|
-
* Walks base URL, canonical model, dim, offline fallback, and auto-sync
|
|
12
|
-
* (off by default). Token is read from `PVM_API_TOKEN` env
|
|
13
|
-
* collected here; only an optional
|
|
12
|
+
* Walks workspace/base URL, canonical model, dim, offline fallback, and auto-sync
|
|
13
|
+
* (off by default). Token is read from `PVM_API_TOKEN` env or
|
|
14
|
+
* `~/.pi/agent/vault-mind.env` (preferred) — not collected here; only an optional
|
|
15
|
+
* config fallback is offered.
|
|
14
16
|
*/
|
|
15
17
|
export declare const configureModalWizard: (ctx: ExtensionContext) => Promise<void>;
|
|
16
18
|
export declare const openSettingsDashboard: (ctx: ExtensionContext) => Promise<void>;
|
package/dist/src/settings-ui.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { testOllamaConnection } from "./lance.js";
|
|
4
|
-
import { MODAL_TOKEN_ENV, createModalClient } from "./modal-config.js";
|
|
5
|
-
import { GLOBAL_CONFIG_PATH, collectionNames, findConfig, loadConfig, } from "./utils.js";
|
|
4
|
+
import { MODAL_TOKEN_ENV, createModalClient, modalUrl, resolveModalToken } from "./modal-config.js";
|
|
5
|
+
import { GLOBAL_CONFIG_PATH, collectionNames, findConfig, loadConfig, shrinkHome, } from "./utils.js";
|
|
6
6
|
export const createCollectionWizard = async (ctx) => {
|
|
7
7
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
8
8
|
if (!cfgPath) {
|
|
@@ -86,7 +86,7 @@ export const createInjectorWizard = async (ctx) => {
|
|
|
86
86
|
export const setupWizard = async (ctx, cliArgs) => {
|
|
87
87
|
const existingGlobal = fs.existsSync(GLOBAL_CONFIG_PATH);
|
|
88
88
|
// ── Non-interactive / CLI mode ──────────────────────────────────────────
|
|
89
|
-
if (cliArgs && (cliArgs.vault || cliArgs.provider)) {
|
|
89
|
+
if (cliArgs && (cliArgs.vault || cliArgs.provider || cliArgs.workspace)) {
|
|
90
90
|
const config = existingGlobal
|
|
91
91
|
? JSON.parse(fs.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"))
|
|
92
92
|
: { wiki: { embedding: {}, vaults: {} } };
|
|
@@ -94,7 +94,7 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
94
94
|
config.wiki.embedding = config.wiki.embedding || {};
|
|
95
95
|
config.wiki.vaults = config.wiki.vaults || {};
|
|
96
96
|
if (cliArgs.vault) {
|
|
97
|
-
config.wiki.vaults.default = { path: cliArgs.vault };
|
|
97
|
+
config.wiki.vaults.default = { path: shrinkHome(cliArgs.vault), autoSync: true };
|
|
98
98
|
}
|
|
99
99
|
if (cliArgs.provider && ["ollama", "transformers", "modal"].includes(cliArgs.provider)) {
|
|
100
100
|
config.wiki.embedding.provider = cliArgs.provider;
|
|
@@ -102,6 +102,10 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
102
102
|
if (cliArgs.model) {
|
|
103
103
|
config.wiki.embedding.ollamaModel = cliArgs.model;
|
|
104
104
|
}
|
|
105
|
+
if (cliArgs.workspace) {
|
|
106
|
+
config.wiki.embedding.modal = config.wiki.embedding.modal || {};
|
|
107
|
+
config.wiki.embedding.modal.workspace = cliArgs.workspace;
|
|
108
|
+
}
|
|
105
109
|
const dir = path.dirname(GLOBAL_CONFIG_PATH);
|
|
106
110
|
if (!fs.existsSync(dir))
|
|
107
111
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -113,6 +117,8 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
113
117
|
lines.push(` Embedding: ${cliArgs.provider}`);
|
|
114
118
|
if (cliArgs.model)
|
|
115
119
|
lines.push(` Model: ${cliArgs.model}`);
|
|
120
|
+
if (cliArgs.workspace)
|
|
121
|
+
lines.push(` Modal workspace: ${cliArgs.workspace}`);
|
|
116
122
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
117
123
|
return;
|
|
118
124
|
}
|
|
@@ -163,8 +169,8 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
165
171
|
if (provider.startsWith("modal")) {
|
|
166
|
-
// Delegate to the dedicated Modal wizard (
|
|
167
|
-
// auto-sync, test connection) and return — it writes the project config.
|
|
172
|
+
// Delegate to the dedicated Modal wizard (workspace/baseUrl, model, dim,
|
|
173
|
+
// fallback, auto-sync, test connection) and return — it writes the project config.
|
|
168
174
|
await configureModalWizard(ctx);
|
|
169
175
|
return;
|
|
170
176
|
}
|
|
@@ -193,7 +199,7 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
193
199
|
config.wiki.embedding.ollamaModel = ollamaModel;
|
|
194
200
|
config.wiki.embedding.ollamaHost = config.wiki.embedding.ollamaHost || "http://127.0.0.1:11434";
|
|
195
201
|
}
|
|
196
|
-
config.wiki.vaults.default = { path: vaultPath, autoSync: true };
|
|
202
|
+
config.wiki.vaults.default = { path: shrinkHome(vaultPath), autoSync: true };
|
|
197
203
|
config.wiki.graph = config.wiki.graph || { enabled: true, canvasSync: true };
|
|
198
204
|
config.wiki.ftsEnabled = config.wiki.ftsEnabled !== false;
|
|
199
205
|
const dir = path.dirname(GLOBAL_CONFIG_PATH);
|
|
@@ -215,9 +221,10 @@ export const setupWizard = async (ctx, cliArgs) => {
|
|
|
215
221
|
};
|
|
216
222
|
/**
|
|
217
223
|
* Interactive Modal embedding configuration + "Test connection" action.
|
|
218
|
-
* Walks base URL, canonical model, dim, offline fallback, and auto-sync
|
|
219
|
-
* (off by default). Token is read from `PVM_API_TOKEN` env
|
|
220
|
-
* collected here; only an optional
|
|
224
|
+
* Walks workspace/base URL, canonical model, dim, offline fallback, and auto-sync
|
|
225
|
+
* (off by default). Token is read from `PVM_API_TOKEN` env or
|
|
226
|
+
* `~/.pi/agent/vault-mind.env` (preferred) — not collected here; only an optional
|
|
227
|
+
* config fallback is offered.
|
|
221
228
|
*/
|
|
222
229
|
export const configureModalWizard = async (ctx) => {
|
|
223
230
|
const { project: cfgPath } = findConfig(ctx.cwd);
|
|
@@ -231,10 +238,20 @@ export const configureModalWizard = async (ctx) => {
|
|
|
231
238
|
}
|
|
232
239
|
const cfg = loadConfig(ctx.cwd);
|
|
233
240
|
const modal = cfg.wiki.embedding.modal ?? {};
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
241
|
+
// Prefer workspace input; fall back to full URL if user already has one.
|
|
242
|
+
const workspace = await ctx.ui.input("Modal workspace slug (blank if you want to enter the full URL):", modal.workspace || "");
|
|
243
|
+
if (workspace === undefined)
|
|
237
244
|
return;
|
|
245
|
+
let baseUrl;
|
|
246
|
+
if (workspace) {
|
|
247
|
+
baseUrl = modalUrl(workspace);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
baseUrl = await ctx.ui.input("Modal base URL (deployed ASGI app):", modal.baseUrl ||
|
|
251
|
+
"https://<workspace>--pi-vault-mind-embed-embeddingservice-fastapi-app.modal.run");
|
|
252
|
+
if (!baseUrl)
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
238
255
|
const model = await ctx.ui.input("Canonical embedder model:", modal.model || "embeddinggemma");
|
|
239
256
|
if (model === undefined)
|
|
240
257
|
return;
|
|
@@ -262,6 +279,11 @@ export const configureModalWizard = async (ctx) => {
|
|
|
262
279
|
const intervalStr = await ctx.ui.input("Auto-sync interval (ms):", "300000");
|
|
263
280
|
intervalMs = Number.parseInt(intervalStr || "300000", 10) || 300000;
|
|
264
281
|
}
|
|
282
|
+
// Token guidance (env/dotenv preferred; config apiToken is a fallback)
|
|
283
|
+
const tokenEnv = resolveModalToken(loadConfig(ctx.cwd).wiki);
|
|
284
|
+
if (!tokenEnv) {
|
|
285
|
+
ctx.ui.notify(`⚠️ No ${MODAL_TOKEN_ENV} env var or ~/.pi/agent/vault-mind.env found. Set it in your shell:\n export ${MODAL_TOKEN_ENV}=<bearer token>\nOr write it to ~/.pi/agent/vault-mind.env. (Env/dotenv preferred; config apiToken is a fallback only.)`, "warning");
|
|
286
|
+
}
|
|
265
287
|
// Persist
|
|
266
288
|
const existing = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
267
289
|
existing.wiki = existing.wiki || {};
|
|
@@ -269,6 +291,7 @@ export const configureModalWizard = async (ctx) => {
|
|
|
269
291
|
existing.wiki.embedding.provider = "modal";
|
|
270
292
|
existing.wiki.embedding.modal = {
|
|
271
293
|
...(existing.wiki.embedding.modal || {}),
|
|
294
|
+
...(workspace ? { workspace } : {}),
|
|
272
295
|
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
273
296
|
model: model || "embeddinggemma",
|
|
274
297
|
...(dim != null ? { dim } : {}),
|
|
@@ -280,11 +303,6 @@ export const configureModalWizard = async (ctx) => {
|
|
|
280
303
|
},
|
|
281
304
|
};
|
|
282
305
|
fs.writeFileSync(cfgPath, `${JSON.stringify(existing, null, 2)}\n`, "utf-8");
|
|
283
|
-
// Token guidance + optional config fallback (env always wins)
|
|
284
|
-
const tokenEnv = process.env[MODAL_TOKEN_ENV];
|
|
285
|
-
if (!tokenEnv) {
|
|
286
|
-
ctx.ui.notify(`⚠️ No ${MODAL_TOKEN_ENV} env var detected. Set it in your shell:\n export ${MODAL_TOKEN_ENV}=<bearer token>\n(Env is preferred; config apiToken is a fallback only.)`, "warning");
|
|
287
|
-
}
|
|
288
306
|
// Test connection against /health
|
|
289
307
|
const test = await ctx.ui.confirm("Test connection", "Call /health now to verify the Modal service?");
|
|
290
308
|
if (test) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -24,7 +24,12 @@ export interface InjectorDef {
|
|
|
24
24
|
*/
|
|
25
25
|
export interface ModalEmbeddingConfig {
|
|
26
26
|
/** Base URL of the deployed Modal ASGI app (no trailing slash needed). */
|
|
27
|
-
baseUrl
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Modal workspace slug. When set, the extension derives `baseUrl` from the
|
|
30
|
+
* canonical app name if `baseUrl` itself is not set.
|
|
31
|
+
*/
|
|
32
|
+
workspace?: string;
|
|
28
33
|
/**
|
|
29
34
|
* Bearer token matching the `pi-vault-mind-auth` Modal secret. Prefer
|
|
30
35
|
* setting via the `PVM_API_TOKEN` env var over committing it to config;
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare const CONFIG_FILES: string[];
|
|
|
4
4
|
export declare const GLOBAL_CONFIG_DIR: string;
|
|
5
5
|
export declare const GLOBAL_CONFIG_PATH: string;
|
|
6
6
|
export declare const EXT_ROOT: string;
|
|
7
|
+
/** Expand a leading `~` to the user's home directory. */
|
|
8
|
+
export declare const expandHome: (p: string) => string;
|
|
9
|
+
/** Store a path as `~/...` when it lives under the user's home directory. */
|
|
10
|
+
export declare const shrinkHome: (p: string) => string;
|
|
7
11
|
export declare const resolvePath: (cwd: string, p: string) => string;
|
|
8
12
|
export declare const ensureDir: (filePath: string) => void;
|
|
9
13
|
export declare const findConfig: (cwd: string) => {
|
package/dist/src/utils.js
CHANGED
|
@@ -6,6 +6,22 @@ export const CONFIG_FILES = ["pi-vault-mind.config.json", ".pi/vault-mind.config
|
|
|
6
6
|
export const GLOBAL_CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent");
|
|
7
7
|
export const GLOBAL_CONFIG_PATH = path.join(GLOBAL_CONFIG_DIR, "vault-mind.config.json");
|
|
8
8
|
export const EXT_ROOT = path.join(import.meta.dirname, "..");
|
|
9
|
+
/** Expand a leading `~` to the user's home directory. */
|
|
10
|
+
export const expandHome = (p) => {
|
|
11
|
+
if (p.startsWith("~/") || p === "~") {
|
|
12
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
13
|
+
return home ? path.join(home, p.slice(1)) : p;
|
|
14
|
+
}
|
|
15
|
+
return p;
|
|
16
|
+
};
|
|
17
|
+
/** Store a path as `~/...` when it lives under the user's home directory. */
|
|
18
|
+
export const shrinkHome = (p) => {
|
|
19
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
20
|
+
if (home && p.startsWith(home)) {
|
|
21
|
+
return `~${p.slice(home.length)}`;
|
|
22
|
+
}
|
|
23
|
+
return p;
|
|
24
|
+
};
|
|
9
25
|
export const resolvePath = (cwd, p) => path.isAbsolute(p) ? p : path.join(cwd, p);
|
|
10
26
|
export const ensureDir = (filePath) => {
|
|
11
27
|
const dir = path.dirname(filePath);
|
|
@@ -38,6 +54,15 @@ const readConfigFile = (p) => {
|
|
|
38
54
|
return {};
|
|
39
55
|
}
|
|
40
56
|
};
|
|
57
|
+
/** Expand `~` in all vault paths after loading a config layer. */
|
|
58
|
+
const expandVaultPaths = (layer) => {
|
|
59
|
+
if (!layer.wiki?.vaults)
|
|
60
|
+
return;
|
|
61
|
+
for (const vault of Object.values(layer.wiki.vaults)) {
|
|
62
|
+
if (vault.path)
|
|
63
|
+
vault.path = expandHome(vault.path);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
41
66
|
const mergeConfigLayer = (base, layer, cwd) => {
|
|
42
67
|
const rawLayer = layer;
|
|
43
68
|
const merged = {
|
|
@@ -80,6 +105,8 @@ export const loadConfig = (cwd) => {
|
|
|
80
105
|
const paths = findConfig(cwd);
|
|
81
106
|
const globalCfg = readConfigFile(paths.global);
|
|
82
107
|
const projectCfg = readConfigFile(paths.project);
|
|
108
|
+
expandVaultPaths(globalCfg);
|
|
109
|
+
expandVaultPaths(projectCfg);
|
|
83
110
|
let merged = mergeConfigLayer(DEFAULT_CONFIG, globalCfg, cwd);
|
|
84
111
|
merged = mergeConfigLayer(merged, projectCfg, cwd);
|
|
85
112
|
return merged;
|
package/dist/src/watcher.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { expandHome } from "./utils.js";
|
|
19
20
|
// ── Constants ────────────────────────────────────────────────────────────────────
|
|
20
21
|
const MARKER_REGEX = /@agent-(\w+)(?::(\S+))?\s*(.*)/;
|
|
21
22
|
const WRITEBACK_MARKER_REGEX = /<!--\s*pi-dispatch:(\S+)\s*-->/;
|
|
@@ -231,7 +232,7 @@ export function startWatcher(pi, vaults, state) {
|
|
|
231
232
|
return;
|
|
232
233
|
}
|
|
233
234
|
for (const [name, vault] of vaultEntries) {
|
|
234
|
-
const vaultPath = vault.path;
|
|
235
|
+
const vaultPath = expandHome(vault.path);
|
|
235
236
|
if (!fs.existsSync(vaultPath)) {
|
|
236
237
|
console.warn(`[pi-vault-mind] Vault "${name}" path does not exist: ${vaultPath}`);
|
|
237
238
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import { MODAL_TOKEN_ENV, createModalClient, isModalConfigured, namespacedTableName, resolveDim, resolveModalToken, resolveModel, } from "../src/modal-config.js";
|
|
3
|
+
import { MODAL_TOKEN_ENV, MODAL_TOKEN_ENV_PATH, createModalClient, isModalConfigured, modalUrl, namespacedTableName, resolveBaseUrl, resolveDim, resolveModalToken, resolveModel, resolveWorkspace, } from "../src/modal-config.js";
|
|
4
4
|
const wiki = (over = {}) => ({
|
|
5
5
|
dataDir: ".lancedb",
|
|
6
6
|
embedding: { provider: "modal" },
|
|
@@ -79,6 +79,59 @@ describe("modal-config", () => {
|
|
|
79
79
|
assert.equal(resolveDim(cfg, "special"), 128);
|
|
80
80
|
assert.equal(resolveDim(wiki()), undefined);
|
|
81
81
|
});
|
|
82
|
+
it("resolveModalToken: dotenv file is a fallback when env is unset", async () => {
|
|
83
|
+
delete process.env[MODAL_TOKEN_ENV];
|
|
84
|
+
const fs = await import("node:fs");
|
|
85
|
+
const path = await import("node:path");
|
|
86
|
+
const dir = path.dirname(MODAL_TOKEN_ENV_PATH);
|
|
87
|
+
if (!fs.existsSync(dir))
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(MODAL_TOKEN_ENV_PATH, 'PVM_API_TOKEN="dotenv-token"\n', "utf-8");
|
|
90
|
+
try {
|
|
91
|
+
const cfg = wiki({ embedding: { provider: "modal", modal: { baseUrl: "https://x" } } });
|
|
92
|
+
assert.equal(resolveModalToken(cfg), "dotenv-token");
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
fs.rmSync(MODAL_TOKEN_ENV_PATH, { force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it("modalUrl derives the canonical service URL from workspace", () => {
|
|
99
|
+
assert.equal(modalUrl("kylebrodeur"), "https://kylebrodeur--pi-vault-mind-embed-embeddingservice-fastapi-app.modal.run");
|
|
100
|
+
});
|
|
101
|
+
it("resolveBaseUrl: explicit baseUrl wins over derived workspace URL", () => {
|
|
102
|
+
const cfg = wiki({
|
|
103
|
+
embedding: {
|
|
104
|
+
provider: "modal",
|
|
105
|
+
modal: {
|
|
106
|
+
baseUrl: "https://explicit.example.com",
|
|
107
|
+
workspace: "kylebrodeur",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
assert.equal(resolveBaseUrl(cfg), "https://explicit.example.com");
|
|
112
|
+
});
|
|
113
|
+
it("resolveBaseUrl: derives URL from workspace when baseUrl is missing", () => {
|
|
114
|
+
const cfg = wiki({
|
|
115
|
+
embedding: { provider: "modal", modal: { workspace: "kylebrodeur" } },
|
|
116
|
+
});
|
|
117
|
+
assert.equal(resolveBaseUrl(cfg), modalUrl("kylebrodeur"));
|
|
118
|
+
});
|
|
119
|
+
it("resolveWorkspace returns the configured workspace slug", () => {
|
|
120
|
+
const cfg = wiki({
|
|
121
|
+
embedding: { provider: "modal", modal: { workspace: "kylebrodeur" } },
|
|
122
|
+
});
|
|
123
|
+
assert.equal(resolveWorkspace(cfg), "kylebrodeur");
|
|
124
|
+
assert.equal(resolveWorkspace(wiki()), undefined);
|
|
125
|
+
});
|
|
126
|
+
it("createModalClient: builds a client from workspace-derived URL", () => {
|
|
127
|
+
process.env[MODAL_TOKEN_ENV] = "tok";
|
|
128
|
+
const c = createModalClient(wiki({ embedding: { provider: "modal", modal: { workspace: "kylebrodeur" } } }));
|
|
129
|
+
assert.ok(c);
|
|
130
|
+
});
|
|
131
|
+
it("isModalConfigured: true when workspace + token are present (no explicit baseUrl)", () => {
|
|
132
|
+
process.env[MODAL_TOKEN_ENV] = "tok";
|
|
133
|
+
assert.equal(isModalConfigured(wiki({ embedding: { provider: "modal", modal: { workspace: "kylebrodeur" } } })), true);
|
|
134
|
+
});
|
|
82
135
|
it("namespacedTableName mirrors the server's ADR-3 scheme", () => {
|
|
83
136
|
assert.equal(namespacedTableName("main", "embeddinggemma", 768), "col_main__embeddinggemma__768");
|
|
84
137
|
assert.equal(namespacedTableName("ctx", "minilm-l6", 384), "col_ctx__minilm-l6__384");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vault-mind",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|