pi-vault-mind 0.7.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +428 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/commands.d.ts +9 -0
  6. package/dist/src/commands.js +813 -0
  7. package/dist/src/events.d.ts +13 -0
  8. package/dist/src/events.js +236 -0
  9. package/dist/src/graph.d.ts +3 -0
  10. package/dist/src/graph.js +234 -0
  11. package/dist/src/index.d.ts +2 -0
  12. package/dist/src/index.js +61 -0
  13. package/dist/src/lance.d.ts +40 -0
  14. package/dist/src/lance.js +409 -0
  15. package/dist/src/server.d.ts +25 -0
  16. package/dist/src/server.js +180 -0
  17. package/dist/src/settings-ui.d.ts +9 -0
  18. package/dist/src/settings-ui.js +313 -0
  19. package/dist/src/state.d.ts +2 -0
  20. package/dist/src/state.js +16 -0
  21. package/dist/src/tools.d.ts +2 -0
  22. package/dist/src/tools.js +772 -0
  23. package/dist/src/types.d.ts +103 -0
  24. package/dist/src/types.js +51 -0
  25. package/dist/src/utils.d.ts +17 -0
  26. package/dist/src/utils.js +102 -0
  27. package/dist/src/vault-writer.d.ts +17 -0
  28. package/dist/src/vault-writer.js +141 -0
  29. package/dist/src/watcher.d.ts +91 -0
  30. package/dist/src/watcher.js +411 -0
  31. package/dist/src/widget.d.ts +3 -0
  32. package/dist/src/widget.js +12 -0
  33. package/dist/test/index.test.d.ts +1 -0
  34. package/dist/test/index.test.js +368 -0
  35. package/package.json +83 -0
  36. package/skills/vault-mind/SKILL.md +260 -0
  37. package/skills/vault-mind/references/tool-reference.md +53 -0
  38. package/skills/vault-mind-broadcaster/SKILL.md +112 -0
  39. package/skills/vault-mind-heavy-lifter/SKILL.md +34 -0
  40. package/skills/vault-mind-manager/SKILL.md +35 -0
  41. package/skills/vault-mind-miner/SKILL.md +40 -0
  42. package/skills/vault-mind-setup/SKILL.md +385 -0
  43. package/skills/vault-mind-setup/references/obsidian-cli-and-plugins.md +269 -0
  44. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +106 -0
  45. package/skills/vault-mind-setup/references/pi-extension-wiring.md +236 -0
  46. package/skills/vault-mind-setup/references/troubleshooting-tree.md +147 -0
@@ -0,0 +1,13 @@
1
+ import type { BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, TurnEndEvent } from "@earendil-works/pi-coding-agent";
2
+ /**
3
+ * Handles before_agent_start to inject collection entries and pi-context tag matches
4
+ * into the system prompt.
5
+ */
6
+ export declare const handleBeforeAgentStart: (pi: ExtensionAPI, event: BeforeAgentStartEvent, ctx: ExtensionContext) => Promise<{
7
+ systemPrompt?: string;
8
+ } | undefined>;
9
+ /**
10
+ * Handles turn_end to capture pi-context events and append them to the
11
+ * context_events collection.
12
+ */
13
+ export declare const handleTurnEnd: (pi: ExtensionAPI, _event: TurnEndEvent, ctx: ExtensionContext) => Promise<void>;
@@ -0,0 +1,236 @@
1
+ import * as fs from "node:fs";
2
+ import { getActiveCollection } from "./state.js";
3
+ import { ensureDir, getPiContextConfig, hasPiContextTools, loadConfig } from "./utils.js";
4
+ /**
5
+ * Handles before_agent_start to inject collection entries and pi-context tag matches
6
+ * into the system prompt.
7
+ */
8
+ export const handleBeforeAgentStart = async (pi, event, ctx) => {
9
+ const cfg = loadConfig(ctx.cwd);
10
+ const activeCollection = getActiveCollection(ctx.cwd);
11
+ let additions = `\n\n[Active Collection Context]\nThe currently active collection is "${activeCollection}". When using append_wiki or running queries, prefer this collection as the target unless specified otherwise.\n`;
12
+ // 1. Standard injector matching
13
+ for (const ij of cfg.injectors) {
14
+ const regex = new RegExp(ij.regex, "i");
15
+ const match = event.prompt.match(regex);
16
+ if (!match)
17
+ continue;
18
+ const capture = match[ij.captureGroup ?? 1];
19
+ const collection = cfg.collections[ij.collection];
20
+ if (!collection)
21
+ continue;
22
+ let entriesText = "";
23
+ if (fs.existsSync(collection.path)) {
24
+ const lines = fs.readFileSync(collection.path, "utf-8").split("\n").filter(Boolean);
25
+ const hits = [];
26
+ for (const line of lines) {
27
+ try {
28
+ const e = JSON.parse(line);
29
+ if (!ij.filterField || e[ij.filterField] === capture)
30
+ hits.push(e);
31
+ }
32
+ catch {
33
+ /* ignore */
34
+ }
35
+ }
36
+ entriesText = JSON.stringify(hits, null, 2);
37
+ }
38
+ const artifactText = ij.artifactPath && fs.existsSync(ij.artifactPath)
39
+ ? fs.readFileSync(ij.artifactPath, "utf-8")
40
+ : "";
41
+ const tmpl = ij.template ??
42
+ "\n\n=== {{injector}} CONTEXT [capture={{capture}}] ===\nENTRIES:\n{{entries}}\nARTIFACT:\n{{artifact}}\n";
43
+ additions += tmpl
44
+ .replace(/\{\{injector\}\}/g, ij.name)
45
+ .replace(/\{\{capture\}\}/g, capture ?? "")
46
+ .replace(/\{\{entries\}\}/g, entriesText || "(none)")
47
+ .replace(/\{\{artifact\}\}/g, artifactText || "(none)");
48
+ }
49
+ if (additions) {
50
+ return { systemPrompt: event.systemPrompt + additions };
51
+ }
52
+ // 2. pi-context integration: auto-ACM and tag-based triggers
53
+ const piContextCfg = getPiContextConfig(cfg);
54
+ if (!piContextCfg.enabled) {
55
+ // pi-context explicitly disabled — nothing to do
56
+ }
57
+ else if (!hasPiContextTools(pi)) {
58
+ // pi-context enabled in config but tools not available — log once per turn_start
59
+ console.warn("[pi-vault-mind] pi-context enabled but tools not found. Install pi-context extension.");
60
+ }
61
+ else {
62
+ if (piContextCfg.autoEnableAcm) {
63
+ pi.sendMessage({
64
+ customType: "pi-context",
65
+ content: "/acm",
66
+ display: false,
67
+ }, {
68
+ deliverAs: "followUp",
69
+ });
70
+ }
71
+ if (piContextCfg.tagPatterns && piContextCfg.tagPatterns.length > 0) {
72
+ try {
73
+ const allTools = pi.getAllTools();
74
+ const contextLogTool = allTools.find((t) => t.name === "context_log");
75
+ if (contextLogTool) {
76
+ const logs = await contextLogTool.execute?.(null, { limit: 100, verbose: true }, null, null, ctx);
77
+ if (logs?.content?.[0]?.text) {
78
+ const logText = logs.content[0].text;
79
+ let tagInjections = 0;
80
+ for (const pattern of piContextCfg.tagPatterns) {
81
+ const tagRegex = new RegExp(pattern, "i");
82
+ const tagMatches = logText.match(/tag:\s*([\w-]+)/gi);
83
+ if (tagMatches) {
84
+ for (const match of tagMatches) {
85
+ const tagValue = match.replace(/tag:\s*/i, "");
86
+ if (tagRegex.test(tagValue)) {
87
+ for (const [collectionName, collectionDef] of Object.entries(cfg.collections)) {
88
+ if (collectionDef.schema.includes("tag") &&
89
+ fs.existsSync(collectionDef.path)) {
90
+ const lines = fs
91
+ .readFileSync(collectionDef.path, "utf-8")
92
+ .split("\n")
93
+ .filter(Boolean);
94
+ const hits = lines
95
+ .map((l) => {
96
+ try {
97
+ return JSON.parse(l);
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ })
103
+ .filter((e) => e && e.tag === tagValue);
104
+ if (hits.length > 0) {
105
+ additions += `\n\n=== pi-context tag [${tagValue}] matched ${collectionName} ===\n${JSON.stringify(hits, null, 2)}\n`;
106
+ tagInjections += hits.length;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ if (tagInjections > 0) {
115
+ ctx.ui?.notify?.(`pi-context: injected ${tagInjections} entries from matching tags`, "info");
116
+ }
117
+ }
118
+ }
119
+ }
120
+ catch (e) {
121
+ console.warn("[pi-vault-mind] pi-context tag integration error:", e);
122
+ }
123
+ }
124
+ }
125
+ if (additions) {
126
+ return { systemPrompt: event.systemPrompt + additions };
127
+ }
128
+ };
129
+ /**
130
+ * Handles turn_end to capture pi-context events and append them to the
131
+ * context_events collection.
132
+ */
133
+ export const handleTurnEnd = async (pi, _event, ctx) => {
134
+ const cfg = loadConfig(ctx.cwd);
135
+ const piContextCfg = getPiContextConfig(cfg);
136
+ const hasPiContext = hasPiContextTools(pi);
137
+ if (!hasPiContext || !piContextCfg.enabled || !piContextCfg.indexContextEvents) {
138
+ return;
139
+ }
140
+ try {
141
+ if (!hasPiContextTools(pi)) {
142
+ return;
143
+ }
144
+ const allTools = pi.getAllTools();
145
+ const contextLogTool = allTools.find((t) => t.name === "context_log");
146
+ const contextCheckoutTool = allTools.find((t) => t.name === "context_checkout");
147
+ if (!contextLogTool || !contextCheckoutTool) {
148
+ return;
149
+ }
150
+ const collectionDef = cfg.collections.context_events;
151
+ if (!collectionDef) {
152
+ console.warn("[pi-vault-mind] 'context_events' collection not defined in config. Cannot index pi-context events.");
153
+ return;
154
+ }
155
+ // Get current session history
156
+ const logs = await contextLogTool.execute?.(null, { limit: 100, verbose: true }, null, null, ctx);
157
+ if (!logs || !logs.content || !logs.content[0]?.text) {
158
+ return;
159
+ }
160
+ const logText = logs.content[0].text;
161
+ const capturedEvents = [];
162
+ const lines = logText.split("\n");
163
+ for (const line of lines) {
164
+ const timestamp = new Date().toISOString();
165
+ let event = null;
166
+ const tagMatch = line.match(/^\*?\s*([0-9a-f]+)\s+\(tag:\s*([\w-]+).*\)\s*\[(AI|USER|BASH|TOOL|SUMMARY)\]\s*(.*)/i);
167
+ if (tagMatch) {
168
+ event = {
169
+ id: `tag-${tagMatch[1]}-${tagMatch[2]}`,
170
+ type: "tag_created",
171
+ session_entry_id: tagMatch[1],
172
+ content: `Tag '${tagMatch[2]}' created at ${tagMatch[1]}`,
173
+ timestamp,
174
+ tags: [tagMatch[2], "pi-context"],
175
+ details: {
176
+ tag_name: tagMatch[2],
177
+ entry_id: tagMatch[1],
178
+ entry_type: tagMatch[3],
179
+ entry_summary: tagMatch[4].trim(),
180
+ },
181
+ };
182
+ }
183
+ const summaryMatch = line.match(/^\*?\s*([0-9a-f]+)\s+\((ROOT|HEAD)?.*summary from (.*)\)\s*\[SUMMARY\]\s*(.*)/i);
184
+ if (summaryMatch) {
185
+ event = {
186
+ id: `checkout-summary-${summaryMatch[1]}`,
187
+ type: "checkout_summary",
188
+ session_entry_id: summaryMatch[1],
189
+ content: `Checkout summary from ${summaryMatch[3]}: ${summaryMatch[4].trim()}`,
190
+ timestamp,
191
+ tags: ["pi-context", "checkout", "summary"],
192
+ details: {
193
+ origin: summaryMatch[3],
194
+ summary_message: summaryMatch[4].trim(),
195
+ entry_id: summaryMatch[1],
196
+ },
197
+ };
198
+ }
199
+ if (event) {
200
+ capturedEvents.push(event);
201
+ }
202
+ }
203
+ // Append events directly to the context_events collection
204
+ if (collectionDef) {
205
+ ensureDir(collectionDef.path);
206
+ const existingIds = new Set();
207
+ if (collectionDef.dedupField && fs.existsSync(collectionDef.path)) {
208
+ const data = fs.readFileSync(collectionDef.path, "utf-8");
209
+ for (const line of data.split("\n").filter(Boolean)) {
210
+ try {
211
+ existingIds.add(JSON.parse(line)[collectionDef.dedupField]);
212
+ }
213
+ catch {
214
+ /* ignore malformed */
215
+ }
216
+ }
217
+ }
218
+ let appended = 0;
219
+ for (const event of capturedEvents) {
220
+ if (collectionDef.dedupField && existingIds.has(event[collectionDef.dedupField])) {
221
+ continue;
222
+ }
223
+ fs.appendFileSync(collectionDef.path, `${JSON.stringify(event)}\n`);
224
+ if (collectionDef.dedupField)
225
+ existingIds.add(event[collectionDef.dedupField]);
226
+ appended++;
227
+ }
228
+ if (appended > 0 && ctx.ui?.notify) {
229
+ ctx.ui.notify(`pi-context: captured ${appended} event(s) → ${collectionDef.path}`, "info");
230
+ }
231
+ }
232
+ }
233
+ catch (e) {
234
+ console.error("[pi-vault-mind] Error capturing pi-context events:", e);
235
+ }
236
+ };
@@ -0,0 +1,3 @@
1
+ import type { WikiConfig } from "./types.js";
2
+ export declare const graphUpsert: (dataDir: string, cfg: WikiConfig, entry: Record<string, string>) => Promise<void>;
3
+ export declare const queryGraph: (dataDir: string, cfg: WikiConfig, entityQuery: string, depth: number) => Promise<unknown>;
@@ -0,0 +1,234 @@
1
+ import { connect } from "./lance.js";
2
+ const RELATION_TYPES = ["relates-to", "depends-on", "contradicts", "is-a", "part-of"];
3
+ const extractEntities = (fact) => {
4
+ // Simple regex-based entity extraction as a lightweight alternative to LLM
5
+ const entities = [];
6
+ // Match capitalized multi-word phrases (likely proper nouns / concepts)
7
+ const capitalized = fact.match(/\b[A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*\b/g);
8
+ if (capitalized) {
9
+ for (const match of capitalized) {
10
+ if (match.length > 2 && !entities.some((e) => e.name === match)) {
11
+ entities.push({ name: match, type: "concept" });
12
+ }
13
+ }
14
+ }
15
+ // Match quoted phrases
16
+ const quoted = fact.match(/"([^"]+)"/g);
17
+ if (quoted) {
18
+ for (const match of quoted) {
19
+ const name = match.replace(/"/g, "");
20
+ if (name.length > 1 && !entities.some((e) => e.name === name)) {
21
+ entities.push({ name, type: "concept" });
22
+ }
23
+ }
24
+ }
25
+ // Keywords that suggest relationships
26
+ const keywordPatterns = {
27
+ authentication: "system",
28
+ authorization: "system",
29
+ database: "system",
30
+ api: "system",
31
+ server: "system",
32
+ config: "concept",
33
+ decision: "decision",
34
+ requirement: "concept",
35
+ architecture: "concept",
36
+ deployment: "concept",
37
+ };
38
+ const lower = fact.toLowerCase();
39
+ for (const [keyword, etype] of Object.entries(keywordPatterns)) {
40
+ if (lower.includes(keyword) && !entities.some((e) => e.name === keyword)) {
41
+ entities.push({ name: keyword.charAt(0).toUpperCase() + keyword.slice(1), type: etype });
42
+ }
43
+ }
44
+ return entities;
45
+ };
46
+ const extractRelations = (fact, entities) => {
47
+ const relations = [];
48
+ if (entities.length < 2)
49
+ return relations;
50
+ // Simple heuristic: if two entities appear in the same sentence, they are likely related
51
+ const sentences = fact.split(/[.!?]+/).filter((s) => s.trim().length > 0);
52
+ for (const sentence of sentences) {
53
+ const inSentence = entities.filter((e) => sentence.includes(e.name));
54
+ if (inSentence.length >= 2) {
55
+ for (let i = 0; i < inSentence.length; i++) {
56
+ for (let j = i + 1; j < inSentence.length; j++) {
57
+ let relationType = "relates-to";
58
+ const lower = sentence.toLowerCase();
59
+ if (lower.includes(" depends on ") || lower.includes(" requires ")) {
60
+ relationType = "depends-on";
61
+ }
62
+ else if (lower.includes(" contradicts ") || lower.includes(" conflicts with ")) {
63
+ relationType = "contradicts";
64
+ }
65
+ else if (lower.match(/\bis (?:a|an)\b/)) {
66
+ relationType = "is-a";
67
+ }
68
+ else if (lower.includes(" part of ")) {
69
+ relationType = "part-of";
70
+ }
71
+ relations.push({
72
+ from: inSentence[i].name,
73
+ relation: relationType,
74
+ to: inSentence[j].name,
75
+ strength: 0.8,
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return relations;
82
+ };
83
+ export const graphUpsert = async (dataDir, cfg, entry) => {
84
+ const conn = await connect(dataDir);
85
+ const entities = extractEntities(entry.fact || "");
86
+ if (entities.length === 0)
87
+ return;
88
+ const entityTable = await getGraphTable(conn, dataDir, cfg, "entities");
89
+ const relationTable = await getGraphTable(conn, dataDir, cfg, "relations");
90
+ const now = new Date().toISOString();
91
+ // Upsert entities
92
+ for (const entity of entities) {
93
+ const existing = await entityTable
94
+ .query()
95
+ .where(`name = '${entity.name.replace(/'/g, "''")}'`)
96
+ .limit(1)
97
+ .toArray();
98
+ if (existing.length === 0) {
99
+ await entityTable.add([
100
+ {
101
+ id: crypto.randomUUID(),
102
+ name: entity.name,
103
+ type: entity.type,
104
+ aliases: "",
105
+ summary: "",
106
+ collection_ids: JSON.stringify([entry.id]),
107
+ created_at: now,
108
+ updated_at: "",
109
+ },
110
+ ]);
111
+ }
112
+ else {
113
+ const existingRow = existing[0];
114
+ const existingIds = existingRow.collection_ids
115
+ ? JSON.parse(existingRow.collection_ids)
116
+ : [];
117
+ if (!existingIds.includes(entry.id)) {
118
+ existingIds.push(entry.id);
119
+ }
120
+ await entityTable
121
+ .mergeInsert("id")
122
+ .whenMatchedUpdateAll()
123
+ .execute([
124
+ {
125
+ id: existingRow.id,
126
+ name: entity.name,
127
+ type: entity.type,
128
+ aliases: existingRow.aliases || "",
129
+ summary: existingRow.summary || "",
130
+ collection_ids: JSON.stringify(existingIds),
131
+ created_at: existingRow.created_at,
132
+ updated_at: now,
133
+ },
134
+ ]);
135
+ }
136
+ }
137
+ // Extract and upsert relations
138
+ const relations = extractRelations(entry.fact || "", entities);
139
+ for (const rel of relations) {
140
+ const fromEntity = entities.find((e) => e.name === rel.from);
141
+ const toEntity = entities.find((e) => e.name === rel.to);
142
+ if (!fromEntity || !toEntity)
143
+ continue;
144
+ const fromExisting = await entityTable
145
+ .query()
146
+ .where(`name = '${rel.from.replace(/'/g, "''")}'`)
147
+ .limit(1)
148
+ .toArray();
149
+ const toExisting = await entityTable
150
+ .query()
151
+ .where(`name = '${rel.to.replace(/'/g, "''")}'`)
152
+ .limit(1)
153
+ .toArray();
154
+ if (fromExisting.length === 0 || toExisting.length === 0)
155
+ continue;
156
+ await relationTable.add([
157
+ {
158
+ id: crypto.randomUUID(),
159
+ from_entity_id: fromExisting[0].id,
160
+ to_entity_id: toExisting[0].id,
161
+ relation_type: rel.relation,
162
+ fact: entry.fact,
163
+ fact_strength: rel.strength,
164
+ source_entry_ids: JSON.stringify([entry.id]),
165
+ valid_at: now,
166
+ expired_at: "",
167
+ created_at: now,
168
+ },
169
+ ]);
170
+ }
171
+ };
172
+ export const queryGraph = async (dataDir, cfg, entityQuery, depth) => {
173
+ const conn = await connect(dataDir);
174
+ const entityTable = await getGraphTable(conn, dataDir, cfg, "entities");
175
+ const relationTable = await getGraphTable(conn, dataDir, cfg, "relations");
176
+ // Find seed entities
177
+ const seedResults = await entityTable
178
+ .query()
179
+ .where(`name LIKE '%${entityQuery.replace(/'/g, "''")}%'`)
180
+ .limit(5)
181
+ .toArray();
182
+ if (seedResults.length === 0) {
183
+ return { entities: [], relations: [], message: "No matching entities found." };
184
+ }
185
+ const visitedIds = new Set();
186
+ const allRelations = [];
187
+ // BFS traversal
188
+ let frontier = [...seedResults];
189
+ for (let hop = 0; hop < depth; hop++) {
190
+ const nextFrontier = [];
191
+ for (const entity of frontier) {
192
+ const eid = entity.id;
193
+ if (visitedIds.has(eid))
194
+ continue;
195
+ visitedIds.add(eid);
196
+ // Find relations where this entity is source or target
197
+ const outgoing = await relationTable
198
+ .query()
199
+ .where(`from_entity_id = '${eid}'`)
200
+ .limit(20)
201
+ .toArray();
202
+ const incoming = await relationTable
203
+ .query()
204
+ .where(`to_entity_id = '${eid}'`)
205
+ .limit(20)
206
+ .toArray();
207
+ for (const rel of [...outgoing, ...incoming]) {
208
+ allRelations.push(rel);
209
+ const otherId = rel.from_entity_id === eid
210
+ ? rel.to_entity_id
211
+ : rel.from_entity_id;
212
+ if (!visitedIds.has(otherId)) {
213
+ const otherEntity = await entityTable
214
+ .query()
215
+ .where(`id = '${otherId}'`)
216
+ .limit(1)
217
+ .toArray();
218
+ if (otherEntity.length > 0) {
219
+ nextFrontier.push(otherEntity[0]);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ frontier = nextFrontier;
225
+ }
226
+ return { entities: seedResults, relations: allRelations };
227
+ };
228
+ async function getGraphTable(conn, _dataDir, cfg, name) {
229
+ const existing = await conn.tableNames();
230
+ if (existing.includes(name)) {
231
+ return conn.openTable(name);
232
+ }
233
+ return conn.openTable(name);
234
+ }
@@ -0,0 +1,2 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export default function (pi: ExtensionAPI): void;
@@ -0,0 +1,61 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { registerCommands, selectActiveCollection, serverState, watcherState } from "./commands.js";
4
+ import { handleBeforeAgentStart, handleTurnEnd } from "./events.js";
5
+ import { startServer, stopServer } from "./server.js";
6
+ import { registerTools } from "./tools.js";
7
+ import { EXT_ROOT } from "./utils.js";
8
+ import { loadConfig } from "./utils.js";
9
+ import { startWatcher, stopWatcher } from "./watcher.js";
10
+ import { updateActiveCollectionWidget } from "./widget.js";
11
+ export default function (pi) {
12
+ /* expose skills directory */
13
+ pi.on("resources_discover", async (_event) => {
14
+ const skillsDir = path.join(EXT_ROOT, "skills");
15
+ return fs.existsSync(skillsDir) ? { skillPaths: [skillsDir] } : {};
16
+ });
17
+ pi.on("session_start", async (_event, ctx) => {
18
+ updateActiveCollectionWidget(ctx);
19
+ });
20
+ /* register shortcut */
21
+ pi.registerShortcut("ctrl+alt+l", {
22
+ description: "Select Active Collection",
23
+ handler: async (ctx) => {
24
+ await selectActiveCollection(ctx);
25
+ },
26
+ });
27
+ /* register commands */
28
+ registerCommands(pi);
29
+ /* register tools */
30
+ registerTools(pi);
31
+ /* before_agent_start: dynamic injectors */
32
+ pi.on("before_agent_start", async (event, ctx) => {
33
+ return handleBeforeAgentStart(pi, event, ctx);
34
+ });
35
+ /* turn_end: capture pi-context events */
36
+ pi.on("turn_end", async (event, ctx) => {
37
+ return handleTurnEnd(pi, event, ctx);
38
+ });
39
+ /* watcher: auto-start if vaults configured */
40
+ try {
41
+ const cfg = loadConfig(process.cwd());
42
+ serverState.port = cfg.wiki.httpPort || 11435;
43
+ startServer(pi, serverState, watcherState);
44
+ if (cfg.wiki.vaults && Object.keys(cfg.wiki.vaults).length > 0) {
45
+ setTimeout(() => {
46
+ console.log("[pi-vault-mind] Auto-starting watcher for", Object.keys(cfg.wiki.vaults).length, "vault(s)");
47
+ const vaults = cfg.wiki.vaults;
48
+ if (vaults)
49
+ startWatcher(pi, vaults, watcherState);
50
+ }, 2000);
51
+ }
52
+ }
53
+ catch {
54
+ /* config may not exist yet; user can start watcher manually via /wiki watcher start */
55
+ }
56
+ /* session_shutdown: stop the watcher and server */
57
+ pi.on("session_shutdown", async () => {
58
+ stopWatcher(watcherState);
59
+ stopServer(serverState);
60
+ });
61
+ }
@@ -0,0 +1,40 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import * as lancedb from "@lancedb/lancedb";
3
+ import type { WikiConfig } from "./types.js";
4
+ export declare const connect: (dataDir: string) => Promise<lancedb.Connection>;
5
+ export declare const resetConnection: () => void;
6
+ export interface OllamaModelInfo {
7
+ name: string;
8
+ size: string;
9
+ modified: string;
10
+ details?: {
11
+ family?: string;
12
+ parameter_size?: string;
13
+ };
14
+ }
15
+ /**
16
+ * Discover Ollama models using multiple paths (matching pi-model-router pattern):
17
+ * 1. Try `pi.exec('ollama', ['list'])` — Pi's managed shell execution
18
+ * 2. Fall back to HTTP /api/tags
19
+ * 3. Fall back to Pi's cached models.json
20
+ */
21
+ export declare const discoverOllamaModels: (piOrHost?: ExtensionAPI | string) => Promise<OllamaModelInfo[]>;
22
+ /**
23
+ * Test Ollama connectivity. Uses pi.exec first, then HTTP.
24
+ */
25
+ export declare const testOllamaConnection: (hostOrPi?: string | ExtensionAPI) => Promise<{
26
+ reachable: boolean;
27
+ models: OllamaModelInfo[];
28
+ error?: string;
29
+ }>;
30
+ /**
31
+ * Pull a model from Ollama. Uses pi.exec for managed timeout, falls back to HTTP.
32
+ */
33
+ export declare const pullOllamaModel: (model: string, piOrHost?: ExtensionAPI | string) => Promise<{
34
+ success: boolean;
35
+ message: string;
36
+ }>;
37
+ export declare const upsertEntry: (dataDir: string, collectionName: string, entry: Record<string, string>, cfg: WikiConfig) => Promise<void>;
38
+ export declare const searchHybrid: (dataDir: string, collectionName: string, query: string, limit: number, cfg: WikiConfig) => Promise<unknown[]>;
39
+ export declare const searchFts: (dataDir: string, collectionName: string, query: string, limit: number, cfg: WikiConfig) => Promise<unknown[]>;
40
+ export declare const getStatus: (dataDir: string) => Promise<Record<string, unknown>>;