pi-vault-mind 0.16.36 → 0.16.37
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/README.md +3 -9
- package/dist/runtime/headless-sync-worker.d.ts +84 -0
- package/dist/runtime/headless-sync-worker.js +319 -0
- package/dist/src/agent-docs.d.ts +6 -0
- package/dist/src/agent-docs.js +7 -2
- package/dist/src/agent-queue.d.ts +1 -1
- package/dist/src/capabilities.d.ts +31 -0
- package/dist/src/capabilities.js +264 -0
- package/dist/src/collection-index.d.ts +10 -0
- package/dist/src/collection-index.js +15 -0
- package/dist/src/commands.js +561 -78
- package/dist/src/config-keys.d.ts +1 -1
- package/dist/src/config-keys.js +5 -0
- package/dist/src/dispatch-writeback.d.ts +5 -0
- package/dist/src/docling.d.ts +3 -0
- package/dist/src/embedding-providers.d.ts +9 -8
- package/dist/src/embedding-providers.js +22 -10
- package/dist/src/engine.js +2 -0
- package/dist/src/graph.d.ts +20 -0
- package/dist/src/graph.js +199 -21
- package/dist/src/headless-rebuild.d.ts +83 -0
- package/dist/src/headless-rebuild.js +654 -0
- package/dist/src/lance.js +16 -4
- package/dist/src/markdown-import.d.ts +18 -0
- package/dist/src/markdown-import.js +193 -0
- package/dist/src/modal-client.d.ts +104 -0
- package/dist/src/modal-client.js +73 -1
- package/dist/src/modal-config.d.ts +6 -4
- package/dist/src/modal-config.js +15 -6
- package/dist/src/personalize.js +1 -1
- package/dist/src/server.js +3 -2
- package/dist/src/status.d.ts +2 -0
- package/dist/src/status.js +6 -0
- package/dist/src/sync.d.ts +25 -1
- package/dist/src/sync.js +320 -0
- package/dist/src/tool-catalog.d.ts +2 -2
- package/dist/src/tool-catalog.js +42 -3
- package/dist/src/tools.js +58 -7
- package/dist/src/transcription/engine.d.ts +144 -0
- package/dist/src/transcription/engine.js +548 -0
- package/dist/src/transcription/local-server.d.ts +30 -0
- package/dist/src/transcription/local-server.js +92 -0
- package/dist/src/transcription/note-template.d.ts +32 -0
- package/dist/src/transcription/note-template.js +231 -0
- package/dist/src/transcription/notebooklm.d.ts +33 -0
- package/dist/src/transcription/notebooklm.js +145 -0
- package/dist/src/transcription/source-resolution.d.ts +51 -0
- package/dist/src/transcription/source-resolution.js +84 -0
- package/dist/src/transcription/types.d.ts +133 -0
- package/dist/src/transcription/types.js +11 -0
- package/dist/src/types.d.ts +69 -1
- package/dist/src/types.js +25 -0
- package/dist/src/utils.js +4 -0
- package/dist/src/vault-link-graph.d.ts +29 -0
- package/dist/src/vault-link-graph.js +119 -0
- package/dist/src/vault-tools.js +23 -2
- package/dist/src/vm-handlers.js +6 -3
- package/dist/src/vm-script.d.ts +39 -0
- package/dist/src/vm-script.js +3 -7
- package/dist/src/watcher.d.ts +5 -1
- package/dist/src/watcher.js +6 -8
- package/dist/test/agent-docs.test.js +1 -1
- package/dist/test/capabilities.test.js +87 -0
- package/dist/test/commands.test.js +210 -5
- package/dist/test/config-merge.test.js +14 -0
- package/dist/test/embedding-providers.test.js +27 -0
- package/dist/test/headless-rebuild.test.js +394 -0
- package/dist/test/headless-sync-worker.test.js +152 -0
- package/dist/test/markdown-import.test.js +66 -0
- package/dist/test/modal-client.test.js +51 -0
- package/dist/test/modal-config.test.js +22 -1
- package/dist/test/personalize.test.js +1 -1
- package/dist/test/pvm-setup.test.js +42 -1
- package/dist/test/tool-catalog.test.js +16 -4
- package/dist/test/transcription-engine.test.js +382 -0
- package/dist/test/transcription-local-server.test.js +65 -0
- package/dist/test/transcription-note-template.test.js +256 -0
- package/dist/test/transcription-source-resolution.test.js +194 -0
- package/dist/test/utils.test.js +7 -0
- package/dist/test/vault-pi-script.test.js +49 -0
- package/dist/test/vm-transcribe-tool.test.js +134 -0
- package/package.json +3 -1
- package/pi-vault-mind.config.example.json +8 -0
- package/scripts/pvm-setup.mjs +14 -2
- package/scripts/vault-pi.sh +36 -11
- package/skills/vault-mind/SKILL.md +39 -7
- package/skills/vault-mind-miner/SKILL.md +16 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Passive Obsidian vault extension for the [pi](https://github.com/mariozechner/pi
|
|
|
18
18
|
- **JSONL Source-of-Truth + LanceDB Index** — every fact lives in a durable, human-readable, version-control-friendly `collections/*.jsonl` file. The LanceDB index is derived and rebuildable via `/vm reindex --all --reembed`.
|
|
19
19
|
- **Bidirectional Obsidian Sync** — substantial entries (`>200 chars` or tagged `decision`/`insight`/`requirement`) auto-write to `Vault/Agent/Inbox/`. Graph entities render as Obsidian Canvas files.
|
|
20
20
|
- **Vault-Scoped Configuration Surface** — Vault Mind setup/config now lives under `<vault>/.vault-mind/`, with the Obsidian setup wizard and `/vm setup` both writing the vault-local surface instead of a shared global config.
|
|
21
|
-
- **
|
|
21
|
+
- **Audio Transcription Lane** — `vm_transcribe` handles audio/video separately from `vm_ingest`, preferring a vault-configured Modal HTTP endpoint for high-quality jobs when `vaultMind.transcription.remoteUrl` is set, while keeping a safe local `faster-whisper` fallback and `/vm transcribe status|config|use|local` ops surface for switching lanes and managing the local server.
|
|
22
22
|
|
|
23
23
|
## Architecture
|
|
24
24
|
|
|
@@ -177,13 +177,7 @@ The Broadcaster agent can generate podcasts, study guides, and slide decks from
|
|
|
177
177
|
- `ollama` — requires Ollama running locally with `embeddinggemma` (higher quality)
|
|
178
178
|
- `modal` — optional, **bring-your-own deploy**: offload embedding + bulk re-index to a cloud GPU service and sync vectors down for offline search (see [docs/integrations/MODAL_EMBEDDING.md](docs/integrations/MODAL_EMBEDDING.md)). Default behavior is unchanged until you opt in.
|
|
179
179
|
|
|
180
|
-
> **On the Modal
|
|
181
|
-
> zero infrastructure — that's the default. `modal` is an **optional, self-hosted
|
|
182
|
-
> tier**: you deploy your own copy of the service (`uvx modal deploy modal/app.py`
|
|
183
|
-
> from a clone of this repo) to your own Modal account and point the extension at
|
|
184
|
-
> *your* URL + token. There is no shared/hosted endpoint — a deployment's URL and
|
|
185
|
-
> bearer token are private to whoever owns it. (A standalone PyPI package for the
|
|
186
|
-
> server may come later.)
|
|
180
|
+
> **On the Modal transcription path:** this is **bring-your-own deploy**. You deploy your own Modal HTTP endpoint from the `audio-transcription` repo, store its URL under `vaultMind.transcription.remoteUrl`, and Vault Mind can prefer that remote high-quality path by default. If no remote transcription URL is configured, `vm_transcribe` falls back to the local `faster-whisper` lane.
|
|
187
181
|
|
|
188
182
|
### 1. Install and set up the vault-local runtime
|
|
189
183
|
|
|
@@ -555,8 +549,8 @@ belongs in `vaultMind.identities` or an explicit environment override.
|
|
|
555
549
|
| `vm_stats` | Dashboard: counts, sizes, LanceDB status |
|
|
556
550
|
| `vm_export` | Export to JSON, CSV, or Markdown |
|
|
557
551
|
| `vm_ingest` | Convert URLs and documents to markdown via docling |
|
|
552
|
+
| `vm_transcribe` | Transcribe audio/video, generate transcript notes + raw artifacts |
|
|
558
553
|
| `vm_script` | Execute sandboxed JS/TS with vm.*, vault.read, log.* |
|
|
559
|
-
|
|
560
554
|
## Commands
|
|
561
555
|
|
|
562
556
|
| Command | Purpose |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type HeadlessRebuildAfterReindexDependencies, type HeadlessRebuildDependencies } from "../src/headless-rebuild.js";
|
|
2
|
+
import type { UniversalConfig } from "../src/types.js";
|
|
3
|
+
type WorkerPhase = "credentials" | "auth" | "sync" | "embed" | "graph" | "config";
|
|
4
|
+
export declare class HeadlessSyncWorkerError extends Error {
|
|
5
|
+
readonly phase: WorkerPhase;
|
|
6
|
+
constructor(phase: WorkerPhase);
|
|
7
|
+
}
|
|
8
|
+
export interface HeadlessSyncWorkerRequest {
|
|
9
|
+
operationId: string;
|
|
10
|
+
graphId: string;
|
|
11
|
+
collections: string[];
|
|
12
|
+
}
|
|
13
|
+
interface SecretClient {
|
|
14
|
+
secrets: {
|
|
15
|
+
resolve(reference: string): Promise<string>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
interface VectorRevision {
|
|
19
|
+
collection: string;
|
|
20
|
+
model: string;
|
|
21
|
+
dim: number;
|
|
22
|
+
watermark: number;
|
|
23
|
+
revision: string;
|
|
24
|
+
}
|
|
25
|
+
interface GraphRevisionRow {
|
|
26
|
+
graph_id: string;
|
|
27
|
+
revision: string;
|
|
28
|
+
source_fingerprint: string;
|
|
29
|
+
updated_at: string;
|
|
30
|
+
}
|
|
31
|
+
interface RevisionManifest {
|
|
32
|
+
vectors: Array<VectorRevision & {
|
|
33
|
+
updated_at: string;
|
|
34
|
+
}>;
|
|
35
|
+
graphs: GraphRevisionRow[];
|
|
36
|
+
}
|
|
37
|
+
export interface HeadlessSyncWorkerDependencies {
|
|
38
|
+
env: Record<string, string | undefined>;
|
|
39
|
+
workspaceRoot: string;
|
|
40
|
+
createClient(options: {
|
|
41
|
+
auth: string;
|
|
42
|
+
integrationName: string;
|
|
43
|
+
integrationVersion: string;
|
|
44
|
+
}): Promise<SecretClient>;
|
|
45
|
+
execFile(command: string, args: string[], options: {
|
|
46
|
+
cwd: string;
|
|
47
|
+
env: Record<string, string | undefined>;
|
|
48
|
+
}): Promise<unknown>;
|
|
49
|
+
runRebuild(request: {
|
|
50
|
+
graphId: string;
|
|
51
|
+
clonePath: string;
|
|
52
|
+
wait: true;
|
|
53
|
+
}, deps: Partial<HeadlessRebuildDependencies> & Pick<HeadlessRebuildAfterReindexDependencies, "reindex">): Promise<{
|
|
54
|
+
graphId: string;
|
|
55
|
+
sourceFingerprint: string;
|
|
56
|
+
jobId?: string;
|
|
57
|
+
jobStatus?: string;
|
|
58
|
+
}>;
|
|
59
|
+
reindexRemote(cfg: UniversalConfig, collections: string[], vaultPath: string, options: {
|
|
60
|
+
syncDown: false;
|
|
61
|
+
}): Promise<Array<{
|
|
62
|
+
collection: string;
|
|
63
|
+
job?: {
|
|
64
|
+
status: string;
|
|
65
|
+
};
|
|
66
|
+
error?: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
dim?: number | null;
|
|
69
|
+
remote_collection?: string;
|
|
70
|
+
}>>;
|
|
71
|
+
readRevisionsManifest(baseUrl: string, token: string): Promise<{
|
|
72
|
+
vectors: VectorRevision[];
|
|
73
|
+
graphs: GraphRevisionRow[];
|
|
74
|
+
}>;
|
|
75
|
+
fetchNamespaceWatermark(baseUrl: string, token: string, collection: string, model: string, dim: number): Promise<number>;
|
|
76
|
+
publishRevisions(baseUrl: string, token: string, manifest: RevisionManifest): Promise<void>;
|
|
77
|
+
now(): string;
|
|
78
|
+
}
|
|
79
|
+
export declare function runHeadlessSyncWorker(request: HeadlessSyncWorkerRequest, deps: HeadlessSyncWorkerDependencies): Promise<{
|
|
80
|
+
graphId: string;
|
|
81
|
+
graphJobId?: string;
|
|
82
|
+
sourceFingerprint: string;
|
|
83
|
+
}>;
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { execFile as nodeExecFile } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { runHeadlessRebuildAfterReindex, } from "../src/headless-rebuild.js";
|
|
6
|
+
import { reindexRemote } from "../src/sync.js";
|
|
7
|
+
const execFileAsync = promisify(nodeExecFile);
|
|
8
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9
|
+
export class HeadlessSyncWorkerError extends Error {
|
|
10
|
+
phase;
|
|
11
|
+
constructor(phase) {
|
|
12
|
+
super(`${phase} failed`);
|
|
13
|
+
this.name = "HeadlessSyncWorkerError";
|
|
14
|
+
this.phase = phase;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function required(env, key, phase) {
|
|
18
|
+
const value = env[key];
|
|
19
|
+
if (!value)
|
|
20
|
+
throw new HeadlessSyncWorkerError(phase);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function assertSafe(value, phase) {
|
|
24
|
+
if (!IDENTIFIER.test(value))
|
|
25
|
+
throw new HeadlessSyncWorkerError(phase);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function reference(env, key) {
|
|
29
|
+
const value = required(env, key, "config");
|
|
30
|
+
if (!value.startsWith("op://") || value.length > 512)
|
|
31
|
+
throw new HeadlessSyncWorkerError("config");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function optionalReference(env, key) {
|
|
35
|
+
const value = env[key];
|
|
36
|
+
if (!value)
|
|
37
|
+
return undefined;
|
|
38
|
+
return reference(env, key);
|
|
39
|
+
}
|
|
40
|
+
function selectConfig(raw, collections, token) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new HeadlessSyncWorkerError("config");
|
|
47
|
+
}
|
|
48
|
+
if (!parsed || typeof parsed !== "object")
|
|
49
|
+
throw new HeadlessSyncWorkerError("config");
|
|
50
|
+
const cfg = parsed;
|
|
51
|
+
const embeddingUrl = cfg.vaultMind?.embedding?.remoteUrl;
|
|
52
|
+
if (!cfg.collections ||
|
|
53
|
+
typeof cfg.collections !== "object" ||
|
|
54
|
+
!cfg.vaultMind ||
|
|
55
|
+
typeof embeddingUrl !== "string" ||
|
|
56
|
+
embeddingUrl.length === 0) {
|
|
57
|
+
throw new HeadlessSyncWorkerError("config");
|
|
58
|
+
}
|
|
59
|
+
const selected = {};
|
|
60
|
+
for (const collection of collections) {
|
|
61
|
+
const definition = cfg.collections[collection];
|
|
62
|
+
if (!definition)
|
|
63
|
+
throw new HeadlessSyncWorkerError("config");
|
|
64
|
+
selected[collection] = definition;
|
|
65
|
+
}
|
|
66
|
+
cfg.collections = selected;
|
|
67
|
+
cfg.vaultMind.embedding = {
|
|
68
|
+
...cfg.vaultMind.embedding,
|
|
69
|
+
remoteApiKey: token,
|
|
70
|
+
};
|
|
71
|
+
return { config: cfg, embeddingUrl };
|
|
72
|
+
}
|
|
73
|
+
function sanitizeError(error, phase) {
|
|
74
|
+
if (error instanceof HeadlessSyncWorkerError)
|
|
75
|
+
throw error;
|
|
76
|
+
throw new HeadlessSyncWorkerError(phase);
|
|
77
|
+
}
|
|
78
|
+
export async function runHeadlessSyncWorker(request, deps) {
|
|
79
|
+
assertSafe(request.operationId, "config");
|
|
80
|
+
assertSafe(request.graphId, "config");
|
|
81
|
+
if (request.collections.length === 0 ||
|
|
82
|
+
new Set(request.collections).size !== request.collections.length) {
|
|
83
|
+
throw new HeadlessSyncWorkerError("config");
|
|
84
|
+
}
|
|
85
|
+
for (const collection of request.collections)
|
|
86
|
+
assertSafe(collection, "config");
|
|
87
|
+
const workspace = path.resolve(deps.workspaceRoot, request.operationId);
|
|
88
|
+
const home = path.join(workspace, ".home");
|
|
89
|
+
const env = { ...deps.env, HOME: home };
|
|
90
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
91
|
+
required(deps.env, "OP_SERVICE_ACCOUNT_TOKEN", "credentials");
|
|
92
|
+
const embeddingToken = required(deps.env, "PVM_REMOTE_EMBEDDING_API_KEY", "credentials");
|
|
93
|
+
let email;
|
|
94
|
+
let password;
|
|
95
|
+
let mfa;
|
|
96
|
+
let e2ePassword;
|
|
97
|
+
let vault;
|
|
98
|
+
try {
|
|
99
|
+
const resolve = async (ref) => {
|
|
100
|
+
const result = (await deps.execFile("op", ["read", "--no-newline", ref], {
|
|
101
|
+
cwd: workspace,
|
|
102
|
+
env,
|
|
103
|
+
}));
|
|
104
|
+
if (typeof result.stdout !== "string" || result.stdout.length === 0) {
|
|
105
|
+
throw new HeadlessSyncWorkerError("credentials");
|
|
106
|
+
}
|
|
107
|
+
return result.stdout;
|
|
108
|
+
};
|
|
109
|
+
const mfaRef = optionalReference(deps.env, "PVM_HEADLESS_OP_MFA_REF");
|
|
110
|
+
const e2eRef = optionalReference(deps.env, "PVM_HEADLESS_OP_E2E_PASSWORD_REF");
|
|
111
|
+
[email, password, mfa, e2ePassword, vault] = await Promise.all([
|
|
112
|
+
resolve(reference(deps.env, "PVM_HEADLESS_OP_EMAIL_REF")),
|
|
113
|
+
resolve(reference(deps.env, "PVM_HEADLESS_OP_PASSWORD_REF")),
|
|
114
|
+
mfaRef ? resolve(mfaRef) : Promise.resolve(undefined),
|
|
115
|
+
e2eRef ? resolve(e2eRef) : Promise.resolve(undefined),
|
|
116
|
+
resolve(reference(deps.env, "PVM_HEADLESS_OP_VAULT_REF")),
|
|
117
|
+
]);
|
|
118
|
+
let parsedVault;
|
|
119
|
+
try {
|
|
120
|
+
parsedVault = JSON.parse(vault);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new HeadlessSyncWorkerError("credentials");
|
|
124
|
+
}
|
|
125
|
+
if (!parsedVault ||
|
|
126
|
+
typeof parsedVault !== "object" ||
|
|
127
|
+
!("vault_id" in parsedVault) ||
|
|
128
|
+
typeof parsedVault.vault_id !== "string" ||
|
|
129
|
+
!IDENTIFIER.test(parsedVault.vault_id)) {
|
|
130
|
+
throw new HeadlessSyncWorkerError("credentials");
|
|
131
|
+
}
|
|
132
|
+
vault = parsedVault.vault_id;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return sanitizeError(error, "credentials");
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const loginArgs = ["login", "--email", email, "--password", password];
|
|
139
|
+
if (mfa)
|
|
140
|
+
loginArgs.push("--mfa", mfa);
|
|
141
|
+
await deps.execFile("ob", loginArgs, { cwd: workspace, env });
|
|
142
|
+
const setupArgs = [
|
|
143
|
+
"sync-setup",
|
|
144
|
+
"--vault",
|
|
145
|
+
vault,
|
|
146
|
+
"--path",
|
|
147
|
+
workspace,
|
|
148
|
+
"--password",
|
|
149
|
+
e2ePassword,
|
|
150
|
+
"--device-name",
|
|
151
|
+
`pvm-headless-${request.operationId}`,
|
|
152
|
+
];
|
|
153
|
+
if (!e2ePassword)
|
|
154
|
+
setupArgs.splice(setupArgs.indexOf("--password"), 2);
|
|
155
|
+
await deps.execFile("ob", setupArgs, { cwd: workspace, env });
|
|
156
|
+
await deps.execFile("ob", ["sync", "--path", workspace], { cwd: workspace, env });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return sanitizeError(error, "auth");
|
|
160
|
+
}
|
|
161
|
+
let cfg;
|
|
162
|
+
let embeddingUrl;
|
|
163
|
+
try {
|
|
164
|
+
const configPath = path.join(workspace, ".vault-mind", "vault-mind.config.json");
|
|
165
|
+
const rawConfig = fs.readFileSync(configPath, "utf8");
|
|
166
|
+
({ config: cfg, embeddingUrl } = selectConfig(rawConfig, request.collections, embeddingToken));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return sanitizeError(error, "config");
|
|
170
|
+
}
|
|
171
|
+
let checkpoint;
|
|
172
|
+
let reindexRevisions;
|
|
173
|
+
try {
|
|
174
|
+
checkpoint = await deps.runRebuild({ graphId: request.graphId, clonePath: workspace, wait: true }, {
|
|
175
|
+
runObSync: async (clonePath) => {
|
|
176
|
+
await deps.execFile("ob", ["sync", "--path", clonePath], { cwd: clonePath, env });
|
|
177
|
+
},
|
|
178
|
+
reindex: async (resolvedConfig, clonePath) => {
|
|
179
|
+
const selectedConfig = {
|
|
180
|
+
...cfg,
|
|
181
|
+
...resolvedConfig,
|
|
182
|
+
collections: cfg.collections,
|
|
183
|
+
};
|
|
184
|
+
const results = await deps.reindexRemote(selectedConfig, request.collections, clonePath, {
|
|
185
|
+
syncDown: false,
|
|
186
|
+
});
|
|
187
|
+
if (results.some((result) => result.error || result.job?.status !== "done")) {
|
|
188
|
+
throw new HeadlessSyncWorkerError("embed");
|
|
189
|
+
}
|
|
190
|
+
reindexRevisions = results
|
|
191
|
+
.filter((result) => result.job?.status === "done" &&
|
|
192
|
+
!result.error &&
|
|
193
|
+
!!result.model &&
|
|
194
|
+
!!result.remote_collection)
|
|
195
|
+
.map((result) => ({
|
|
196
|
+
collection: result.remote_collection,
|
|
197
|
+
model: result.model,
|
|
198
|
+
dim: (result.dim ?? 0),
|
|
199
|
+
revision: result.job.status,
|
|
200
|
+
watermark: 0,
|
|
201
|
+
}));
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return sanitizeError(error, error instanceof HeadlessSyncWorkerError ? error.phase : "graph");
|
|
207
|
+
}
|
|
208
|
+
if (checkpoint.jobStatus !== "done" || !checkpoint.jobId)
|
|
209
|
+
throw new HeadlessSyncWorkerError("graph");
|
|
210
|
+
let vectors;
|
|
211
|
+
try {
|
|
212
|
+
vectors = reindexRevisions;
|
|
213
|
+
const remoteCollections = new Set(vectors.map((vector) => vector.collection));
|
|
214
|
+
if (vectors.length !== request.collections.length ||
|
|
215
|
+
vectors.some((vector) => !remoteCollections.has(vector.collection) ||
|
|
216
|
+
!(typeof vector.dim === "number" && vector.dim > 0) ||
|
|
217
|
+
!Number.isSafeInteger(vector.watermark) ||
|
|
218
|
+
vector.watermark < 0)) {
|
|
219
|
+
throw new HeadlessSyncWorkerError("embed");
|
|
220
|
+
}
|
|
221
|
+
const watermarks = await Promise.all(vectors.map(async (vector) => {
|
|
222
|
+
try {
|
|
223
|
+
return await deps.fetchNamespaceWatermark(embeddingUrl, embeddingToken, vector.collection, vector.model, vector.dim);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
throw new HeadlessSyncWorkerError("embed");
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
vectors = vectors.map((vector, index) => ({
|
|
230
|
+
...vector,
|
|
231
|
+
watermark: watermarks[index],
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
return sanitizeError(error, "embed");
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const existing = await deps.readRevisionsManifest(embeddingUrl, embeddingToken);
|
|
239
|
+
const selectedRemote = new Set(vectors.map((vector) => vector.collection));
|
|
240
|
+
const survivingVectors = existing.vectors
|
|
241
|
+
.filter((vector) => !selectedRemote.has(vector.collection))
|
|
242
|
+
.map((vector) => ({ ...vector, updated_at: deps.now() }));
|
|
243
|
+
await deps.publishRevisions(embeddingUrl, embeddingToken, {
|
|
244
|
+
vectors: [
|
|
245
|
+
...survivingVectors,
|
|
246
|
+
...vectors.map((vector) => ({
|
|
247
|
+
...vector,
|
|
248
|
+
updated_at: deps.now(),
|
|
249
|
+
})),
|
|
250
|
+
],
|
|
251
|
+
graphs: [
|
|
252
|
+
...existing.graphs,
|
|
253
|
+
{
|
|
254
|
+
graph_id: checkpoint.graphId,
|
|
255
|
+
revision: checkpoint.jobId,
|
|
256
|
+
source_fingerprint: checkpoint.sourceFingerprint,
|
|
257
|
+
updated_at: deps.now(),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return sanitizeError(error, "graph");
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
graphId: checkpoint.graphId,
|
|
267
|
+
graphJobId: checkpoint.jobId,
|
|
268
|
+
sourceFingerprint: checkpoint.sourceFingerprint,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const defaultDependencies = {
|
|
272
|
+
env: process.env,
|
|
273
|
+
workspaceRoot: process.env.PVM_HEADLESS_WORKSPACE_ROOT || "/pvm-vault/workspaces",
|
|
274
|
+
execFile: async (command, args, options) => execFileAsync(command, args, options),
|
|
275
|
+
runRebuild: runHeadlessRebuildAfterReindex,
|
|
276
|
+
reindexRemote: reindexRemote,
|
|
277
|
+
readRevisionsManifest: async (baseUrl, token) => {
|
|
278
|
+
const target = baseUrl.replace(/\/$/, "");
|
|
279
|
+
const response = await fetch(`${target}/sync/revisions`, {
|
|
280
|
+
method: "GET",
|
|
281
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
282
|
+
});
|
|
283
|
+
if (!response.ok)
|
|
284
|
+
throw new Error("revision read failed");
|
|
285
|
+
return (await response.json());
|
|
286
|
+
},
|
|
287
|
+
fetchNamespaceWatermark: async (baseUrl, token, collection, model, dim) => {
|
|
288
|
+
const target = baseUrl.replace(/\/$/, "");
|
|
289
|
+
const params = new URLSearchParams({ collection, model, dim: String(dim) });
|
|
290
|
+
const response = await fetch(`${target}/sync/namespace-watermark?${params.toString()}`, {
|
|
291
|
+
method: "GET",
|
|
292
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
293
|
+
});
|
|
294
|
+
if (!response.ok)
|
|
295
|
+
throw new Error("namespace watermark read failed");
|
|
296
|
+
const body = (await response.json());
|
|
297
|
+
return body.watermark;
|
|
298
|
+
},
|
|
299
|
+
publishRevisions: async (baseUrl, token, manifest) => {
|
|
300
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/sync/revisions`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
303
|
+
body: JSON.stringify(manifest),
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok)
|
|
306
|
+
throw new Error("revision publish failed");
|
|
307
|
+
},
|
|
308
|
+
now: () => new Date().toISOString(),
|
|
309
|
+
};
|
|
310
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
311
|
+
const [operationId, graphId, collectionsJson] = process.argv.slice(2);
|
|
312
|
+
void (async () => {
|
|
313
|
+
const collections = JSON.parse(collectionsJson || "[]");
|
|
314
|
+
const result = await runHeadlessSyncWorker({ operationId: operationId || "", graphId: graphId || "", collections }, defaultDependencies);
|
|
315
|
+
process.stdout.write(JSON.stringify(result));
|
|
316
|
+
})().catch(() => {
|
|
317
|
+
process.exitCode = 1;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const BASELINE_START = "<!-- vm:baseline:start -->";
|
|
2
|
+
export declare const BASELINE_END = "<!-- vm:baseline:end -->";
|
|
3
|
+
export declare const PERSONALIZATION_START = "<!-- vm:personalization:start -->";
|
|
4
|
+
export declare const PERSONALIZATION_END = "<!-- vm:personalization:end -->";
|
|
5
|
+
export declare const buildDefaultSystemMd: (vaultPath: string) => string;
|
|
6
|
+
export declare const buildDefaultAgentsMd: () => string;
|
package/dist/src/agent-docs.js
CHANGED
|
@@ -17,8 +17,10 @@ You are the unrestricted main interactive agent for this vault. Bundled speciali
|
|
|
17
17
|
- Prefer \`uv\` for Python tools and CLIs. Use \`pnpm\`/\`npm\` for JavaScript and TypeScript packages such as \`run\`.
|
|
18
18
|
- Use \`docling\` for document conversion in ingest flows.
|
|
19
19
|
- Use \`vm_script\` for reusable sandboxed JS/TS workflows when the task fits the approved host-function surface.
|
|
20
|
+
- Use \`vm_transcribe\` for audio/video transcription (voice notes, podcasts, NotebookLM audio overviews). Defaults come from \`vaultMind.transcription\`: prefer the remote Modal HTTP endpoint when \`remoteUrl\` is configured, otherwise fall back to the local engine. Keep this lane separate from \`vm_ingest\`/docling.
|
|
21
|
+
- Use the \`nlm\` CLI for NotebookLM source inventory lookups. Do not use MCP as the primary path for NotebookLM notebook/source queries.
|
|
20
22
|
- Never auto-publish, never fabricate facts, and keep human approval at external side effects.
|
|
21
|
-
- The watcher recognizes \`@agent:<role>\` and \`@agent-<role>\` anywhere in watchable files. \`@agent:main\` is reserved for
|
|
23
|
+
- The watcher recognizes \`@agent:<role>\` and \`@agent-<role>\` anywhere in watchable files. \`@agent:main\` is reserved for human-authored review and follow-up notes; it is never auto-dispatched. Automated transcript preparation, source linking, and note population use \`@agent:audio-overviews\`.
|
|
22
24
|
${BASELINE_END}
|
|
23
25
|
|
|
24
26
|
${PERSONALIZATION_START}
|
|
@@ -36,16 +38,19 @@ ${BASELINE_START}
|
|
|
36
38
|
- \`broadcaster\` — human-facing artifact generation.
|
|
37
39
|
- \`heavy-lifter\` — large-context implementation and deeper delegated work.
|
|
38
40
|
- \`personalization\` — resolves annotated feedback on generated system/agent docs.
|
|
41
|
+
- \`audio-overviews\` — automated worker for NotebookLM audio overview transcription, source resolution, and note population. Uses deterministic scripts/functions rather than freeform improvisation.
|
|
39
42
|
|
|
40
43
|
## Watcher + inline requests
|
|
41
44
|
- Use \`@agent:<role>\` or \`@agent-<role>\` in watchable files to request agent help.
|
|
42
|
-
- \`@agent:main\` is recognized but never auto-dispatched; it is for
|
|
45
|
+
- \`@agent:main\` is recognized but never auto-dispatched; it is for human-authored review notes and follow-up only — not automated transcript generation.
|
|
43
46
|
- Auto-dispatched watcher jobs preserve the request block in the source note and replace only the running-status line with the result summary.
|
|
44
47
|
- Watcher dispatches should be logged to collections so they can be queried later.
|
|
45
48
|
|
|
46
49
|
## Tooling baseline
|
|
47
50
|
- Prefer \`uv\` for Python tools.
|
|
48
51
|
- Prefer reusable \`vm_script\` workflows over ad-hoc one-off scripts when the workflow should be repeatable.
|
|
52
|
+
- Use \`vm_transcribe\` for audio/video transcription; keep this lane separate from \`vm_ingest\`/docling.
|
|
53
|
+
- Use \`nlm\` for NotebookLM source inventory. Do not use MCP as the primary path for NotebookLM lookups.
|
|
49
54
|
- Keep reusable content, test, and update workflows in skills/tools rather than scattered shell snippets.
|
|
50
55
|
${BASELINE_END}
|
|
51
56
|
|
|
@@ -75,7 +75,7 @@ export declare const markJobFailed: (vaultPath: string, id: string, error: strin
|
|
|
75
75
|
export declare const markJobRunning: (vaultPath: string, id: string) => AgentJob | undefined;
|
|
76
76
|
/** Mark a pending job as cancelled. */
|
|
77
77
|
export declare const cancelJob: (vaultPath: string, id: string) => AgentJob | undefined;
|
|
78
|
-
/** True if the file still contains the dispatch
|
|
78
|
+
/** True if the file still contains the pending dispatch status placeholder. */
|
|
79
79
|
export declare const markerStillPresent: (filePath: string, dispatchId: string) => boolean;
|
|
80
80
|
/** Scan a vault's queue and mark any running job whose marker is gone as done. */
|
|
81
81
|
export declare const sweepCompletedJobs: (vaultPath: string) => AgentJob[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CapabilityClass = "extensions" | "skills" | "agents";
|
|
2
|
+
export interface DesiredCapability {
|
|
3
|
+
source: string;
|
|
4
|
+
provider?: string;
|
|
5
|
+
version: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface DesiredState {
|
|
9
|
+
version: number;
|
|
10
|
+
extensions: Record<string, DesiredCapability>;
|
|
11
|
+
skills: Record<string, DesiredCapability>;
|
|
12
|
+
agents: Record<string, DesiredCapability>;
|
|
13
|
+
}
|
|
14
|
+
export interface ObservedCapability {
|
|
15
|
+
id: string;
|
|
16
|
+
class: CapabilityClass;
|
|
17
|
+
version: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
provider?: string;
|
|
20
|
+
}
|
|
21
|
+
export type DriftStatus = "Installed / Active" | "Disabled" | "Missing" | "Drifted" | "Unverifiable" | "Orphaned" | "Discoverable";
|
|
22
|
+
export interface DriftReportItem {
|
|
23
|
+
id: string;
|
|
24
|
+
class: CapabilityClass;
|
|
25
|
+
desired?: DesiredCapability;
|
|
26
|
+
observed?: ObservedCapability;
|
|
27
|
+
status: DriftStatus;
|
|
28
|
+
}
|
|
29
|
+
export declare const readCapabilities: (vaultPath: string) => DesiredState;
|
|
30
|
+
export declare const scanInventory: (vaultPath: string) => ObservedCapability[];
|
|
31
|
+
export declare const computeDrift: (desired: DesiredState, observed: ObservedCapability[]) => DriftReportItem[];
|