pi-para 0.6.7 → 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/ARCHITECTURE.md +4 -6
- package/CHANGELOG.md +14 -0
- package/CONFIGURATION.md +1 -16
- package/README.md +2 -13
- package/SETUP.md +1 -7
- package/STRUCTURE.md +7 -18
- package/dist/cli-legacy.d.ts +2 -1
- package/dist/cli-legacy.d.ts.map +1 -1
- package/dist/cli-legacy.js +13 -25
- package/dist/cli-legacy.js.map +1 -1
- package/dist/cli.d.ts +2 -24
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -271
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +1 -28
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +1 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -64
- package/dist/index.js.map +1 -1
- package/dist/link-utils.d.ts +1 -1
- package/dist/link-utils.js +1 -1
- package/dist/settings.d.ts +0 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -11
- package/dist/settings.js.map +1 -1
- package/dist/tools/edit-read.d.ts +18 -0
- package/dist/tools/edit-read.d.ts.map +1 -0
- package/dist/tools/edit-read.js +161 -0
- package/dist/tools/edit-read.js.map +1 -0
- package/dist/tools/ingest.d.ts +12 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +48 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/lint.d.ts +10 -0
- package/dist/tools/lint.d.ts.map +1 -0
- package/dist/tools/lint.js +57 -0
- package/dist/tools/lint.js.map +1 -0
- package/dist/tools/migrate.d.ts +6 -0
- package/dist/tools/migrate.d.ts.map +1 -0
- package/dist/tools/migrate.js +36 -0
- package/dist/tools/migrate.js.map +1 -0
- package/dist/tools/move.d.ts +9 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +61 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/query.d.ts +12 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +47 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/schemas.d.ts +109 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +80 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/shared.d.ts +14 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +25 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/summarize.d.ts +8 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +82 -0
- package/dist/tools/summarize.js.map +1 -0
- package/dist/tools/write.d.ts +23 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +215 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/tools.d.ts +1 -8
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -795
- package/dist/tools.js.map +1 -1
- package/package.json +2 -6
- package/src/cli-legacy.ts +14 -26
- package/src/cli.ts +29 -293
- package/src/commands.ts +0 -21
- package/src/config.ts +1 -6
- package/src/index.ts +5 -66
- package/src/link-utils.ts +1 -1
- package/src/settings.ts +0 -14
- package/src/tools/edit-read.ts +193 -0
- package/src/tools/ingest.ts +63 -0
- package/src/tools/lint.ts +74 -0
- package/src/tools/migrate.ts +44 -0
- package/src/tools/move.ts +79 -0
- package/src/tools/query.ts +63 -0
- package/src/tools/schemas.ts +157 -0
- package/src/tools/shared.ts +35 -0
- package/src/tools/summarize.ts +89 -0
- package/src/tools/write.ts +252 -0
- package/src/tools.ts +18 -1046
- package/dist/daemon.d.ts +0 -49
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -226
- package/dist/daemon.js.map +0 -1
- package/dist/link-discovery.d.ts +0 -40
- package/dist/link-discovery.d.ts.map +0 -1
- package/dist/link-discovery.js +0 -122
- package/dist/link-discovery.js.map +0 -1
- package/dist/maintainer.d.ts +0 -24
- package/dist/maintainer.d.ts.map +0 -1
- package/dist/maintainer.js +0 -358
- package/dist/maintainer.js.map +0 -1
- package/dist/watcher.d.ts +0 -25
- package/dist/watcher.d.ts.map +0 -1
- package/dist/watcher.js +0 -84
- package/dist/watcher.js.map +0 -1
- package/dist/webui/client/dist/index.html +0 -1885
- package/dist/webui/server.d.ts +0 -31
- package/dist/webui/server.d.ts.map +0 -1
- package/dist/webui/server.js +0 -566
- package/dist/webui/server.js.map +0 -1
- package/scripts/build-assets.mjs +0 -17
- package/src/daemon.ts +0 -288
- package/src/link-discovery.ts +0 -170
- package/src/maintainer.ts +0 -404
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +0 -198
- package/src/python/rlm_capture.py +0 -215
- package/src/watcher.ts +0 -101
- package/src/webui/client/dist/index.html +0 -1885
- package/src/webui/server.ts +0 -765
package/src/config.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface ParaUserConfig {
|
|
|
52
52
|
autoFix: boolean;
|
|
53
53
|
staleDays: number;
|
|
54
54
|
};
|
|
55
|
+
/** Deprecated compatibility input. Preserved when loading but ignored by the runtime. */
|
|
55
56
|
webWiki: {
|
|
56
57
|
enabled: boolean;
|
|
57
58
|
host: string;
|
|
@@ -71,7 +72,6 @@ export interface LegacyParaRuntimeConfig {
|
|
|
71
72
|
searchIncludeArchives: boolean;
|
|
72
73
|
searchGraphBoost: boolean;
|
|
73
74
|
daemonModel: string | null;
|
|
74
|
-
webWiki: { enabled: boolean; host: string; port: number };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface LoadedParaConfig {
|
|
@@ -292,11 +292,6 @@ export function toLegacyRuntimeConfig(config: ParaUserConfig): LegacyParaRuntime
|
|
|
292
292
|
searchIncludeArchives: false,
|
|
293
293
|
searchGraphBoost: config.context.searchGraphBoost,
|
|
294
294
|
daemonModel,
|
|
295
|
-
webWiki: {
|
|
296
|
-
enabled: config.webWiki.enabled,
|
|
297
|
-
host: config.webWiki.host,
|
|
298
|
-
port: config.webWiki.port,
|
|
299
|
-
},
|
|
300
295
|
};
|
|
301
296
|
}
|
|
302
297
|
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { homedir
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
13
|
|
|
14
14
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import type { QMDStore } from "qmd-engine";
|
|
@@ -22,7 +22,6 @@ import { registerTools } from "./tools.js";
|
|
|
22
22
|
import { setupContextInjection, markContextDirty } from "./context.js";
|
|
23
23
|
import type { ContextConfig } from "./context.js";
|
|
24
24
|
import { registerCommands } from "./commands.js";
|
|
25
|
-
import { createServer } from "node:http";
|
|
26
25
|
import { startWikiScheduler, stopWikiScheduler } from "./scheduler/index.js";
|
|
27
26
|
import {
|
|
28
27
|
appendCompletedSession,
|
|
@@ -59,12 +58,6 @@ interface ParaConfig {
|
|
|
59
58
|
/** Legacy capture LLM: "provider/model-id" (e.g. "anthropic/claude-sonnet-4").
|
|
60
59
|
* New config stores this as models.capture; legacy field remains for migration. */
|
|
61
60
|
daemonModel: string | null;
|
|
62
|
-
/** Web wiki UI settings */
|
|
63
|
-
webWiki: {
|
|
64
|
-
enabled: boolean;
|
|
65
|
-
host: string;
|
|
66
|
-
port: number;
|
|
67
|
-
};
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
function getDefaultConfig(): ParaConfig {
|
|
@@ -79,11 +72,6 @@ function getDefaultConfig(): ParaConfig {
|
|
|
79
72
|
searchIncludeArchives: false,
|
|
80
73
|
searchGraphBoost: true,
|
|
81
74
|
daemonModel: null,
|
|
82
|
-
webWiki: {
|
|
83
|
-
enabled: false,
|
|
84
|
-
host: "0.0.0.0",
|
|
85
|
-
port: 10973,
|
|
86
|
-
},
|
|
87
75
|
};
|
|
88
76
|
}
|
|
89
77
|
|
|
@@ -107,43 +95,10 @@ async function loadConfig(): Promise<LoadedRuntimeConfig> {
|
|
|
107
95
|
}
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
// -- LAN IP detection --------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
/** Check if the optional web wiki server is alive on the given port. */
|
|
113
|
-
function checkWebServerAlive(port: number): Promise<boolean> {
|
|
114
|
-
return new Promise((resolve) => {
|
|
115
|
-
const req = createServer().listen(port, "127.0.0.1");
|
|
116
|
-
req.on("error", (err: NodeJS.ErrnoException) => {
|
|
117
|
-
req.close();
|
|
118
|
-
// EADDRINUSE means something is already listening — daemon is running
|
|
119
|
-
resolve(err.code === "EADDRINUSE");
|
|
120
|
-
});
|
|
121
|
-
req.on("listening", () => {
|
|
122
|
-
// Port is free — no daemon web server
|
|
123
|
-
req.close();
|
|
124
|
-
resolve(false);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
98
|
function isPrintModeProcess(argv = process.argv): boolean {
|
|
130
99
|
return argv.includes("-p") || argv.includes("--print") || argv.includes("--mode") && argv[argv.indexOf("--mode") + 1] === "json";
|
|
131
100
|
}
|
|
132
101
|
|
|
133
|
-
function getLanIp(): string | null {
|
|
134
|
-
const interfaces = networkInterfaces();
|
|
135
|
-
for (const name of Object.keys(interfaces)) {
|
|
136
|
-
const addrs = interfaces[name];
|
|
137
|
-
if (!addrs) continue;
|
|
138
|
-
for (const addr of addrs) {
|
|
139
|
-
if (addr.family === "IPv4" && !addr.internal) {
|
|
140
|
-
return addr.address;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
102
|
// -- Session state -----------------------------------------------------------
|
|
148
103
|
|
|
149
104
|
interface ParaSessionState {
|
|
@@ -370,27 +325,11 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
370
325
|
}
|
|
371
326
|
}
|
|
372
327
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const port = config.webWiki.port;
|
|
376
|
-
try {
|
|
377
|
-
const alive = await checkWebServerAlive(port);
|
|
378
|
-
if (alive) {
|
|
379
|
-
const lanIp = getLanIp();
|
|
380
|
-
const url = lanIp ? `http://${lanIp}:${port}` : `http://localhost:${port}`;
|
|
381
|
-
ctx.ui.setStatus("pi-para", `wiki: ${url}`);
|
|
382
|
-
} else {
|
|
383
|
-
ctx.ui.setStatus("pi-para", "wiki: ready (web UI disabled)");
|
|
384
|
-
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
385
|
-
}
|
|
386
|
-
} catch {
|
|
387
|
-
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
388
|
-
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
392
|
-
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
328
|
+
if (loadedConfig.userConfig?.webWiki.enabled && !printMode) {
|
|
329
|
+
ctx.ui.notify("webWiki configuration is ignored because Web Wiki was removed in 0.7.", "warning");
|
|
393
330
|
}
|
|
331
|
+
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
332
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
394
333
|
});
|
|
395
334
|
|
|
396
335
|
// -- session_tree ----------------------------------------------------------
|
package/src/link-utils.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared wikilink utilities — extraction, auto-linking, and sync.
|
|
3
3
|
*
|
|
4
4
|
* Centralizes the [[wikilink]] logic that was duplicated across
|
|
5
|
-
* tools.ts, lint.ts,
|
|
5
|
+
* tools.ts, lint.ts, and processor.ts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// -- Wikilink extraction ----------------------------------------------------
|
package/src/settings.ts
CHANGED
|
@@ -54,20 +54,6 @@ export function setCaptureModel(config: ParaUserConfig, provider: string, model:
|
|
|
54
54
|
return profile;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function setWebWikiEnabled(config: ParaUserConfig, enabled: boolean): void {
|
|
58
|
-
config.webWiki.enabled = enabled;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function setWebWikiHost(config: ParaUserConfig, host: string): void {
|
|
62
|
-
config.webWiki.host = host.trim() || config.webWiki.host;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function setWebWikiPort(config: ParaUserConfig, value: string): number {
|
|
66
|
-
const parsed = parsePositiveInteger(value, config.webWiki.port);
|
|
67
|
-
config.webWiki.port = parsed;
|
|
68
|
-
return parsed;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
57
|
export function setProviderProfileField(
|
|
72
58
|
profile: ProviderCredentialRef,
|
|
73
59
|
field: "provider" | "baseUrl" | "model" | "apiFormat" | "credentialRef",
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { QMDStore } from "qmd-engine";
|
|
3
|
+
import { formatFreshness } from "../query.js";
|
|
4
|
+
import { autoLinkSlugs, syncFrontmatterLinks } from "../link-utils.js";
|
|
5
|
+
import { normalizeTags, normalizeScopes } from "../tag-registry.js";
|
|
6
|
+
import { redactSecrets } from "../redact.js";
|
|
7
|
+
import { appendLog, gitCommit, listPages, readIndex, readPage, writePage, type WikiPage } from "../wiki.js";
|
|
8
|
+
import type { WikiReadDetails, WikiWriteDetails } from "./schemas.js";
|
|
9
|
+
import { parsePagePath, scheduleWikiMaintenance } from "./shared.js";
|
|
10
|
+
|
|
11
|
+
// -- Factory: wiki_read execute ----------------------------------------------
|
|
12
|
+
|
|
13
|
+
export function createEditExecute(
|
|
14
|
+
wikiDir: string,
|
|
15
|
+
store: QMDStore,
|
|
16
|
+
markDirty: () => void,
|
|
17
|
+
) {
|
|
18
|
+
return async (
|
|
19
|
+
params: {
|
|
20
|
+
path: string;
|
|
21
|
+
edits: Array<{ oldText: string; newText: string }>;
|
|
22
|
+
title?: string;
|
|
23
|
+
scope?: string[];
|
|
24
|
+
tags?: string[];
|
|
25
|
+
logSummary?: string;
|
|
26
|
+
},
|
|
27
|
+
): Promise<AgentToolResult<WikiWriteDetails>> => {
|
|
28
|
+
const parsed = parsePagePath(params.path);
|
|
29
|
+
if (!parsed) {
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: `Invalid page path: ${params.path}` }],
|
|
32
|
+
details: { pagesWritten: [], pagesSkipped: [params.path], indexUpdated: false, logAppended: false },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const existing = await readPage(wikiDir, parsed.category, parsed.slug);
|
|
37
|
+
const pagePath = `${parsed.category}/${parsed.slug}`;
|
|
38
|
+
if (!existing) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Page not found: ${pagePath}` }],
|
|
41
|
+
details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (params.edits.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: "No edits supplied." }],
|
|
48
|
+
details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Atomic validation: all oldText strings must exist exactly once before writing.
|
|
53
|
+
const errors: string[] = [];
|
|
54
|
+
for (const edit of params.edits) {
|
|
55
|
+
const first = existing.body.indexOf(edit.oldText);
|
|
56
|
+
const last = existing.body.lastIndexOf(edit.oldText);
|
|
57
|
+
if (first === -1) {
|
|
58
|
+
errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
|
|
59
|
+
} else if (first !== last) {
|
|
60
|
+
errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (errors.length > 0) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: `No changes written.\n${errors.join("\n")}` }],
|
|
66
|
+
details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let editedBody = existing.body;
|
|
71
|
+
for (const edit of params.edits) {
|
|
72
|
+
editedBody = editedBody.replace(edit.oldText, edit.newText);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Surgical edit path: do not scan/autolink/rebuild/reindex the whole wiki.
|
|
76
|
+
// wiki_edit is often called mid-task and must stay fast; broader maintenance
|
|
77
|
+
// can run via wiki_lint/rebuildIndex or on startup.
|
|
78
|
+
const linkedBody = redactSecrets(editedBody).text;
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const rawScope = params.scope ?? existing.frontmatter.scope;
|
|
81
|
+
const newScope = normalizeScopes(rawScope);
|
|
82
|
+
const rawTags = params.tags ?? existing.frontmatter.tags;
|
|
83
|
+
const updatedPage: WikiPage = {
|
|
84
|
+
category: parsed.category,
|
|
85
|
+
slug: parsed.slug,
|
|
86
|
+
frontmatter: {
|
|
87
|
+
...existing.frontmatter,
|
|
88
|
+
title: params.title ?? existing.frontmatter.title,
|
|
89
|
+
scope: newScope,
|
|
90
|
+
tags: normalizeTags(rawTags, newScope),
|
|
91
|
+
updated: now,
|
|
92
|
+
links: syncFrontmatterLinks(linkedBody),
|
|
93
|
+
},
|
|
94
|
+
body: linkedBody,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await writePage(wikiDir, updatedPage);
|
|
98
|
+
if (params.logSummary) {
|
|
99
|
+
await appendLog(wikiDir, {
|
|
100
|
+
date: now.split("T")[0],
|
|
101
|
+
operation: "edit",
|
|
102
|
+
summary: params.logSummary,
|
|
103
|
+
pages: [pagePath],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
markDirty();
|
|
107
|
+
await gitCommit(wikiDir, params.logSummary ?? `wiki_edit: ${pagePath}`);
|
|
108
|
+
scheduleWikiMaintenance(wikiDir, store, markDirty);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: `Edited ${pagePath}. Scheduled background index/search refresh.` }],
|
|
112
|
+
details: {
|
|
113
|
+
pagesWritten: [pagePath],
|
|
114
|
+
pagesSkipped: [],
|
|
115
|
+
indexUpdated: false,
|
|
116
|
+
logAppended: !!params.logSummary,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createReadExecute(wikiDir: string) {
|
|
123
|
+
return async (
|
|
124
|
+
params: { path: string },
|
|
125
|
+
): Promise<AgentToolResult<WikiReadDetails>> => {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const normalizedPath = params.path.trim().toLowerCase().replace(/^\/+/, "");
|
|
128
|
+
|
|
129
|
+
if (normalizedPath === "index" || normalizedPath === "index.md" || normalizedPath === "wiki-index") {
|
|
130
|
+
const index = await readIndex(wikiDir);
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: index }],
|
|
133
|
+
details: { found: true, path: "index.md" },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Try parsing as category/slug path
|
|
138
|
+
const parsed = parsePagePath(params.path);
|
|
139
|
+
if (parsed) {
|
|
140
|
+
const page = await readPage(wikiDir, parsed.category, parsed.slug);
|
|
141
|
+
if (page) {
|
|
142
|
+
const fm = page.frontmatter;
|
|
143
|
+
const freshness = formatFreshness(fm.updated, now);
|
|
144
|
+
const header = [
|
|
145
|
+
`# ${fm.title}`,
|
|
146
|
+
`PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
|
|
147
|
+
`Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
|
|
148
|
+
fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
|
|
149
|
+
fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
|
|
150
|
+
]
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join("\n");
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: `${header}\n\n${page.body}` }],
|
|
156
|
+
details: { found: true, path: `${parsed.category}/${parsed.slug}` },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Try finding by title (case-insensitive search across all pages)
|
|
162
|
+
const allPages = await listPages(wikiDir);
|
|
163
|
+
const titleLower = params.path.toLowerCase();
|
|
164
|
+
for (const ref of allPages) {
|
|
165
|
+
if (ref.title.toLowerCase() === titleLower || ref.slug === titleLower) {
|
|
166
|
+
const page = await readPage(wikiDir, ref.category, ref.slug);
|
|
167
|
+
if (page) {
|
|
168
|
+
const fm = page.frontmatter;
|
|
169
|
+
const freshness = formatFreshness(fm.updated, now);
|
|
170
|
+
const header = [
|
|
171
|
+
`# ${fm.title}`,
|
|
172
|
+
`PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
|
|
173
|
+
`Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
|
|
174
|
+
fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
|
|
175
|
+
fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join("\n");
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: `${header}\n\n${page.body}` }],
|
|
182
|
+
details: { found: true, path: `${ref.category}/${ref.slug}` },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: `Page not found: "${params.path}"` }],
|
|
190
|
+
details: { found: false },
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { QMDStore } from "qmd-engine";
|
|
3
|
+
import type { ProjectScope } from "../scope.js";
|
|
4
|
+
import type { ParaCategory } from "../wiki.js";
|
|
5
|
+
import { resolveSource, truncateSource } from "../ingest.js";
|
|
6
|
+
import { INGEST_PROMPT } from "../templates/prompts.js";
|
|
7
|
+
import type { WikiIngestDetails } from "./schemas.js";
|
|
8
|
+
|
|
9
|
+
// -- Factory: wiki_ingest execute --------------------------------------------
|
|
10
|
+
|
|
11
|
+
export function createIngestExecute(
|
|
12
|
+
wikiDir: string,
|
|
13
|
+
_store: QMDStore,
|
|
14
|
+
getScope: () => ProjectScope,
|
|
15
|
+
) {
|
|
16
|
+
return async (
|
|
17
|
+
params: { source: string; sourceType?: "url" | "file" | "text"; category?: ParaCategory; scope?: string[] },
|
|
18
|
+
): Promise<AgentToolResult<WikiIngestDetails>> => {
|
|
19
|
+
const scope = getScope();
|
|
20
|
+
const resolved = await resolveSource(wikiDir, {
|
|
21
|
+
source: params.source,
|
|
22
|
+
sourceType: params.sourceType,
|
|
23
|
+
scope: params.scope,
|
|
24
|
+
category: params.category,
|
|
25
|
+
}, scope);
|
|
26
|
+
|
|
27
|
+
const categoryHint = resolved.categoryHint
|
|
28
|
+
? `\nSuggested PARA category: ${resolved.categoryHint}`
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
// Assemble the tool result with instructions for the LLM
|
|
32
|
+
const toolResultText = [
|
|
33
|
+
INGEST_PROMPT,
|
|
34
|
+
"",
|
|
35
|
+
`Current scope: ${resolved.scopeName} (tags: ${resolved.scopeTags.join(", ")})`,
|
|
36
|
+
categoryHint,
|
|
37
|
+
resolved.rawPath ? `Raw source saved to: ${resolved.rawPath}` : "",
|
|
38
|
+
"",
|
|
39
|
+
"<wiki-schema>",
|
|
40
|
+
resolved.schema,
|
|
41
|
+
"</wiki-schema>",
|
|
42
|
+
"",
|
|
43
|
+
"<wiki-index>",
|
|
44
|
+
resolved.index,
|
|
45
|
+
"</wiki-index>",
|
|
46
|
+
"",
|
|
47
|
+
"<source-content>",
|
|
48
|
+
truncateSource(resolved.content),
|
|
49
|
+
"</source-content>",
|
|
50
|
+
"",
|
|
51
|
+
"Now analyze the source content and use wiki_write to create/update pages, update the index, and log the operation.",
|
|
52
|
+
].join("\n");
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: toolResultText }],
|
|
56
|
+
details: {
|
|
57
|
+
sourceType: resolved.sourceType,
|
|
58
|
+
rawPath: resolved.rawPath,
|
|
59
|
+
sourceLength: resolved.content.length,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { QMDStore } from "qmd-engine";
|
|
3
|
+
import { lintWiki } from "../lint.js";
|
|
4
|
+
import { PARA_CATEGORIES } from "../wiki.js";
|
|
5
|
+
import { LINT_PROMPT } from "../templates/prompts.js";
|
|
6
|
+
import type { WikiLintDetails } from "./schemas.js";
|
|
7
|
+
import { scheduleWikiMaintenance } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
// -- Factory: wiki_lint execute ----------------------------------------------
|
|
10
|
+
|
|
11
|
+
export function createLintExecute(
|
|
12
|
+
wikiDir: string,
|
|
13
|
+
store: QMDStore,
|
|
14
|
+
markDirty: () => void,
|
|
15
|
+
getLintConfig?: () => { autoFix: boolean; staleDays: number },
|
|
16
|
+
) {
|
|
17
|
+
return async (
|
|
18
|
+
params: { autoFix?: boolean },
|
|
19
|
+
): Promise<AgentToolResult<WikiLintDetails>> => {
|
|
20
|
+
const configured = getLintConfig?.();
|
|
21
|
+
const report = await lintWiki(wikiDir, {
|
|
22
|
+
autoFix: params.autoFix ?? configured?.autoFix ?? true,
|
|
23
|
+
staleDays: configured?.staleDays ?? 90,
|
|
24
|
+
});
|
|
25
|
+
if (report.fixed.length > 0) {
|
|
26
|
+
scheduleWikiMaintenance(wikiDir, store, markDirty);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { issues, fixed, stats } = report;
|
|
30
|
+
|
|
31
|
+
// Format report for the LLM
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
|
|
34
|
+
parts.push("## Wiki Lint Report\n");
|
|
35
|
+
parts.push(`Total pages: ${stats.totalPages}`);
|
|
36
|
+
parts.push(
|
|
37
|
+
`By category: ${PARA_CATEGORIES.map((c) => `${c}: ${stats.byCategory[c]}`).join(", ")}`,
|
|
38
|
+
);
|
|
39
|
+
parts.push(`Total links: ${stats.totalLinks} (broken: ${stats.brokenLinks})`);
|
|
40
|
+
parts.push(`Orphan pages: ${stats.orphanPages}`);
|
|
41
|
+
if (stats.oldestPage) parts.push(`Oldest page: ${stats.oldestPage}`);
|
|
42
|
+
if (stats.newestPage) parts.push(`Newest page: ${stats.newestPage}`);
|
|
43
|
+
if (stats.lastIngest) parts.push(`Last ingest: ${stats.lastIngest}`);
|
|
44
|
+
|
|
45
|
+
if (fixed.length > 0) {
|
|
46
|
+
parts.push(`\n## Auto-fixed (${fixed.length})\n`);
|
|
47
|
+
for (const f of fixed) {
|
|
48
|
+
parts.push(`- [${f.severity}] ${f.page ?? ""}: ${f.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (issues.length > 0) {
|
|
53
|
+
parts.push(`\n## Remaining Issues (${issues.length})\n`);
|
|
54
|
+
for (const issue of issues) {
|
|
55
|
+
parts.push(
|
|
56
|
+
`- [${issue.severity}] ${issue.category}${issue.page ? ` (${issue.page})` : ""}: ${issue.message}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
parts.push("");
|
|
60
|
+
parts.push(LINT_PROMPT);
|
|
61
|
+
} else {
|
|
62
|
+
parts.push("\nNo remaining issues. Wiki is healthy.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
67
|
+
details: {
|
|
68
|
+
issueCount: issues.length,
|
|
69
|
+
fixedCount: fixed.length,
|
|
70
|
+
stats,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { CURRENT_SCHEMA_VERSION, migrateToLatest } from "../frontmatter.js";
|
|
3
|
+
import { listPages, readPage, writePage, type PageFrontmatter } from "../wiki.js";
|
|
4
|
+
|
|
5
|
+
// -- Factory: wiki_migrate execute -------------------------------------------
|
|
6
|
+
|
|
7
|
+
export function createMigrateExecute(
|
|
8
|
+
wikiDir: string,
|
|
9
|
+
) {
|
|
10
|
+
return async (): Promise<AgentToolResult<{ migratedCount: number; totalPages: number }>> => {
|
|
11
|
+
const allRefs = await listPages(wikiDir);
|
|
12
|
+
let migratedCount = 0;
|
|
13
|
+
|
|
14
|
+
for (const ref of allRefs) {
|
|
15
|
+
const page = await readPage(wikiDir, ref.category, ref.slug);
|
|
16
|
+
if (!page) continue;
|
|
17
|
+
|
|
18
|
+
const version = page.frontmatter.schemaVersion ?? 1;
|
|
19
|
+
if (version >= CURRENT_SCHEMA_VERSION) continue;
|
|
20
|
+
|
|
21
|
+
const rawObj = page.frontmatter as unknown as Record<string, unknown>;
|
|
22
|
+
const result = migrateToLatest(rawObj, page.body);
|
|
23
|
+
|
|
24
|
+
const migratedFm = result.fm as unknown as PageFrontmatter;
|
|
25
|
+
migratedFm.updated = new Date().toISOString();
|
|
26
|
+
await writePage(wikiDir, {
|
|
27
|
+
category: ref.category,
|
|
28
|
+
slug: ref.slug,
|
|
29
|
+
frontmatter: migratedFm,
|
|
30
|
+
body: result.body,
|
|
31
|
+
});
|
|
32
|
+
migratedCount++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const summary = migratedCount > 0
|
|
36
|
+
? `Migrated ${migratedCount} page(s) to schema version ${CURRENT_SCHEMA_VERSION}.`
|
|
37
|
+
: `All ${allRefs.length} page(s) already at schema version ${CURRENT_SCHEMA_VERSION}.`;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: summary }],
|
|
41
|
+
details: { migratedCount, totalPages: allRefs.length },
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { QMDStore } from "qmd-engine";
|
|
3
|
+
import { appendLog, gitCommit, movePage, readPage, rebuildIndex, type PageRef, type ParaCategory } from "../wiki.js";
|
|
4
|
+
import type { WikiMoveDetails } from "./schemas.js";
|
|
5
|
+
import { parsePagePath, scheduleWikiMaintenance } from "./shared.js";
|
|
6
|
+
|
|
7
|
+
// -- Factory: wiki_move execute ----------------------------------------------
|
|
8
|
+
|
|
9
|
+
export function createMoveExecute(
|
|
10
|
+
wikiDir: string,
|
|
11
|
+
store: QMDStore,
|
|
12
|
+
markDirty: () => void,
|
|
13
|
+
) {
|
|
14
|
+
return async (
|
|
15
|
+
params: { path: string; to: ParaCategory },
|
|
16
|
+
): Promise<AgentToolResult<WikiMoveDetails>> => {
|
|
17
|
+
const parsed = parsePagePath(params.path);
|
|
18
|
+
if (!parsed) {
|
|
19
|
+
throw new Error(`Invalid page path: "${params.path}". Expected format: category/slug`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const page = await readPage(wikiDir, parsed.category, parsed.slug);
|
|
23
|
+
if (!page) {
|
|
24
|
+
throw new Error(`Page not found: "${params.path}"`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (parsed.category === params.to) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: `Page "${params.path}" is already in ${params.to}. No move needed.`,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
details: { from: params.path, to: `${params.to}/${parsed.slug}` },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ref: PageRef = {
|
|
40
|
+
category: parsed.category,
|
|
41
|
+
slug: parsed.slug,
|
|
42
|
+
title: page.frontmatter.title,
|
|
43
|
+
path: `${parsed.category}/${parsed.slug}.md`,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
await movePage(wikiDir, ref, params.to);
|
|
47
|
+
|
|
48
|
+
// Rebuild index.md inline (fast, single-pass); defer QMD reindex to the
|
|
49
|
+
// debounced background maintenance queue like wiki_write/wiki_edit.
|
|
50
|
+
await rebuildIndex(wikiDir);
|
|
51
|
+
scheduleWikiMaintenance(wikiDir, store, markDirty);
|
|
52
|
+
markDirty();
|
|
53
|
+
|
|
54
|
+
// Git commit
|
|
55
|
+
await gitCommit(wikiDir, `move: ${page.frontmatter.title} → ${params.to}`);
|
|
56
|
+
|
|
57
|
+
// Log the move
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
await appendLog(wikiDir, {
|
|
60
|
+
date: now.split("T")[0],
|
|
61
|
+
operation: "move",
|
|
62
|
+
summary: `Moved ${page.frontmatter.title} from ${parsed.category} to ${params.to}`,
|
|
63
|
+
pages: [`${params.to}/${parsed.slug}`],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `Moved "${page.frontmatter.title}" from ${parsed.category}/ to ${params.to}/`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
details: {
|
|
74
|
+
from: `${parsed.category}/${parsed.slug}`,
|
|
75
|
+
to: `${params.to}/${parsed.slug}`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|