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.
- package/LICENSE +21 -0
- package/README.md +428 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/commands.d.ts +9 -0
- package/dist/src/commands.js +813 -0
- package/dist/src/events.d.ts +13 -0
- package/dist/src/events.js +236 -0
- package/dist/src/graph.d.ts +3 -0
- package/dist/src/graph.js +234 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +61 -0
- package/dist/src/lance.d.ts +40 -0
- package/dist/src/lance.js +409 -0
- package/dist/src/server.d.ts +25 -0
- package/dist/src/server.js +180 -0
- package/dist/src/settings-ui.d.ts +9 -0
- package/dist/src/settings-ui.js +313 -0
- package/dist/src/state.d.ts +2 -0
- package/dist/src/state.js +16 -0
- package/dist/src/tools.d.ts +2 -0
- package/dist/src/tools.js +772 -0
- package/dist/src/types.d.ts +103 -0
- package/dist/src/types.js +51 -0
- package/dist/src/utils.d.ts +17 -0
- package/dist/src/utils.js +102 -0
- package/dist/src/vault-writer.d.ts +17 -0
- package/dist/src/vault-writer.js +141 -0
- package/dist/src/watcher.d.ts +91 -0
- package/dist/src/watcher.js +411 -0
- package/dist/src/widget.d.ts +3 -0
- package/dist/src/widget.js +12 -0
- package/dist/test/index.test.d.ts +1 -0
- package/dist/test/index.test.js +368 -0
- package/package.json +83 -0
- package/skills/vault-mind/SKILL.md +260 -0
- package/skills/vault-mind/references/tool-reference.md +53 -0
- package/skills/vault-mind-broadcaster/SKILL.md +112 -0
- package/skills/vault-mind-heavy-lifter/SKILL.md +34 -0
- package/skills/vault-mind-manager/SKILL.md +35 -0
- package/skills/vault-mind-miner/SKILL.md +40 -0
- package/skills/vault-mind-setup/SKILL.md +385 -0
- package/skills/vault-mind-setup/references/obsidian-cli-and-plugins.md +269 -0
- package/skills/vault-mind-setup/references/obsidian-vault-structure.md +106 -0
- package/skills/vault-mind-setup/references/pi-extension-wiring.md +236 -0
- 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,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>>;
|