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.
Files changed (124) hide show
  1. package/ARCHITECTURE.md +4 -6
  2. package/CHANGELOG.md +14 -0
  3. package/CONFIGURATION.md +1 -16
  4. package/README.md +2 -13
  5. package/SETUP.md +1 -7
  6. package/STRUCTURE.md +7 -18
  7. package/dist/cli-legacy.d.ts +2 -1
  8. package/dist/cli-legacy.d.ts.map +1 -1
  9. package/dist/cli-legacy.js +13 -25
  10. package/dist/cli-legacy.js.map +1 -1
  11. package/dist/cli.d.ts +2 -24
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +29 -271
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +1 -28
  17. package/dist/commands.js.map +1 -1
  18. package/dist/config.d.ts +1 -5
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +0 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -64
  24. package/dist/index.js.map +1 -1
  25. package/dist/link-utils.d.ts +1 -1
  26. package/dist/link-utils.js +1 -1
  27. package/dist/settings.d.ts +0 -3
  28. package/dist/settings.d.ts.map +1 -1
  29. package/dist/settings.js +0 -11
  30. package/dist/settings.js.map +1 -1
  31. package/dist/tools/edit-read.d.ts +18 -0
  32. package/dist/tools/edit-read.d.ts.map +1 -0
  33. package/dist/tools/edit-read.js +161 -0
  34. package/dist/tools/edit-read.js.map +1 -0
  35. package/dist/tools/ingest.d.ts +12 -0
  36. package/dist/tools/ingest.d.ts.map +1 -0
  37. package/dist/tools/ingest.js +48 -0
  38. package/dist/tools/ingest.js.map +1 -0
  39. package/dist/tools/lint.d.ts +10 -0
  40. package/dist/tools/lint.d.ts.map +1 -0
  41. package/dist/tools/lint.js +57 -0
  42. package/dist/tools/lint.js.map +1 -0
  43. package/dist/tools/migrate.d.ts +6 -0
  44. package/dist/tools/migrate.d.ts.map +1 -0
  45. package/dist/tools/migrate.js +36 -0
  46. package/dist/tools/migrate.js.map +1 -0
  47. package/dist/tools/move.d.ts +9 -0
  48. package/dist/tools/move.d.ts.map +1 -0
  49. package/dist/tools/move.js +61 -0
  50. package/dist/tools/move.js.map +1 -0
  51. package/dist/tools/query.d.ts +12 -0
  52. package/dist/tools/query.d.ts.map +1 -0
  53. package/dist/tools/query.js +47 -0
  54. package/dist/tools/query.js.map +1 -0
  55. package/dist/tools/schemas.d.ts +109 -0
  56. package/dist/tools/schemas.d.ts.map +1 -0
  57. package/dist/tools/schemas.js +80 -0
  58. package/dist/tools/schemas.js.map +1 -0
  59. package/dist/tools/shared.d.ts +14 -0
  60. package/dist/tools/shared.d.ts.map +1 -0
  61. package/dist/tools/shared.js +25 -0
  62. package/dist/tools/shared.js.map +1 -0
  63. package/dist/tools/summarize.d.ts +8 -0
  64. package/dist/tools/summarize.d.ts.map +1 -0
  65. package/dist/tools/summarize.js +82 -0
  66. package/dist/tools/summarize.js.map +1 -0
  67. package/dist/tools/write.d.ts +23 -0
  68. package/dist/tools/write.d.ts.map +1 -0
  69. package/dist/tools/write.js +215 -0
  70. package/dist/tools/write.js.map +1 -0
  71. package/dist/tools.d.ts +1 -8
  72. package/dist/tools.d.ts.map +1 -1
  73. package/dist/tools.js +13 -795
  74. package/dist/tools.js.map +1 -1
  75. package/package.json +2 -6
  76. package/src/cli-legacy.ts +14 -26
  77. package/src/cli.ts +29 -293
  78. package/src/commands.ts +0 -21
  79. package/src/config.ts +1 -6
  80. package/src/index.ts +5 -66
  81. package/src/link-utils.ts +1 -1
  82. package/src/settings.ts +0 -14
  83. package/src/tools/edit-read.ts +193 -0
  84. package/src/tools/ingest.ts +63 -0
  85. package/src/tools/lint.ts +74 -0
  86. package/src/tools/migrate.ts +44 -0
  87. package/src/tools/move.ts +79 -0
  88. package/src/tools/query.ts +63 -0
  89. package/src/tools/schemas.ts +157 -0
  90. package/src/tools/shared.ts +35 -0
  91. package/src/tools/summarize.ts +89 -0
  92. package/src/tools/write.ts +252 -0
  93. package/src/tools.ts +18 -1046
  94. package/dist/daemon.d.ts +0 -49
  95. package/dist/daemon.d.ts.map +0 -1
  96. package/dist/daemon.js +0 -226
  97. package/dist/daemon.js.map +0 -1
  98. package/dist/link-discovery.d.ts +0 -40
  99. package/dist/link-discovery.d.ts.map +0 -1
  100. package/dist/link-discovery.js +0 -122
  101. package/dist/link-discovery.js.map +0 -1
  102. package/dist/maintainer.d.ts +0 -24
  103. package/dist/maintainer.d.ts.map +0 -1
  104. package/dist/maintainer.js +0 -358
  105. package/dist/maintainer.js.map +0 -1
  106. package/dist/watcher.d.ts +0 -25
  107. package/dist/watcher.d.ts.map +0 -1
  108. package/dist/watcher.js +0 -84
  109. package/dist/watcher.js.map +0 -1
  110. package/dist/webui/client/dist/index.html +0 -1885
  111. package/dist/webui/server.d.ts +0 -31
  112. package/dist/webui/server.d.ts.map +0 -1
  113. package/dist/webui/server.js +0 -566
  114. package/dist/webui/server.js.map +0 -1
  115. package/scripts/build-assets.mjs +0 -17
  116. package/src/daemon.ts +0 -288
  117. package/src/link-discovery.ts +0 -170
  118. package/src/maintainer.ts +0 -404
  119. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  120. package/src/python/minimax_lm.py +0 -198
  121. package/src/python/rlm_capture.py +0 -215
  122. package/src/watcher.ts +0 -101
  123. package/src/webui/client/dist/index.html +0 -1885
  124. 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, networkInterfaces } from "node:os";
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
- // Check if web wiki server is running (don't start our own yet)
374
- if (config.webWiki.enabled) {
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, processor.ts, and webui/server.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
+ }