pi-para 0.5.2
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/CHANGELOG.md +214 -0
- package/README.md +305 -0
- package/SETUP.md +171 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +461 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +405 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +15 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +786 -0
- package/dist/commands.js.map +1 -0
- package/dist/context.d.ts +61 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +289 -0
- package/dist/context.js.map +1 -0
- package/dist/daemon.d.ts +49 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +226 -0
- package/dist/daemon.js.map +1 -0
- package/dist/frontmatter.d.ts +59 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +170 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/gepa/dataset.d.ts +36 -0
- package/dist/gepa/dataset.d.ts.map +1 -0
- package/dist/gepa/dataset.js +148 -0
- package/dist/gepa/dataset.js.map +1 -0
- package/dist/gepa/evaluator.d.ts +51 -0
- package/dist/gepa/evaluator.d.ts.map +1 -0
- package/dist/gepa/evaluator.js +317 -0
- package/dist/gepa/evaluator.js.map +1 -0
- package/dist/gepa/index.d.ts +66 -0
- package/dist/gepa/index.d.ts.map +1 -0
- package/dist/gepa/index.js +240 -0
- package/dist/gepa/index.js.map +1 -0
- package/dist/gepa/llm.d.ts +35 -0
- package/dist/gepa/llm.d.ts.map +1 -0
- package/dist/gepa/llm.js +268 -0
- package/dist/gepa/llm.js.map +1 -0
- package/dist/gepa/optimized/capture-prompt.txt +65 -0
- package/dist/gepa/optimizer.d.ts +29 -0
- package/dist/gepa/optimizer.d.ts.map +1 -0
- package/dist/gepa/optimizer.js +249 -0
- package/dist/gepa/optimizer.js.map +1 -0
- package/dist/gepa/targets.d.ts +39 -0
- package/dist/gepa/targets.d.ts.map +1 -0
- package/dist/gepa/targets.js +326 -0
- package/dist/gepa/targets.js.map +1 -0
- package/dist/gepa/types.d.ts +168 -0
- package/dist/gepa/types.d.ts.map +1 -0
- package/dist/gepa/types.js +124 -0
- package/dist/gepa/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +294 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +88 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +105 -0
- package/dist/ingest.js.map +1 -0
- package/dist/link-discovery.d.ts +40 -0
- package/dist/link-discovery.d.ts.map +1 -0
- package/dist/link-discovery.js +122 -0
- package/dist/link-discovery.js.map +1 -0
- package/dist/link-utils.d.ts +30 -0
- package/dist/link-utils.d.ts.map +1 -0
- package/dist/link-utils.js +145 -0
- package/dist/link-utils.js.map +1 -0
- package/dist/lint.d.ts +39 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +827 -0
- package/dist/lint.js.map +1 -0
- package/dist/maintainer.d.ts +24 -0
- package/dist/maintainer.d.ts.map +1 -0
- package/dist/maintainer.js +358 -0
- package/dist/maintainer.js.map +1 -0
- package/dist/processor.d.ts +14 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +361 -0
- package/dist/processor.js.map +1 -0
- package/dist/query.d.ts +69 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +107 -0
- package/dist/query.js.map +1 -0
- package/dist/raw.d.ts +57 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +219 -0
- package/dist/raw.js.map +1 -0
- package/dist/redact.d.ts +21 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +143 -0
- package/dist/redact.js.map +1 -0
- package/dist/scope.d.ts +54 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +161 -0
- package/dist/scope.js.map +1 -0
- package/dist/scripts/bulk-link-fix.d.ts +9 -0
- package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
- package/dist/scripts/bulk-link-fix.js +106 -0
- package/dist/scripts/bulk-link-fix.js.map +1 -0
- package/dist/session-tools.d.ts +33 -0
- package/dist/session-tools.d.ts.map +1 -0
- package/dist/session-tools.js +211 -0
- package/dist/session-tools.js.map +1 -0
- package/dist/state.d.ts +54 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +176 -0
- package/dist/state.js.map +1 -0
- package/dist/store.d.ts +56 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +227 -0
- package/dist/store.js.map +1 -0
- package/dist/summarize.d.ts +56 -0
- package/dist/summarize.d.ts.map +1 -0
- package/dist/summarize.js +231 -0
- package/dist/summarize.js.map +1 -0
- package/dist/tag-registry.d.ts +56 -0
- package/dist/tag-registry.d.ts.map +1 -0
- package/dist/tag-registry.js +146 -0
- package/dist/tag-registry.js.map +1 -0
- package/dist/templates/prompts.d.ts +63 -0
- package/dist/templates/prompts.d.ts.map +1 -0
- package/dist/templates/prompts.js +260 -0
- package/dist/templates/prompts.js.map +1 -0
- package/dist/tools.d.ts +26 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1033 -0
- package/dist/tools.js.map +1 -0
- package/dist/watcher.d.ts +25 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +84 -0
- package/dist/watcher.js.map +1 -0
- package/dist/webui/client/dist/index.html +1885 -0
- package/dist/webui/server.d.ts +31 -0
- package/dist/webui/server.d.ts.map +1 -0
- package/dist/webui/server.js +566 -0
- package/dist/webui/server.js.map +1 -0
- package/dist/wiki.d.ts +103 -0
- package/dist/wiki.d.ts.map +1 -0
- package/dist/wiki.js +428 -0
- package/dist/wiki.js.map +1 -0
- package/package.json +142 -0
- package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
- package/scripts/gepa/dataset.py +192 -0
- package/scripts/gepa/lm_providers.py +472 -0
- package/scripts/gepa/metric.py +279 -0
- package/scripts/gepa/optimize.py +367 -0
- package/scripts/gepa/program.py +99 -0
- package/scripts/gepa/pyproject.toml +19 -0
- package/scripts/gepa/run-all.sh +53 -0
- package/scripts/gepa/uv.lock +2241 -0
- package/scripts/gepa/wiki_reader.py +113 -0
- package/skills/para/SKILL.md +77 -0
- package/skills/setup/SKILL.md +20 -0
- package/src/capture.ts +563 -0
- package/src/cli.ts +416 -0
- package/src/commands.ts +878 -0
- package/src/context.ts +390 -0
- package/src/daemon.ts +288 -0
- package/src/frontmatter.ts +219 -0
- package/src/gepa/index.ts +287 -0
- package/src/gepa/optimized/capture-prompt.txt +65 -0
- package/src/index.ts +384 -0
- package/src/ingest.ts +177 -0
- package/src/link-discovery.ts +170 -0
- package/src/link-utils.ts +169 -0
- package/src/lint.ts +1018 -0
- package/src/maintainer.ts +404 -0
- package/src/processor.ts +415 -0
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +198 -0
- package/src/python/rlm_capture.py +215 -0
- package/src/query.ts +148 -0
- package/src/raw.ts +276 -0
- package/src/redact.ts +159 -0
- package/src/scope.ts +214 -0
- package/src/scripts/bulk-link-fix.ts +126 -0
- package/src/session-tools.ts +260 -0
- package/src/state.ts +271 -0
- package/src/store.ts +288 -0
- package/src/summarize.ts +273 -0
- package/src/tag-registry.ts +157 -0
- package/src/templates/index.md +17 -0
- package/src/templates/prompts.ts +274 -0
- package/src/templates/schema.md +116 -0
- package/src/tools.ts +1293 -0
- package/src/watcher.ts +101 -0
- package/src/webui/client/dist/index.html +1885 -0
- package/src/webui/server.ts +765 -0
- package/src/wiki.ts +550 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-para — PARA Knowledge Base Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Main extension entry point. Wires together wiki filesystem, qmd store,
|
|
5
|
+
* scope detection, context injection, tools, and commands.
|
|
6
|
+
*
|
|
7
|
+
* Session capture is handled by a separate daemon (pi-para-daemon).
|
|
8
|
+
* This extension registers completed sessions for the daemon to process.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
import { homedir, networkInterfaces } from "node:os";
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import type { QMDStore } from "qmd-engine";
|
|
17
|
+
|
|
18
|
+
import type { ProjectScope } from "./scope.js";
|
|
19
|
+
import { detectScope } from "./scope.js";
|
|
20
|
+
import { initWiki } from "./wiki.js";
|
|
21
|
+
import { openStore, closeStore } from "./store.js";
|
|
22
|
+
import { registerTools } from "./tools.js";
|
|
23
|
+
import { setupContextInjection, markContextDirty } from "./context.js";
|
|
24
|
+
import type { ContextConfig } from "./context.js";
|
|
25
|
+
import { registerCommands } from "./commands.js";
|
|
26
|
+
import { createServer } from "node:http";
|
|
27
|
+
|
|
28
|
+
// Re-export public types for consumers
|
|
29
|
+
export type { ParaCategory, WikiPage, PageFrontmatter, PageRef, LogEntry } from "./wiki.js";
|
|
30
|
+
export type { WikiSearchOptions, WikiSearchResult } from "./store.js";
|
|
31
|
+
export type { ProjectScope, ScopeConfig } from "./scope.js";
|
|
32
|
+
export type { IngestOptions, IngestReport, IngestResult } from "./ingest.js";
|
|
33
|
+
export type { QueryOptions, QueryResult } from "./query.js";
|
|
34
|
+
export type { LintOptions, LintReport, LintIssue, WikiStats } from "./lint.js";
|
|
35
|
+
export type { ContextOptions } from "./context.js";
|
|
36
|
+
export type { RawSource, SessionDigest } from "./raw.js";
|
|
37
|
+
export type { SummarizeOptions } from "./summarize.js";
|
|
38
|
+
|
|
39
|
+
// -- Config ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
interface ParaConfig {
|
|
42
|
+
wikiDir: string;
|
|
43
|
+
contextMaxTokens: number;
|
|
44
|
+
contextIncludeSchema: boolean;
|
|
45
|
+
contextIncludeIndex: boolean;
|
|
46
|
+
lintAutoFix: boolean;
|
|
47
|
+
lintStaleDays: number;
|
|
48
|
+
searchLimit: number;
|
|
49
|
+
searchIncludeArchives: boolean;
|
|
50
|
+
searchGraphBoost: boolean;
|
|
51
|
+
/** Daemon LLM: "provider/model-id" (e.g. "anthropic/claude-sonnet-4").
|
|
52
|
+
* If not set, daemon auto-detects from pi env keys or qmd config. */
|
|
53
|
+
daemonModel: string | null;
|
|
54
|
+
/** Web wiki UI settings */
|
|
55
|
+
webWiki: {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
host: string;
|
|
58
|
+
port: number;
|
|
59
|
+
};
|
|
60
|
+
/** GEPA prompt optimizer settings */
|
|
61
|
+
gepa: {
|
|
62
|
+
/** When true, load optimized prompts from ~/.pi/wiki/gepa/optimized/ at runtime. */
|
|
63
|
+
useOptimized: boolean;
|
|
64
|
+
/** Student model — runs the proxy (generates wiki output). Fast + cheap.
|
|
65
|
+
* Default: anthropic/claude-sonnet-4-20250514 */
|
|
66
|
+
studentModel: string | null;
|
|
67
|
+
/** Teacher/reflection model — proposes instruction mutations. Smart + creative.
|
|
68
|
+
* Default: anthropic/claude-opus-4-6 */
|
|
69
|
+
teacherModel: string | null;
|
|
70
|
+
/** Judge model — scores candidate outputs (LLM-as-judge). Fast + cheap.
|
|
71
|
+
* Default: same as studentModel */
|
|
72
|
+
judgeModel: string | null;
|
|
73
|
+
/** Budget preset: light (~460 calls), medium (~1500), heavy (~5000) */
|
|
74
|
+
auto: "light" | "medium" | "heavy";
|
|
75
|
+
/** Parallel eval threads */
|
|
76
|
+
threads: number;
|
|
77
|
+
/** Random seed for reproducibility */
|
|
78
|
+
seed: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getDefaultConfig(): ParaConfig {
|
|
83
|
+
return {
|
|
84
|
+
wikiDir: join(homedir(), ".pi", "wiki"),
|
|
85
|
+
contextMaxTokens: 4000,
|
|
86
|
+
contextIncludeSchema: true,
|
|
87
|
+
contextIncludeIndex: true,
|
|
88
|
+
lintAutoFix: true,
|
|
89
|
+
lintStaleDays: 90,
|
|
90
|
+
searchLimit: 10,
|
|
91
|
+
searchIncludeArchives: false,
|
|
92
|
+
searchGraphBoost: true,
|
|
93
|
+
daemonModel: null,
|
|
94
|
+
webWiki: {
|
|
95
|
+
enabled: false,
|
|
96
|
+
host: "0.0.0.0",
|
|
97
|
+
port: 10973,
|
|
98
|
+
},
|
|
99
|
+
gepa: {
|
|
100
|
+
useOptimized: true,
|
|
101
|
+
studentModel: null,
|
|
102
|
+
teacherModel: null,
|
|
103
|
+
judgeModel: null,
|
|
104
|
+
auto: "light",
|
|
105
|
+
threads: 2,
|
|
106
|
+
seed: 42,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function loadConfig(): Promise<ParaConfig> {
|
|
112
|
+
const defaults = getDefaultConfig();
|
|
113
|
+
const configPath = join(defaults.wikiDir, "config.json");
|
|
114
|
+
try {
|
|
115
|
+
const raw = await readFile(configPath, "utf-8");
|
|
116
|
+
const parsed = JSON.parse(raw) as Partial<ParaConfig>;
|
|
117
|
+
const wikiDir = parsed.wikiDir
|
|
118
|
+
? parsed.wikiDir.replace(/^~\//, `${homedir()}/`)
|
|
119
|
+
: defaults.wikiDir;
|
|
120
|
+
return { ...defaults, ...parsed, wikiDir };
|
|
121
|
+
} catch {
|
|
122
|
+
try {
|
|
123
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
124
|
+
await writeFile(configPath, JSON.stringify(defaults, null, 2), "utf-8");
|
|
125
|
+
} catch {
|
|
126
|
+
// Non-fatal
|
|
127
|
+
}
|
|
128
|
+
return { ...defaults };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// -- LAN IP detection --------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/** Check if the daemon's web server is alive on the given port. */
|
|
135
|
+
function checkWebServerAlive(port: number): Promise<boolean> {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const req = createServer().listen(port, "127.0.0.1");
|
|
138
|
+
req.on("error", (err: NodeJS.ErrnoException) => {
|
|
139
|
+
req.close();
|
|
140
|
+
// EADDRINUSE means something is already listening — daemon is running
|
|
141
|
+
resolve(err.code === "EADDRINUSE");
|
|
142
|
+
});
|
|
143
|
+
req.on("listening", () => {
|
|
144
|
+
// Port is free — no daemon web server
|
|
145
|
+
req.close();
|
|
146
|
+
resolve(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLanIp(): string | null {
|
|
152
|
+
const interfaces = networkInterfaces();
|
|
153
|
+
for (const name of Object.keys(interfaces)) {
|
|
154
|
+
const addrs = interfaces[name];
|
|
155
|
+
if (!addrs) continue;
|
|
156
|
+
for (const addr of addrs) {
|
|
157
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
158
|
+
return addr.address;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -- Session state -----------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
interface ParaSessionState {
|
|
168
|
+
lastScope: ProjectScope;
|
|
169
|
+
sessionFile: string | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ENTRY_TYPE = "pi-para-state";
|
|
173
|
+
|
|
174
|
+
function reconstructState(ctx: ExtensionContext): ParaSessionState | null {
|
|
175
|
+
let latest: ParaSessionState | null = null;
|
|
176
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
177
|
+
if (entry.type === "custom" && entry.customType === ENTRY_TYPE) {
|
|
178
|
+
latest = entry.data as ParaSessionState;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return latest;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -- Extension entry point ---------------------------------------------------
|
|
185
|
+
|
|
186
|
+
export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
187
|
+
const config = await loadConfig();
|
|
188
|
+
const wikiDir = config.wikiDir;
|
|
189
|
+
|
|
190
|
+
let store: QMDStore | null = null;
|
|
191
|
+
let currentScope: ProjectScope | null = null;
|
|
192
|
+
let storeDisabled = false;
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
const getScope = (): ProjectScope => {
|
|
196
|
+
if (!currentScope) {
|
|
197
|
+
return { name: "unknown", include: ["unknown"], exclude: [], source: "dirname" };
|
|
198
|
+
}
|
|
199
|
+
return currentScope;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const setScope = (scope: ProjectScope): void => {
|
|
203
|
+
currentScope = scope;
|
|
204
|
+
markContextDirty();
|
|
205
|
+
pi.appendEntry(ENTRY_TYPE, {
|
|
206
|
+
lastScope: scope,
|
|
207
|
+
sessionFile: null,
|
|
208
|
+
} satisfies ParaSessionState);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const getConfig = (): ContextConfig => ({
|
|
212
|
+
contextMaxTokens: config.contextMaxTokens,
|
|
213
|
+
contextIncludeSchema: config.contextIncludeSchema,
|
|
214
|
+
contextIncludeIndex: config.contextIncludeIndex,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Lazy retry: if the store failed at startup, try reopening on first tool use.
|
|
218
|
+
// This handles cases where the db was temporarily locked or had transient errors.
|
|
219
|
+
let storeRetrying = false;
|
|
220
|
+
async function retryStoreOpen(): Promise<boolean> {
|
|
221
|
+
if (storeRetrying) return false;
|
|
222
|
+
storeRetrying = true;
|
|
223
|
+
try {
|
|
224
|
+
store = await openStore(wikiDir);
|
|
225
|
+
storeDisabled = false;
|
|
226
|
+
markContextDirty(); // rebuild context now that store works
|
|
227
|
+
return true;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
} finally {
|
|
231
|
+
storeRetrying = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Store proxy — delegates to real store once opened, with lazy retry on failure
|
|
236
|
+
const storeProxy = new Proxy({} as QMDStore, {
|
|
237
|
+
get(_target, prop) {
|
|
238
|
+
if (!store) {
|
|
239
|
+
if (storeDisabled) {
|
|
240
|
+
// Return an async function that retries opening the store first
|
|
241
|
+
return async (...args: unknown[]) => {
|
|
242
|
+
const reopened = await retryStoreOpen();
|
|
243
|
+
if (reopened && store) {
|
|
244
|
+
const method = (store as unknown as Record<string, unknown>)[prop as string];
|
|
245
|
+
if (typeof method === "function") {
|
|
246
|
+
return (method as Function).apply(store, args);
|
|
247
|
+
}
|
|
248
|
+
return method;
|
|
249
|
+
}
|
|
250
|
+
throw new Error("Wiki search is disabled: qmd store failed to open.");
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return (..._args: unknown[]) => {
|
|
254
|
+
throw new Error("Wiki store not initialized yet.");
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return (store as unknown as Record<string, unknown>)[prop as string];
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Register tools, context injection, commands
|
|
262
|
+
registerTools(pi, wikiDir, storeProxy, getScope, markContextDirty, () => config.searchGraphBoost);
|
|
263
|
+
setupContextInjection(pi, wikiDir, storeProxy, () => currentScope, getConfig);
|
|
264
|
+
registerCommands(pi, wikiDir, storeProxy, getScope, setScope);
|
|
265
|
+
|
|
266
|
+
// -- session_start ---------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
269
|
+
ctx.ui.setStatus("pi-para", "wiki: loading...");
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await initWiki(wikiDir);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
ctx.ui.notify(
|
|
275
|
+
`Wiki init failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
276
|
+
"error",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
ctx.ui.setStatus("pi-para", "wiki: indexing...");
|
|
281
|
+
storeDisabled = false;
|
|
282
|
+
try {
|
|
283
|
+
store = await openStore(wikiDir);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
store = null;
|
|
286
|
+
storeDisabled = true;
|
|
287
|
+
ctx.ui.notify(
|
|
288
|
+
`Wiki search disabled: ${err instanceof Error ? err.message : String(err)}`,
|
|
289
|
+
"warning",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
currentScope = await detectScope(ctx.cwd);
|
|
294
|
+
|
|
295
|
+
const savedState = reconstructState(ctx);
|
|
296
|
+
if (savedState?.lastScope.source === "config") {
|
|
297
|
+
currentScope = savedState.lastScope;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
pi.appendEntry(ENTRY_TYPE, {
|
|
301
|
+
lastScope: currentScope,
|
|
302
|
+
sessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
303
|
+
} satisfies ParaSessionState);
|
|
304
|
+
|
|
305
|
+
// Check if daemon's web wiki server is running (don't start our own)
|
|
306
|
+
if (config.webWiki.enabled) {
|
|
307
|
+
const port = config.webWiki.port;
|
|
308
|
+
try {
|
|
309
|
+
const alive = await checkWebServerAlive(port);
|
|
310
|
+
if (alive) {
|
|
311
|
+
const lanIp = getLanIp();
|
|
312
|
+
const url = lanIp ? `http://${lanIp}:${port}` : `http://localhost:${port}`;
|
|
313
|
+
ctx.ui.setStatus("pi-para", `wiki: ${url}`);
|
|
314
|
+
} else {
|
|
315
|
+
ctx.ui.setStatus("pi-para", "wiki: ready (start daemon for web UI)");
|
|
316
|
+
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
320
|
+
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
324
|
+
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// -- session_tree ----------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
331
|
+
const savedState = reconstructState(ctx);
|
|
332
|
+
if (savedState?.lastScope.source === "config") {
|
|
333
|
+
currentScope = savedState.lastScope;
|
|
334
|
+
} else {
|
|
335
|
+
currentScope = await detectScope(ctx.cwd);
|
|
336
|
+
}
|
|
337
|
+
markContextDirty();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// -- session_compact: register session for daemon capture on compaction ----
|
|
341
|
+
|
|
342
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
343
|
+
// Compaction means the session has accumulated enough content that details
|
|
344
|
+
// are about to be compressed/lost. Perfect time to capture knowledge.
|
|
345
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
346
|
+
if (sessionFile) {
|
|
347
|
+
try {
|
|
348
|
+
const registry = join(wikiDir, ".completed-sessions");
|
|
349
|
+
const entry = `${new Date().toISOString()}|${sessionFile}\n`;
|
|
350
|
+
await appendFile(registry, entry);
|
|
351
|
+
ctx.ui.setStatus("pi-para", "wiki: capture queued (compaction)");
|
|
352
|
+
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
353
|
+
} catch {
|
|
354
|
+
// Non-fatal
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// -- session_shutdown: register session for daemon, close store ------------
|
|
360
|
+
|
|
361
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
362
|
+
// 1. Register session as completed (for daemon to process later)
|
|
363
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
364
|
+
if (sessionFile) {
|
|
365
|
+
try {
|
|
366
|
+
const registry = join(wikiDir, ".completed-sessions");
|
|
367
|
+
const entry = `${new Date().toISOString()}|${sessionFile}\n`;
|
|
368
|
+
await appendFile(registry, entry);
|
|
369
|
+
} catch {
|
|
370
|
+
// Non-fatal — daemon can also discover sessions via filesystem scan
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 2. Close the store (instant)
|
|
375
|
+
if (store) {
|
|
376
|
+
try {
|
|
377
|
+
await closeStore(store);
|
|
378
|
+
} catch {
|
|
379
|
+
// Non-fatal
|
|
380
|
+
}
|
|
381
|
+
store = null;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
package/src/ingest.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest pipeline — processes sources and integrates knowledge into the wiki.
|
|
3
|
+
*
|
|
4
|
+
* Resolves source content (URL, file, text), saves raw copy to the vault,
|
|
5
|
+
* and returns structured data for the caller (tool or standalone agent)
|
|
6
|
+
* to pass to the LLM for synthesis into wiki pages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import type { QMDStore } from "qmd-engine";
|
|
11
|
+
import type { ParaCategory, PageRef, LogEntry } from "./wiki.js";
|
|
12
|
+
import { readSchema, readIndex } from "./wiki.js";
|
|
13
|
+
import { saveRawSource } from "./raw.js";
|
|
14
|
+
import type { ProjectScope } from "./scope.js";
|
|
15
|
+
|
|
16
|
+
// -- Types ------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface IngestOptions {
|
|
19
|
+
source: string; // URL, file path, or raw text
|
|
20
|
+
sourceType?: "url" | "file" | "text"; // auto-detected if omitted
|
|
21
|
+
scope?: string[]; // override auto-detected scope
|
|
22
|
+
category?: ParaCategory; // hint for PARA classification
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IngestReport {
|
|
26
|
+
pagesCreated: PageRef[];
|
|
27
|
+
pagesUpdated: PageRef[];
|
|
28
|
+
logEntry: LogEntry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Structure returned by LLM during ingest. */
|
|
32
|
+
export interface IngestResult {
|
|
33
|
+
newPages: Array<{
|
|
34
|
+
category: ParaCategory;
|
|
35
|
+
slug: string;
|
|
36
|
+
title: string;
|
|
37
|
+
scope: string[];
|
|
38
|
+
tags: string[];
|
|
39
|
+
body: string;
|
|
40
|
+
}>;
|
|
41
|
+
updatedPages: Array<{
|
|
42
|
+
category: ParaCategory;
|
|
43
|
+
slug: string;
|
|
44
|
+
appendOrReplace: "append" | "replace";
|
|
45
|
+
body: string;
|
|
46
|
+
}>;
|
|
47
|
+
indexUpdate: string;
|
|
48
|
+
logSummary: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolved source data from the ingest pipeline. */
|
|
52
|
+
export interface ResolvedSource {
|
|
53
|
+
/** Original source string (URL, path, or text) */
|
|
54
|
+
source: string;
|
|
55
|
+
/** Detected or explicit source type */
|
|
56
|
+
sourceType: "url" | "file" | "text";
|
|
57
|
+
/** Fetched/read source content */
|
|
58
|
+
content: string;
|
|
59
|
+
/** Path in raw/ vault (undefined for inline text) */
|
|
60
|
+
rawPath: string | undefined;
|
|
61
|
+
/** Schema.md content for LLM context */
|
|
62
|
+
schema: string;
|
|
63
|
+
/** Index.md content for LLM context */
|
|
64
|
+
index: string;
|
|
65
|
+
/** Scope tags to assign */
|
|
66
|
+
scopeTags: string[];
|
|
67
|
+
/** Scope name */
|
|
68
|
+
scopeName: string;
|
|
69
|
+
/** Category hint (if provided) */
|
|
70
|
+
categoryHint: ParaCategory | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// -- Source resolution -------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/** Detect source type from the source string. */
|
|
76
|
+
export function detectSourceType(source: string): "url" | "file" | "text" {
|
|
77
|
+
if (/^https?:\/\//i.test(source)) return "url";
|
|
78
|
+
if (/^\/|^\.\/|^~\/|^[a-zA-Z]:\\/.test(source)) return "file";
|
|
79
|
+
// Heuristic: if it contains newlines or is very long, treat as text
|
|
80
|
+
if (source.includes("\n") || source.length > 500) return "text";
|
|
81
|
+
// Check for file-like paths (has extension)
|
|
82
|
+
if (/\.\w{1,10}$/.test(source) && !source.includes(" ")) return "file";
|
|
83
|
+
return "text";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Fetch content from a URL. */
|
|
87
|
+
export async function fetchUrl(url: string): Promise<string> {
|
|
88
|
+
const response = await fetch(url);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
91
|
+
}
|
|
92
|
+
return response.text();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Read content from a file path. */
|
|
96
|
+
export async function readFileSource(filePath: string): Promise<string> {
|
|
97
|
+
const resolved = filePath.replace(/^~\//, `${process.env.HOME ?? ""}/`);
|
|
98
|
+
return readFile(resolved, "utf-8");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// -- Main pipeline -----------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a source and prepare it for LLM synthesis.
|
|
105
|
+
*
|
|
106
|
+
* This is the reusable library function. It:
|
|
107
|
+
* 1. Detects or uses the explicit source type
|
|
108
|
+
* 2. Fetches URL / reads file / uses text directly
|
|
109
|
+
* 3. Saves a raw copy to the vault (for non-text sources)
|
|
110
|
+
* 4. Reads schema.md and index.md for LLM context
|
|
111
|
+
* 5. Returns everything the caller needs to build an LLM prompt
|
|
112
|
+
*
|
|
113
|
+
* The caller (tool or standalone agent) assembles the prompt and
|
|
114
|
+
* passes it to the LLM, which calls wiki_write to create pages.
|
|
115
|
+
*/
|
|
116
|
+
export async function resolveSource(
|
|
117
|
+
wikiDir: string,
|
|
118
|
+
options: IngestOptions,
|
|
119
|
+
scope: ProjectScope,
|
|
120
|
+
): Promise<ResolvedSource> {
|
|
121
|
+
const sourceType = options.sourceType ?? detectSourceType(options.source);
|
|
122
|
+
|
|
123
|
+
// Resolve source content
|
|
124
|
+
let content: string;
|
|
125
|
+
switch (sourceType) {
|
|
126
|
+
case "url":
|
|
127
|
+
content = await fetchUrl(options.source);
|
|
128
|
+
break;
|
|
129
|
+
case "file":
|
|
130
|
+
content = await readFileSource(options.source);
|
|
131
|
+
break;
|
|
132
|
+
case "text":
|
|
133
|
+
content = options.source;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Save raw source (skip for inline text — it's ephemeral)
|
|
138
|
+
let rawPath: string | undefined;
|
|
139
|
+
if (sourceType !== "text") {
|
|
140
|
+
rawPath = await saveRawSource(wikiDir, {
|
|
141
|
+
type: sourceType,
|
|
142
|
+
content,
|
|
143
|
+
originalPath: options.source,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Read schema and index for LLM context
|
|
148
|
+
const schema = await readSchema(wikiDir);
|
|
149
|
+
const index = await readIndex(wikiDir);
|
|
150
|
+
|
|
151
|
+
// Build scope info
|
|
152
|
+
const scopeTags = options.scope ?? scope.include;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
source: options.source,
|
|
156
|
+
sourceType,
|
|
157
|
+
content,
|
|
158
|
+
rawPath,
|
|
159
|
+
schema,
|
|
160
|
+
index,
|
|
161
|
+
scopeTags,
|
|
162
|
+
scopeName: scope.name,
|
|
163
|
+
categoryHint: options.category,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Maximum source content length before truncation. */
|
|
168
|
+
export const MAX_SOURCE_LENGTH = 50_000;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Truncate source content if it exceeds the limit.
|
|
172
|
+
* Returns the truncated content with a marker.
|
|
173
|
+
*/
|
|
174
|
+
export function truncateSource(content: string, maxLength: number = MAX_SOURCE_LENGTH): string {
|
|
175
|
+
if (content.length <= maxLength) return content;
|
|
176
|
+
return content.slice(0, maxLength) + "\n\n[... truncated]";
|
|
177
|
+
}
|