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.
Files changed (198) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +305 -0
  3. package/SETUP.md +171 -0
  4. package/dist/capture.d.ts +34 -0
  5. package/dist/capture.d.ts.map +1 -0
  6. package/dist/capture.js +461 -0
  7. package/dist/capture.js.map +1 -0
  8. package/dist/cli.d.ts +15 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +405 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +15 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +786 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/context.d.ts +61 -0
  17. package/dist/context.d.ts.map +1 -0
  18. package/dist/context.js +289 -0
  19. package/dist/context.js.map +1 -0
  20. package/dist/daemon.d.ts +49 -0
  21. package/dist/daemon.d.ts.map +1 -0
  22. package/dist/daemon.js +226 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/frontmatter.d.ts +59 -0
  25. package/dist/frontmatter.d.ts.map +1 -0
  26. package/dist/frontmatter.js +170 -0
  27. package/dist/frontmatter.js.map +1 -0
  28. package/dist/gepa/dataset.d.ts +36 -0
  29. package/dist/gepa/dataset.d.ts.map +1 -0
  30. package/dist/gepa/dataset.js +148 -0
  31. package/dist/gepa/dataset.js.map +1 -0
  32. package/dist/gepa/evaluator.d.ts +51 -0
  33. package/dist/gepa/evaluator.d.ts.map +1 -0
  34. package/dist/gepa/evaluator.js +317 -0
  35. package/dist/gepa/evaluator.js.map +1 -0
  36. package/dist/gepa/index.d.ts +66 -0
  37. package/dist/gepa/index.d.ts.map +1 -0
  38. package/dist/gepa/index.js +240 -0
  39. package/dist/gepa/index.js.map +1 -0
  40. package/dist/gepa/llm.d.ts +35 -0
  41. package/dist/gepa/llm.d.ts.map +1 -0
  42. package/dist/gepa/llm.js +268 -0
  43. package/dist/gepa/llm.js.map +1 -0
  44. package/dist/gepa/optimized/capture-prompt.txt +65 -0
  45. package/dist/gepa/optimizer.d.ts +29 -0
  46. package/dist/gepa/optimizer.d.ts.map +1 -0
  47. package/dist/gepa/optimizer.js +249 -0
  48. package/dist/gepa/optimizer.js.map +1 -0
  49. package/dist/gepa/targets.d.ts +39 -0
  50. package/dist/gepa/targets.d.ts.map +1 -0
  51. package/dist/gepa/targets.js +326 -0
  52. package/dist/gepa/targets.js.map +1 -0
  53. package/dist/gepa/types.d.ts +168 -0
  54. package/dist/gepa/types.d.ts.map +1 -0
  55. package/dist/gepa/types.js +124 -0
  56. package/dist/gepa/types.js.map +1 -0
  57. package/dist/index.d.ts +21 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +294 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/ingest.d.ts +88 -0
  62. package/dist/ingest.d.ts.map +1 -0
  63. package/dist/ingest.js +105 -0
  64. package/dist/ingest.js.map +1 -0
  65. package/dist/link-discovery.d.ts +40 -0
  66. package/dist/link-discovery.d.ts.map +1 -0
  67. package/dist/link-discovery.js +122 -0
  68. package/dist/link-discovery.js.map +1 -0
  69. package/dist/link-utils.d.ts +30 -0
  70. package/dist/link-utils.d.ts.map +1 -0
  71. package/dist/link-utils.js +145 -0
  72. package/dist/link-utils.js.map +1 -0
  73. package/dist/lint.d.ts +39 -0
  74. package/dist/lint.d.ts.map +1 -0
  75. package/dist/lint.js +827 -0
  76. package/dist/lint.js.map +1 -0
  77. package/dist/maintainer.d.ts +24 -0
  78. package/dist/maintainer.d.ts.map +1 -0
  79. package/dist/maintainer.js +358 -0
  80. package/dist/maintainer.js.map +1 -0
  81. package/dist/processor.d.ts +14 -0
  82. package/dist/processor.d.ts.map +1 -0
  83. package/dist/processor.js +361 -0
  84. package/dist/processor.js.map +1 -0
  85. package/dist/query.d.ts +69 -0
  86. package/dist/query.d.ts.map +1 -0
  87. package/dist/query.js +107 -0
  88. package/dist/query.js.map +1 -0
  89. package/dist/raw.d.ts +57 -0
  90. package/dist/raw.d.ts.map +1 -0
  91. package/dist/raw.js +219 -0
  92. package/dist/raw.js.map +1 -0
  93. package/dist/redact.d.ts +21 -0
  94. package/dist/redact.d.ts.map +1 -0
  95. package/dist/redact.js +143 -0
  96. package/dist/redact.js.map +1 -0
  97. package/dist/scope.d.ts +54 -0
  98. package/dist/scope.d.ts.map +1 -0
  99. package/dist/scope.js +161 -0
  100. package/dist/scope.js.map +1 -0
  101. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  102. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  103. package/dist/scripts/bulk-link-fix.js +106 -0
  104. package/dist/scripts/bulk-link-fix.js.map +1 -0
  105. package/dist/session-tools.d.ts +33 -0
  106. package/dist/session-tools.d.ts.map +1 -0
  107. package/dist/session-tools.js +211 -0
  108. package/dist/session-tools.js.map +1 -0
  109. package/dist/state.d.ts +54 -0
  110. package/dist/state.d.ts.map +1 -0
  111. package/dist/state.js +176 -0
  112. package/dist/state.js.map +1 -0
  113. package/dist/store.d.ts +56 -0
  114. package/dist/store.d.ts.map +1 -0
  115. package/dist/store.js +227 -0
  116. package/dist/store.js.map +1 -0
  117. package/dist/summarize.d.ts +56 -0
  118. package/dist/summarize.d.ts.map +1 -0
  119. package/dist/summarize.js +231 -0
  120. package/dist/summarize.js.map +1 -0
  121. package/dist/tag-registry.d.ts +56 -0
  122. package/dist/tag-registry.d.ts.map +1 -0
  123. package/dist/tag-registry.js +146 -0
  124. package/dist/tag-registry.js.map +1 -0
  125. package/dist/templates/prompts.d.ts +63 -0
  126. package/dist/templates/prompts.d.ts.map +1 -0
  127. package/dist/templates/prompts.js +260 -0
  128. package/dist/templates/prompts.js.map +1 -0
  129. package/dist/tools.d.ts +26 -0
  130. package/dist/tools.d.ts.map +1 -0
  131. package/dist/tools.js +1033 -0
  132. package/dist/tools.js.map +1 -0
  133. package/dist/watcher.d.ts +25 -0
  134. package/dist/watcher.d.ts.map +1 -0
  135. package/dist/watcher.js +84 -0
  136. package/dist/watcher.js.map +1 -0
  137. package/dist/webui/client/dist/index.html +1885 -0
  138. package/dist/webui/server.d.ts +31 -0
  139. package/dist/webui/server.d.ts.map +1 -0
  140. package/dist/webui/server.js +566 -0
  141. package/dist/webui/server.js.map +1 -0
  142. package/dist/wiki.d.ts +103 -0
  143. package/dist/wiki.d.ts.map +1 -0
  144. package/dist/wiki.js +428 -0
  145. package/dist/wiki.js.map +1 -0
  146. package/package.json +142 -0
  147. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  148. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  149. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  150. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  151. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  152. package/scripts/gepa/dataset.py +192 -0
  153. package/scripts/gepa/lm_providers.py +472 -0
  154. package/scripts/gepa/metric.py +279 -0
  155. package/scripts/gepa/optimize.py +367 -0
  156. package/scripts/gepa/program.py +99 -0
  157. package/scripts/gepa/pyproject.toml +19 -0
  158. package/scripts/gepa/run-all.sh +53 -0
  159. package/scripts/gepa/uv.lock +2241 -0
  160. package/scripts/gepa/wiki_reader.py +113 -0
  161. package/skills/para/SKILL.md +77 -0
  162. package/skills/setup/SKILL.md +20 -0
  163. package/src/capture.ts +563 -0
  164. package/src/cli.ts +416 -0
  165. package/src/commands.ts +878 -0
  166. package/src/context.ts +390 -0
  167. package/src/daemon.ts +288 -0
  168. package/src/frontmatter.ts +219 -0
  169. package/src/gepa/index.ts +287 -0
  170. package/src/gepa/optimized/capture-prompt.txt +65 -0
  171. package/src/index.ts +384 -0
  172. package/src/ingest.ts +177 -0
  173. package/src/link-discovery.ts +170 -0
  174. package/src/link-utils.ts +169 -0
  175. package/src/lint.ts +1018 -0
  176. package/src/maintainer.ts +404 -0
  177. package/src/processor.ts +415 -0
  178. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  179. package/src/python/minimax_lm.py +198 -0
  180. package/src/python/rlm_capture.py +215 -0
  181. package/src/query.ts +148 -0
  182. package/src/raw.ts +276 -0
  183. package/src/redact.ts +159 -0
  184. package/src/scope.ts +214 -0
  185. package/src/scripts/bulk-link-fix.ts +126 -0
  186. package/src/session-tools.ts +260 -0
  187. package/src/state.ts +271 -0
  188. package/src/store.ts +288 -0
  189. package/src/summarize.ts +273 -0
  190. package/src/tag-registry.ts +157 -0
  191. package/src/templates/index.md +17 -0
  192. package/src/templates/prompts.ts +274 -0
  193. package/src/templates/schema.md +116 -0
  194. package/src/tools.ts +1293 -0
  195. package/src/watcher.ts +101 -0
  196. package/src/webui/client/dist/index.html +1885 -0
  197. package/src/webui/server.ts +765 -0
  198. package/src/wiki.ts +550 -0
package/dist/tools.js ADDED
@@ -0,0 +1,1033 @@
1
+ /**
2
+ * Tool registrations — wiki tools for both pi's session agent and standalone mini-agent.
3
+ *
4
+ * Tool implementations are shared between both consumers. Only the registration
5
+ * wrapper differs (pi.registerTool vs AgentTool).
6
+ *
7
+ * Factory functions (createXxxExecute) close over wikiDir and store — no ctx dependency.
8
+ */
9
+ import { StringEnum } from "@mariozechner/pi-ai";
10
+ import { Text } from "@mariozechner/pi-tui";
11
+ import { Type } from "typebox";
12
+ import { resolveSource, truncateSource, detectSourceType } from "./ingest.js";
13
+ import { queryWiki as queryWikiLib, formatQueryResults, formatFreshness } from "./query.js";
14
+ import { reindex } from "./store.js";
15
+ import { readPage, writePage, movePage, listPages, readIndex, writeIndex, readSchema, appendLog, gitCommit, rebuildIndex, PARA_CATEGORIES, } from "./wiki.js";
16
+ import { validateFrontmatter, parseFrontmatter, CURRENT_SCHEMA_VERSION, migrateToLatest, } from "./frontmatter.js";
17
+ import { extractWikilinks, autoLinkSlugs, syncFrontmatterLinks } from "./link-utils.js";
18
+ import { normalizeTags, normalizeScopes } from "./tag-registry.js";
19
+ import { redactSecrets } from "./redact.js";
20
+ import { lintWiki } from "./lint.js";
21
+ import { generateOverviewPrompt } from "./summarize.js";
22
+ import { INGEST_PROMPT, QUERY_PROMPT, LINT_PROMPT, } from "./templates/prompts.js";
23
+ // -- Parameter schemas -------------------------------------------------------
24
+ const PARA_ENUM = StringEnum(["projects", "areas", "resources", "archives"], { description: "PARA category" });
25
+ const PARA_ENUM_NO_ARCHIVES = StringEnum(["projects", "areas", "resources"], { description: "PARA category (archives excluded for new content)" });
26
+ const WikiIngestParams = Type.Object({
27
+ source: Type.String({ description: "URL, file path, or raw text to ingest" }),
28
+ sourceType: Type.Optional(StringEnum(["url", "file", "text"], {
29
+ description: "Source type (auto-detected if omitted)",
30
+ })),
31
+ category: Type.Optional(PARA_ENUM_NO_ARCHIVES),
32
+ scope: Type.Optional(Type.Array(Type.String(), { description: "Scope tags for the ingested content" })),
33
+ });
34
+ const WikiQueryParams = Type.Object({
35
+ query: Type.String({ description: "Natural language search query" }),
36
+ global: Type.Optional(Type.Boolean({ description: "Search all scopes, not just current project" })),
37
+ category: Type.Optional(PARA_ENUM),
38
+ limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
39
+ });
40
+ const WikiEditSchema = Type.Object({
41
+ oldText: Type.String({ description: "Exact text to find in the page body" }),
42
+ newText: Type.String({ description: "Replacement text" }),
43
+ });
44
+ const WikiWritePageSchema = Type.Object({
45
+ category: PARA_ENUM,
46
+ slug: Type.String({ description: "Page slug (lowercase, hyphens)" }),
47
+ title: Type.String({ description: "Page title" }),
48
+ scope: Type.Array(Type.String(), { description: "Scope tags" }),
49
+ tags: Type.Array(Type.String(), { description: "Topic tags" }),
50
+ body: Type.String({ description: "Page body in markdown (wiki summary format)" }),
51
+ mode: StringEnum(["create", "replace", "append", "edit"], {
52
+ description: "Write mode: create new, replace existing, append to existing, or edit specific sections",
53
+ }),
54
+ edits: Type.Optional(Type.Array(WikiEditSchema, {
55
+ description: "For mode=edit: targeted text replacements (oldText→newText). Page must exist.",
56
+ })),
57
+ });
58
+ const WikiWriteParams = Type.Object({
59
+ pages: Type.Array(WikiWritePageSchema, {
60
+ description: "Pages to write or update",
61
+ }),
62
+ // DEPRECATED: indexContent is ignored. Index is auto-rebuilt from all pages on disk.
63
+ // Kept in schema to avoid breaking existing callers.
64
+ indexContent: Type.Optional(Type.String({ description: "Updated index.md content (full replacement)" })),
65
+ logSummary: Type.Optional(Type.String({ description: "One-line summary for log.md" })),
66
+ });
67
+ const WikiReadParams = Type.Object({
68
+ path: Type.String({
69
+ description: "Page path (e.g. 'projects/auth-refactor') or page title",
70
+ }),
71
+ });
72
+ const WikiMoveParams = Type.Object({
73
+ path: Type.String({
74
+ description: "Current page path (e.g. 'projects/auth-refactor')",
75
+ }),
76
+ to: PARA_ENUM,
77
+ });
78
+ const WikiLintParams = Type.Object({
79
+ autoFix: Type.Optional(Type.Boolean({ description: "Auto-fix simple issues (default true)" })),
80
+ });
81
+ const WikiMigrateParams = Type.Object({});
82
+ const WikiSummarizeParams = Type.Object({
83
+ target: Type.String({
84
+ description: "Page path, category name (e.g. 'projects'), or 'all'",
85
+ }),
86
+ depth: Type.Optional(StringEnum(["brief", "detailed"], {
87
+ description: "Summary depth (default brief)",
88
+ })),
89
+ });
90
+ // -- Source resolution helpers -----------------------------------------------
91
+ /** Resolve a page path like "projects/auth-refactor" into category + slug. */
92
+ function parsePagePath(path) {
93
+ const parts = path.split("/");
94
+ if (parts.length === 2) {
95
+ const cat = parts[0];
96
+ if (PARA_CATEGORIES.includes(cat)) {
97
+ const slug = parts[1].replace(/\.md$/, "");
98
+ return { category: cat, slug };
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ // -- Factory: wiki_ingest execute --------------------------------------------
104
+ function createIngestExecute(wikiDir, _store, getScope) {
105
+ return async (params) => {
106
+ const scope = getScope();
107
+ const resolved = await resolveSource(wikiDir, {
108
+ source: params.source,
109
+ sourceType: params.sourceType,
110
+ scope: params.scope,
111
+ category: params.category,
112
+ }, scope);
113
+ const categoryHint = resolved.categoryHint
114
+ ? `\nSuggested PARA category: ${resolved.categoryHint}`
115
+ : "";
116
+ // Assemble the tool result with instructions for the LLM
117
+ const toolResultText = [
118
+ INGEST_PROMPT,
119
+ "",
120
+ `Current scope: ${resolved.scopeName} (tags: ${resolved.scopeTags.join(", ")})`,
121
+ categoryHint,
122
+ resolved.rawPath ? `Raw source saved to: ${resolved.rawPath}` : "",
123
+ "",
124
+ "<wiki-schema>",
125
+ resolved.schema,
126
+ "</wiki-schema>",
127
+ "",
128
+ "<wiki-index>",
129
+ resolved.index,
130
+ "</wiki-index>",
131
+ "",
132
+ "<source-content>",
133
+ truncateSource(resolved.content),
134
+ "</source-content>",
135
+ "",
136
+ "Now analyze the source content and use wiki_write to create/update pages, update the index, and log the operation.",
137
+ ].join("\n");
138
+ return {
139
+ content: [{ type: "text", text: toolResultText }],
140
+ details: {
141
+ sourceType: resolved.sourceType,
142
+ rawPath: resolved.rawPath,
143
+ sourceLength: resolved.content.length,
144
+ },
145
+ };
146
+ };
147
+ }
148
+ // -- Factory: wiki_query execute ---------------------------------------------
149
+ function createQueryExecute(_wikiDir, store, getScope, getGraphBoost) {
150
+ return async (params) => {
151
+ const scope = getScope();
152
+ const result = await queryWikiLib(store, {
153
+ query: params.query,
154
+ global: params.global,
155
+ category: params.category,
156
+ limit: params.limit,
157
+ graphBoost: getGraphBoost?.() ?? true,
158
+ }, scope);
159
+ if (result.results.length === 0) {
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `No wiki pages found for "${params.query}". The wiki may not have content on this topic yet.`,
165
+ },
166
+ ],
167
+ details: {
168
+ resultCount: 0,
169
+ scopeUsed: result.wasGlobal ? "global" : scope.name,
170
+ },
171
+ };
172
+ }
173
+ const formatted = formatQueryResults(result.results);
174
+ const toolResultText = [
175
+ QUERY_PROMPT,
176
+ "",
177
+ `Query: "${params.query}"`,
178
+ `Scope: ${result.wasGlobal ? "global (all scopes)" : scope.name}`,
179
+ `Results: ${result.results.length}`,
180
+ "",
181
+ formatted,
182
+ ].join("\n");
183
+ return {
184
+ content: [{ type: "text", text: toolResultText }],
185
+ details: {
186
+ resultCount: result.results.length,
187
+ scopeUsed: result.wasGlobal ? "global" : scope.name,
188
+ },
189
+ };
190
+ };
191
+ }
192
+ // -- Factory: wiki_write execute ---------------------------------------------
193
+ function createWriteExecute(wikiDir, store, getScope, markDirty) {
194
+ return async (params) => {
195
+ const pagesWritten = [];
196
+ const now = new Date().toISOString();
197
+ const scope = getScope();
198
+ // Gather all existing slugs for auto-linking
199
+ const allRefs = await listPages(wikiDir);
200
+ const allSlugs = new Set(allRefs.map(r => r.slug));
201
+ for (const pageSpec of params.pages) {
202
+ // Sanitize slug
203
+ const slug = pageSpec.slug
204
+ .toLowerCase()
205
+ .replace(/[^a-z0-9-]/g, "-")
206
+ .replace(/-+/g, "-")
207
+ .replace(/^-|-$/g, "");
208
+ if (!slug) {
209
+ continue; // skip invalid slugs
210
+ }
211
+ const existing = await readPage(wikiDir, pageSpec.category, slug);
212
+ const pagePath = `${pageSpec.category}/${slug}`;
213
+ // Track this slug for auto-linking other pages in this batch
214
+ allSlugs.add(slug);
215
+ if (pageSpec.mode === "edit" && existing) {
216
+ // Edit mode: apply targeted oldText→newText replacements
217
+ const edits = pageSpec.edits ?? [];
218
+ if (edits.length === 0) {
219
+ continue; // no edits to apply
220
+ }
221
+ let editedBody = existing.body;
222
+ const errors = [];
223
+ for (const edit of edits) {
224
+ const idx = editedBody.indexOf(edit.oldText);
225
+ if (idx === -1) {
226
+ errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
227
+ continue;
228
+ }
229
+ editedBody = editedBody.slice(0, idx) + edit.newText + editedBody.slice(idx + edit.oldText.length);
230
+ }
231
+ const linkedBody = redactSecrets(autoLinkSlugs(editedBody, allSlugs, slug)).text;
232
+ const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : existing.frontmatter.scope;
233
+ const newScope = normalizeScopes(rawScope);
234
+ const newTags = pageSpec.tags.length > 0 ? pageSpec.tags : existing.frontmatter.tags;
235
+ const updatedPage = {
236
+ category: pageSpec.category,
237
+ slug,
238
+ frontmatter: {
239
+ ...existing.frontmatter,
240
+ title: pageSpec.title || existing.frontmatter.title,
241
+ scope: newScope,
242
+ tags: normalizeTags(newTags, newScope),
243
+ updated: now,
244
+ links: syncFrontmatterLinks(linkedBody),
245
+ },
246
+ body: linkedBody,
247
+ };
248
+ await writePage(wikiDir, updatedPage);
249
+ pagesWritten.push(pagePath + (errors.length > 0 ? ` (${errors.length} edit(s) failed)` : ""));
250
+ }
251
+ else if (pageSpec.mode === "append" && existing) {
252
+ // Append to existing page body
253
+ const combinedBody = existing.body.trimEnd() + "\n\n" + pageSpec.body;
254
+ const linkedBody = redactSecrets(autoLinkSlugs(combinedBody, allSlugs, slug)).text;
255
+ const mergedTags = [...new Set([...existing.frontmatter.tags, ...pageSpec.tags])];
256
+ const mergedScope = normalizeScopes([...new Set([...existing.frontmatter.scope, ...pageSpec.scope])]);
257
+ const updatedPage = {
258
+ ...existing,
259
+ body: linkedBody,
260
+ frontmatter: {
261
+ ...existing.frontmatter,
262
+ updated: now,
263
+ tags: normalizeTags(mergedTags, mergedScope),
264
+ scope: mergedScope,
265
+ links: syncFrontmatterLinks(linkedBody),
266
+ },
267
+ };
268
+ await writePage(wikiDir, updatedPage);
269
+ pagesWritten.push(pagePath);
270
+ }
271
+ else if (pageSpec.mode === "replace" && existing) {
272
+ // Replace existing page body, update frontmatter
273
+ const linkedBody = redactSecrets(autoLinkSlugs(pageSpec.body, allSlugs, slug)).text;
274
+ const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : existing.frontmatter.scope;
275
+ const newScope = normalizeScopes(rawScope);
276
+ const newTags = pageSpec.tags.length > 0 ? pageSpec.tags : existing.frontmatter.tags;
277
+ const updatedPage = {
278
+ category: pageSpec.category,
279
+ slug,
280
+ frontmatter: {
281
+ ...existing.frontmatter,
282
+ title: pageSpec.title,
283
+ para: pageSpec.category,
284
+ scope: newScope,
285
+ tags: normalizeTags(newTags, newScope),
286
+ updated: now,
287
+ links: syncFrontmatterLinks(linkedBody),
288
+ },
289
+ body: linkedBody,
290
+ };
291
+ await writePage(wikiDir, updatedPage);
292
+ pagesWritten.push(pagePath);
293
+ }
294
+ else {
295
+ // Create new page (or mode=create/replace on non-existing)
296
+ const linkedBody = redactSecrets(autoLinkSlugs(pageSpec.body, allSlugs, slug)).text;
297
+ const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : [scope.name];
298
+ const newScope = normalizeScopes(rawScope);
299
+ const frontmatter = validateFrontmatter({
300
+ title: pageSpec.title,
301
+ para: pageSpec.category,
302
+ scope: newScope,
303
+ tags: normalizeTags(pageSpec.tags, newScope),
304
+ sources: [],
305
+ created: now,
306
+ updated: now,
307
+ links: syncFrontmatterLinks(linkedBody),
308
+ });
309
+ const newPage = {
310
+ category: pageSpec.category,
311
+ slug,
312
+ frontmatter,
313
+ body: linkedBody,
314
+ };
315
+ await writePage(wikiDir, newPage);
316
+ pagesWritten.push(pagePath);
317
+ }
318
+ }
319
+ // Always auto-rebuild index from disk.
320
+ // The LLM's indexContent is unreliable — it only knows about pages
321
+ // in its context, so it produces partial indexes that overwrite
322
+ // the full one. indexContent parameter is deprecated and ignored.
323
+ let indexUpdated = false;
324
+ if (pagesWritten.length > 0) {
325
+ await rebuildIndex(wikiDir);
326
+ indexUpdated = true;
327
+ }
328
+ // Append to log.md if summary provided
329
+ let logAppended = false;
330
+ if (params.logSummary && pagesWritten.length > 0) {
331
+ await appendLog(wikiDir, {
332
+ date: now.split("T")[0],
333
+ operation: "ingest",
334
+ summary: params.logSummary,
335
+ pages: pagesWritten,
336
+ });
337
+ logAppended = true;
338
+ }
339
+ // Re-index for BM25 (fast). Do NOT embed — deferred to shutdown.
340
+ await reindex(store);
341
+ // Mark context cache dirty so before_agent_start rebuilds
342
+ markDirty();
343
+ // Git auto-commit
344
+ if (pagesWritten.length > 0) {
345
+ const commitMsg = params.logSummary
346
+ ? params.logSummary
347
+ : `wiki: ${pagesWritten.join(", ")}`;
348
+ await gitCommit(wikiDir, commitMsg);
349
+ }
350
+ const summary = pagesWritten.length > 0
351
+ ? `Wrote ${pagesWritten.length} page(s): ${pagesWritten.join(", ")}`
352
+ : "No pages written.";
353
+ return {
354
+ content: [
355
+ {
356
+ type: "text",
357
+ text: [
358
+ summary,
359
+ indexUpdated ? "Updated index.md." : "",
360
+ logAppended ? `Logged: ${params.logSummary}` : "",
361
+ ]
362
+ .filter(Boolean)
363
+ .join("\n"),
364
+ },
365
+ ],
366
+ details: {
367
+ pagesWritten,
368
+ indexUpdated,
369
+ logAppended,
370
+ },
371
+ };
372
+ };
373
+ }
374
+ // -- Factory: wiki_read execute ----------------------------------------------
375
+ function createReadExecute(wikiDir) {
376
+ return async (params) => {
377
+ const now = Date.now();
378
+ // Try parsing as category/slug path
379
+ const parsed = parsePagePath(params.path);
380
+ if (parsed) {
381
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
382
+ if (page) {
383
+ const fm = page.frontmatter;
384
+ const freshness = formatFreshness(fm.updated, now);
385
+ const header = [
386
+ `# ${fm.title}`,
387
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
388
+ `Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
389
+ fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
390
+ fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
391
+ ]
392
+ .filter(Boolean)
393
+ .join("\n");
394
+ return {
395
+ content: [{ type: "text", text: `${header}\n\n${page.body}` }],
396
+ details: { found: true, path: `${parsed.category}/${parsed.slug}` },
397
+ };
398
+ }
399
+ }
400
+ // Try finding by title (case-insensitive search across all pages)
401
+ const allPages = await listPages(wikiDir);
402
+ const titleLower = params.path.toLowerCase();
403
+ for (const ref of allPages) {
404
+ if (ref.title.toLowerCase() === titleLower || ref.slug === titleLower) {
405
+ const page = await readPage(wikiDir, ref.category, ref.slug);
406
+ if (page) {
407
+ const fm = page.frontmatter;
408
+ const freshness = formatFreshness(fm.updated, now);
409
+ const header = [
410
+ `# ${fm.title}`,
411
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
412
+ `Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
413
+ fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
414
+ fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
415
+ ]
416
+ .filter(Boolean)
417
+ .join("\n");
418
+ return {
419
+ content: [{ type: "text", text: `${header}\n\n${page.body}` }],
420
+ details: { found: true, path: `${ref.category}/${ref.slug}` },
421
+ };
422
+ }
423
+ }
424
+ }
425
+ return {
426
+ content: [{ type: "text", text: `Page not found: "${params.path}"` }],
427
+ details: { found: false },
428
+ };
429
+ };
430
+ }
431
+ // -- Factory: wiki_move execute ----------------------------------------------
432
+ function createMoveExecute(wikiDir, store, markDirty) {
433
+ return async (params) => {
434
+ const parsed = parsePagePath(params.path);
435
+ if (!parsed) {
436
+ throw new Error(`Invalid page path: "${params.path}". Expected format: category/slug`);
437
+ }
438
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
439
+ if (!page) {
440
+ throw new Error(`Page not found: "${params.path}"`);
441
+ }
442
+ if (parsed.category === params.to) {
443
+ return {
444
+ content: [
445
+ {
446
+ type: "text",
447
+ text: `Page "${params.path}" is already in ${params.to}. No move needed.`,
448
+ },
449
+ ],
450
+ details: { from: params.path, to: `${params.to}/${parsed.slug}` },
451
+ };
452
+ }
453
+ const ref = {
454
+ category: parsed.category,
455
+ slug: parsed.slug,
456
+ title: page.frontmatter.title,
457
+ path: `${parsed.category}/${parsed.slug}.md`,
458
+ };
459
+ await movePage(wikiDir, ref, params.to);
460
+ // Re-index and rebuild index.md
461
+ await reindex(store);
462
+ await rebuildIndex(wikiDir);
463
+ markDirty();
464
+ // Git commit
465
+ await gitCommit(wikiDir, `move: ${page.frontmatter.title} → ${params.to}`);
466
+ // Log the move
467
+ const now = new Date().toISOString();
468
+ await appendLog(wikiDir, {
469
+ date: now.split("T")[0],
470
+ operation: "move",
471
+ summary: `Moved ${page.frontmatter.title} from ${parsed.category} to ${params.to}`,
472
+ pages: [`${params.to}/${parsed.slug}`],
473
+ });
474
+ return {
475
+ content: [
476
+ {
477
+ type: "text",
478
+ text: `Moved "${page.frontmatter.title}" from ${parsed.category}/ to ${params.to}/`,
479
+ },
480
+ ],
481
+ details: {
482
+ from: `${parsed.category}/${parsed.slug}`,
483
+ to: `${params.to}/${parsed.slug}`,
484
+ },
485
+ };
486
+ };
487
+ }
488
+ // -- Factory: wiki_lint execute ----------------------------------------------
489
+ function createLintExecute(wikiDir) {
490
+ return async (params) => {
491
+ const report = await lintWiki(wikiDir, {
492
+ autoFix: params.autoFix ?? true,
493
+ });
494
+ const { issues, fixed, stats } = report;
495
+ // Format report for the LLM
496
+ const parts = [];
497
+ parts.push("## Wiki Lint Report\n");
498
+ parts.push(`Total pages: ${stats.totalPages}`);
499
+ parts.push(`By category: ${PARA_CATEGORIES.map((c) => `${c}: ${stats.byCategory[c]}`).join(", ")}`);
500
+ parts.push(`Total links: ${stats.totalLinks} (broken: ${stats.brokenLinks})`);
501
+ parts.push(`Orphan pages: ${stats.orphanPages}`);
502
+ if (stats.oldestPage)
503
+ parts.push(`Oldest page: ${stats.oldestPage}`);
504
+ if (stats.newestPage)
505
+ parts.push(`Newest page: ${stats.newestPage}`);
506
+ if (stats.lastIngest)
507
+ parts.push(`Last ingest: ${stats.lastIngest}`);
508
+ if (fixed.length > 0) {
509
+ parts.push(`\n## Auto-fixed (${fixed.length})\n`);
510
+ for (const f of fixed) {
511
+ parts.push(`- [${f.severity}] ${f.page ?? ""}: ${f.message}`);
512
+ }
513
+ }
514
+ if (issues.length > 0) {
515
+ parts.push(`\n## Remaining Issues (${issues.length})\n`);
516
+ for (const issue of issues) {
517
+ parts.push(`- [${issue.severity}] ${issue.category}${issue.page ? ` (${issue.page})` : ""}: ${issue.message}`);
518
+ }
519
+ parts.push("");
520
+ parts.push(LINT_PROMPT);
521
+ }
522
+ else {
523
+ parts.push("\nNo remaining issues. Wiki is healthy.");
524
+ }
525
+ return {
526
+ content: [{ type: "text", text: parts.join("\n") }],
527
+ details: {
528
+ issueCount: issues.length,
529
+ fixedCount: fixed.length,
530
+ stats,
531
+ },
532
+ };
533
+ };
534
+ }
535
+ // -- Factory: wiki_summarize execute -----------------------------------------
536
+ function createSummarizeExecute(wikiDir, getScope) {
537
+ return async (params) => {
538
+ const scope = getScope();
539
+ const pages = [];
540
+ if (params.target === "all") {
541
+ // Summarize entire wiki
542
+ const refs = await listPages(wikiDir);
543
+ for (const ref of refs) {
544
+ const page = await readPage(wikiDir, ref.category, ref.slug);
545
+ if (page)
546
+ pages.push(page);
547
+ }
548
+ }
549
+ else if (PARA_CATEGORIES.includes(params.target)) {
550
+ // Summarize a category
551
+ const refs = await listPages(wikiDir, params.target);
552
+ for (const ref of refs) {
553
+ const page = await readPage(wikiDir, ref.category, ref.slug);
554
+ if (page)
555
+ pages.push(page);
556
+ }
557
+ }
558
+ else {
559
+ // Summarize a single page
560
+ const parsed = parsePagePath(params.target);
561
+ if (parsed) {
562
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
563
+ if (page)
564
+ pages.push(page);
565
+ }
566
+ else {
567
+ // Try finding by slug alone across categories
568
+ const allPages = await listPages(wikiDir);
569
+ const targetLower = params.target.toLowerCase();
570
+ for (const ref of allPages) {
571
+ if (ref.slug === targetLower || ref.title.toLowerCase() === targetLower) {
572
+ const page = await readPage(wikiDir, ref.category, ref.slug);
573
+ if (page)
574
+ pages.push(page);
575
+ break;
576
+ }
577
+ }
578
+ }
579
+ }
580
+ if (pages.length === 0) {
581
+ return {
582
+ content: [
583
+ {
584
+ type: "text",
585
+ text: `No pages found for target "${params.target}". Nothing to summarize.`,
586
+ },
587
+ ],
588
+ details: { target: params.target, pageCount: 0 },
589
+ };
590
+ }
591
+ // Generate overview prompt for the LLM
592
+ const overviewPrompt = generateOverviewPrompt(pages, scope);
593
+ const depthNote = params.depth === "detailed"
594
+ ? "\nProvide a DETAILED summary with full Key Facts, Insights, and Connections for each theme."
595
+ : "\nProvide a BRIEF overview — one paragraph per theme, focus on high-level patterns.";
596
+ return {
597
+ content: [
598
+ {
599
+ type: "text",
600
+ text: overviewPrompt + depthNote +
601
+ "\n\nYou may optionally create a summary page via wiki_write if the overview is worth preserving.",
602
+ },
603
+ ],
604
+ details: {
605
+ target: params.target,
606
+ pageCount: pages.length,
607
+ },
608
+ };
609
+ };
610
+ }
611
+ // -- Factory: wiki_migrate execute -------------------------------------------
612
+ function createMigrateExecute(wikiDir) {
613
+ return async () => {
614
+ const allRefs = await listPages(wikiDir);
615
+ let migratedCount = 0;
616
+ for (const ref of allRefs) {
617
+ const page = await readPage(wikiDir, ref.category, ref.slug);
618
+ if (!page)
619
+ continue;
620
+ const version = page.frontmatter.schemaVersion ?? 1;
621
+ if (version >= CURRENT_SCHEMA_VERSION)
622
+ continue;
623
+ const rawObj = page.frontmatter;
624
+ const result = migrateToLatest(rawObj, page.body);
625
+ const migratedFm = result.fm;
626
+ migratedFm.updated = new Date().toISOString();
627
+ await writePage(wikiDir, {
628
+ category: ref.category,
629
+ slug: ref.slug,
630
+ frontmatter: migratedFm,
631
+ body: result.body,
632
+ });
633
+ migratedCount++;
634
+ }
635
+ const summary = migratedCount > 0
636
+ ? `Migrated ${migratedCount} page(s) to schema version ${CURRENT_SCHEMA_VERSION}.`
637
+ : `All ${allRefs.length} page(s) already at schema version ${CURRENT_SCHEMA_VERSION}.`;
638
+ return {
639
+ content: [{ type: "text", text: summary }],
640
+ details: { migratedCount, totalPages: allRefs.length },
641
+ };
642
+ };
643
+ }
644
+ // -- Wikilink extraction helper (re-exported from link-utils) ----------------
645
+ // extractWikilinks, autoLinkSlugs, syncFrontmatterLinks imported above
646
+ // -- Public API: register tools for pi session -------------------------------
647
+ /**
648
+ * Register all wiki tools with the pi extension API.
649
+ *
650
+ * Tools: wiki_ingest, wiki_query, wiki_write, wiki_read,
651
+ * wiki_move, wiki_lint, wiki_summarize
652
+ */
653
+ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphBoost) {
654
+ const ingestExec = createIngestExecute(wikiDir, store, getScope);
655
+ const queryExec = createQueryExecute(wikiDir, store, getScope, getGraphBoost);
656
+ const writeExec = createWriteExecute(wikiDir, store, getScope, markDirty);
657
+ const readExec = createReadExecute(wikiDir);
658
+ const moveExec = createMoveExecute(wikiDir, store, markDirty);
659
+ const lintExec = createLintExecute(wikiDir);
660
+ const migrateExec = createMigrateExecute(wikiDir);
661
+ const summarizeExec = createSummarizeExecute(wikiDir, getScope);
662
+ // -- wiki_ingest --
663
+ pi.registerTool({
664
+ name: "wiki_ingest",
665
+ label: "Wiki Ingest",
666
+ description: "Ingest a source (URL, file path, or raw text) into the wiki. " +
667
+ "Fetches the content, saves a raw copy, and returns it with schema and index " +
668
+ "so you can synthesize wiki pages via wiki_write.",
669
+ promptSnippet: "Ingest a URL, file, or text into the PARA wiki knowledge base",
670
+ promptGuidelines: [
671
+ "Use wiki_ingest when the user provides a URL, file path, or text to add to the knowledge base.",
672
+ ],
673
+ parameters: WikiIngestParams,
674
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
675
+ ctx.ui.setStatus("pi-para", "wiki: ingesting...");
676
+ try {
677
+ return await ingestExec(params);
678
+ }
679
+ finally {
680
+ ctx.ui.setStatus("pi-para", undefined);
681
+ }
682
+ },
683
+ renderCall(args, theme) {
684
+ const srcType = args.sourceType ?? detectSourceType(args.source);
685
+ const src = args.source.length > 60
686
+ ? args.source.slice(0, 57) + "..."
687
+ : args.source;
688
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_ingest ")) +
689
+ theme.fg("muted", `[${srcType}] `) +
690
+ theme.fg("dim", src), 0, 0);
691
+ },
692
+ renderResult(result, _options, theme) {
693
+ const details = result.details;
694
+ if (!details) {
695
+ const t = result.content[0];
696
+ return new Text(t?.type === "text" ? t.text.slice(0, 80) : "", 0, 0);
697
+ }
698
+ const parts = [
699
+ theme.fg("success", "Fetched ") +
700
+ theme.fg("muted", `${details.sourceType} (${formatBytes(details.sourceLength)})`),
701
+ ];
702
+ if (details.rawPath) {
703
+ parts.push(theme.fg("dim", `Saved: ${details.rawPath}`));
704
+ }
705
+ return new Text(parts.join("\n"), 0, 0);
706
+ },
707
+ });
708
+ // -- wiki_query --
709
+ pi.registerTool({
710
+ name: "wiki_query",
711
+ label: "Wiki Query",
712
+ description: "Search the wiki knowledge base with a natural language query. " +
713
+ "Returns relevant pages with content snippets. " +
714
+ "Use global=true to search across all project scopes.",
715
+ promptSnippet: "Search the PARA wiki for relevant knowledge pages",
716
+ promptGuidelines: [
717
+ "Use wiki_query BEFORE answering questions that might have relevant context in the wiki — architecture decisions, past debugging solutions, project conventions, or domain knowledge.",
718
+ "Use wiki_query when the user asks about something you previously discussed or captured in the wiki.",
719
+ "Wiki results include freshness indicators (FRESH/AGING/STALE/VERY STALE). When a page is AGING or STALE and makes claims about code, files, configs, ports, or APIs — verify against the actual source before trusting. If you find the wiki is wrong, fix it immediately with wiki_write(mode: 'edit').",
720
+ ],
721
+ parameters: WikiQueryParams,
722
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
723
+ ctx.ui.setStatus("pi-para", "wiki: searching...");
724
+ try {
725
+ return await queryExec(params);
726
+ }
727
+ finally {
728
+ ctx.ui.setStatus("pi-para", undefined);
729
+ }
730
+ },
731
+ renderCall(args, theme) {
732
+ const q = args.query.length > 50 ? args.query.slice(0, 47) + "..." : args.query;
733
+ let text = theme.fg("toolTitle", theme.bold("wiki_query ")) +
734
+ theme.fg("dim", `"${q}"`);
735
+ if (args.global)
736
+ text += theme.fg("muted", " (global)");
737
+ if (args.category)
738
+ text += theme.fg("muted", ` [${args.category}]`);
739
+ return new Text(text, 0, 0);
740
+ },
741
+ renderResult(result, _options, theme) {
742
+ const details = result.details;
743
+ if (!details) {
744
+ return new Text(theme.fg("dim", "No results"), 0, 0);
745
+ }
746
+ return new Text(theme.fg("success", `${details.resultCount} result(s)`) +
747
+ theme.fg("dim", ` scope: ${details.scopeUsed}`), 0, 0);
748
+ },
749
+ });
750
+ // -- wiki_write --
751
+ pi.registerTool({
752
+ name: "wiki_write",
753
+ label: "Wiki Write",
754
+ description: "Write or update wiki pages. Supports create, replace, append, and edit modes. " +
755
+ "Optionally update index.md and append to log.md. " +
756
+ "Pages are automatically re-indexed for search after writing.",
757
+ promptSnippet: "Write/update PARA wiki pages with frontmatter and markdown body. Use mode=edit with edits[{oldText,newText}] to surgically update sections.",
758
+ promptGuidelines: [
759
+ "Use wiki_write after wiki_ingest to create/update pages based on the ingested content.",
760
+ "Use wiki_write to save valuable answers or synthesis back into the wiki when they add new knowledge.",
761
+ "After making significant architectural decisions, solving debugging problems, establishing project conventions, or completing substantial implementation work, use wiki_write to persist the knowledge without being asked.",
762
+ "Prefer mode=edit with edits[{oldText,newText}] to update specific sections of existing pages. Use mode=append to add new sections. Only use mode=replace when rewriting the entire page.",
763
+ ],
764
+ parameters: WikiWriteParams,
765
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
766
+ ctx.ui.setStatus("pi-para", "wiki: writing...");
767
+ try {
768
+ return await writeExec(params);
769
+ }
770
+ finally {
771
+ ctx.ui.setStatus("pi-para", undefined);
772
+ }
773
+ },
774
+ renderCall(args, theme) {
775
+ const count = args.pages?.length ?? 0;
776
+ const slugs = (args.pages ?? [])
777
+ .slice(0, 3)
778
+ .map((p) => p.slug)
779
+ .join(", ");
780
+ let text = theme.fg("toolTitle", theme.bold("wiki_write ")) +
781
+ theme.fg("muted", `${count} page(s)`);
782
+ if (slugs)
783
+ text += theme.fg("dim", ` [${slugs}]`);
784
+ text += theme.fg("muted", " +index"); // always auto-rebuilds index
785
+ return new Text(text, 0, 0);
786
+ },
787
+ renderResult(result, _options, theme) {
788
+ const details = result.details;
789
+ if (!details) {
790
+ const t = result.content[0];
791
+ return new Text(t?.type === "text" ? t.text : "", 0, 0);
792
+ }
793
+ const parts = [];
794
+ if (details.pagesWritten.length > 0) {
795
+ parts.push(theme.fg("success", `Wrote ${details.pagesWritten.length} page(s): `) +
796
+ theme.fg("muted", details.pagesWritten.join(", ")));
797
+ }
798
+ if (details.indexUpdated)
799
+ parts.push(theme.fg("dim", "Updated index.md"));
800
+ if (details.logAppended)
801
+ parts.push(theme.fg("dim", "Logged operation"));
802
+ return new Text(parts.join("\n") || theme.fg("dim", "No changes"), 0, 0);
803
+ },
804
+ });
805
+ // -- wiki_read --
806
+ pi.registerTool({
807
+ name: "wiki_read",
808
+ label: "Wiki Read",
809
+ description: "Read a specific wiki page by path (e.g. 'projects/auth-refactor') " +
810
+ "or by title. Returns the full page content with frontmatter metadata and a freshness indicator.",
811
+ promptSnippet: "Read a wiki page by path or title. Includes freshness indicator — verify STALE pages against actual code before trusting.",
812
+ promptGuidelines: [
813
+ "wiki_read results include a freshness indicator (FRESH/AGING/STALE/VERY STALE). When a page is AGING or STALE, verify claims about code, configs, or APIs against the actual source before trusting. Fix incorrect wiki pages with wiki_write(mode: 'edit').",
814
+ ],
815
+ parameters: WikiReadParams,
816
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
817
+ ctx.ui.setStatus("pi-para", "wiki: reading...");
818
+ try {
819
+ return await readExec(params);
820
+ }
821
+ finally {
822
+ ctx.ui.setStatus("pi-para", undefined);
823
+ }
824
+ },
825
+ renderCall(args, theme) {
826
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_read ")) +
827
+ theme.fg("dim", args.path), 0, 0);
828
+ },
829
+ renderResult(result, _options, theme) {
830
+ const details = result.details;
831
+ if (!details || !details.found) {
832
+ return new Text(theme.fg("error", "Page not found"), 0, 0);
833
+ }
834
+ return new Text(theme.fg("success", "Found ") + theme.fg("muted", details.path ?? ""), 0, 0);
835
+ },
836
+ });
837
+ // -- wiki_move --
838
+ pi.registerTool({
839
+ name: "wiki_move",
840
+ label: "Wiki Move",
841
+ description: "Move a wiki page between PARA categories (e.g. move a completed project to archives). " +
842
+ "Updates frontmatter and re-indexes.",
843
+ promptSnippet: "Move a wiki page between PARA categories",
844
+ parameters: WikiMoveParams,
845
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
846
+ ctx.ui.setStatus("pi-para", "wiki: moving...");
847
+ try {
848
+ return await moveExec(params);
849
+ }
850
+ finally {
851
+ ctx.ui.setStatus("pi-para", undefined);
852
+ }
853
+ },
854
+ renderCall(args, theme) {
855
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_move ")) +
856
+ theme.fg("dim", `${args.path} -> ${args.to}`), 0, 0);
857
+ },
858
+ renderResult(result, _options, theme) {
859
+ const details = result.details;
860
+ if (!details) {
861
+ const t = result.content[0];
862
+ return new Text(t?.type === "text" ? t.text : "", 0, 0);
863
+ }
864
+ return new Text(theme.fg("success", "Moved ") +
865
+ theme.fg("muted", `${details.from} -> ${details.to}`), 0, 0);
866
+ },
867
+ });
868
+ // -- wiki_lint --
869
+ pi.registerTool({
870
+ name: "wiki_lint",
871
+ label: "Wiki Lint",
872
+ description: "Run wiki health checks: orphan pages, broken links, stale pages, " +
873
+ "scope drift, archive candidates, missing pages, frontmatter issues, " +
874
+ "index drift, and duplicate slugs. Auto-fixes simple issues by default.",
875
+ promptSnippet: "Run wiki health checks and auto-fix issues",
876
+ parameters: WikiLintParams,
877
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
878
+ ctx.ui.setStatus("pi-para", "wiki: linting...");
879
+ try {
880
+ return await lintExec(params);
881
+ }
882
+ finally {
883
+ ctx.ui.setStatus("pi-para", undefined);
884
+ }
885
+ },
886
+ renderCall(args, theme) {
887
+ const mode = args.autoFix === false ? "report-only" : "auto-fix";
888
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_lint ")) +
889
+ theme.fg("muted", `[${mode}]`), 0, 0);
890
+ },
891
+ renderResult(result, _options, theme) {
892
+ const details = result.details;
893
+ if (!details) {
894
+ return new Text(theme.fg("dim", "Lint complete"), 0, 0);
895
+ }
896
+ const parts = [
897
+ theme.fg("muted", `${details.stats.totalPages} pages`),
898
+ ];
899
+ if (details.fixedCount > 0) {
900
+ parts.push(theme.fg("success", `${details.fixedCount} fixed`));
901
+ }
902
+ if (details.issueCount > 0) {
903
+ parts.push(theme.fg("warning", `${details.issueCount} remaining`));
904
+ }
905
+ else {
906
+ parts.push(theme.fg("success", "healthy"));
907
+ }
908
+ return new Text(parts.join(" | "), 0, 0);
909
+ },
910
+ });
911
+ // -- wiki_migrate --
912
+ pi.registerTool({
913
+ name: "wiki_migrate",
914
+ label: "Wiki Migrate",
915
+ description: "Migrate all wiki pages to the current schema version. " +
916
+ "Runs pending schema migrations on any pages with an older schemaVersion.",
917
+ promptSnippet: "Batch-migrate all wiki pages to the current schema version",
918
+ parameters: WikiMigrateParams,
919
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
920
+ ctx.ui.setStatus("pi-para", "wiki: migrating...");
921
+ try {
922
+ return await migrateExec();
923
+ }
924
+ finally {
925
+ ctx.ui.setStatus("pi-para", undefined);
926
+ }
927
+ },
928
+ renderCall(_args, theme) {
929
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_migrate ")) +
930
+ theme.fg("muted", `→ v${CURRENT_SCHEMA_VERSION}`), 0, 0);
931
+ },
932
+ renderResult(result, _options, theme) {
933
+ const details = result.details;
934
+ if (!details) {
935
+ return new Text(theme.fg("dim", "Migration complete"), 0, 0);
936
+ }
937
+ if (details.migratedCount === 0) {
938
+ return new Text(theme.fg("success", `All ${details.totalPages} pages at v${CURRENT_SCHEMA_VERSION}`), 0, 0);
939
+ }
940
+ return new Text(theme.fg("success", `Migrated ${details.migratedCount}/${details.totalPages} page(s)`), 0, 0);
941
+ },
942
+ });
943
+ // -- wiki_summarize --
944
+ pi.registerTool({
945
+ name: "wiki_summarize",
946
+ label: "Wiki Summarize",
947
+ description: "Summarize a page, category, or the entire wiki. " +
948
+ "Target can be a page path (e.g. 'projects/auth-refactor'), " +
949
+ "a category name (e.g. 'projects'), or 'all'. " +
950
+ "Returns content with a summary prompt for you to synthesize.",
951
+ promptSnippet: "Summarize wiki pages, categories, or the entire wiki",
952
+ parameters: WikiSummarizeParams,
953
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
954
+ ctx.ui.setStatus("pi-para", "wiki: summarizing...");
955
+ try {
956
+ return await summarizeExec(params);
957
+ }
958
+ finally {
959
+ ctx.ui.setStatus("pi-para", undefined);
960
+ }
961
+ },
962
+ renderCall(args, theme) {
963
+ let text = theme.fg("toolTitle", theme.bold("wiki_summarize ")) +
964
+ theme.fg("dim", args.target);
965
+ if (args.depth)
966
+ text += theme.fg("muted", ` [${args.depth}]`);
967
+ return new Text(text, 0, 0);
968
+ },
969
+ renderResult(result, _options, theme) {
970
+ const details = result.details;
971
+ if (!details) {
972
+ return new Text(theme.fg("dim", "Summary generated"), 0, 0);
973
+ }
974
+ if (details.pageCount === 0) {
975
+ return new Text(theme.fg("warning", "No pages found"), 0, 0);
976
+ }
977
+ return new Text(theme.fg("success", `Summarizing ${details.pageCount} page(s)`) +
978
+ theme.fg("dim", ` target: ${details.target}`), 0, 0);
979
+ },
980
+ });
981
+ }
982
+ // -- Public API: standalone tools for mini-agent -----------------------------
983
+ /**
984
+ * Create standalone AgentTool instances for the mini-agent (capture/lint/summarize).
985
+ * Returns only the tools needed for autonomous wiki operations:
986
+ * wiki_write, wiki_read, wiki_query, wiki_move.
987
+ */
988
+ export function createStandaloneTools(wikiDir, store, getScope, markDirty) {
989
+ const noopDirty = markDirty ?? (() => { });
990
+ const writeExec = createWriteExecute(wikiDir, store, getScope, noopDirty);
991
+ const readExec = createReadExecute(wikiDir);
992
+ const queryExec = createQueryExecute(wikiDir, store, getScope);
993
+ const moveExec = createMoveExecute(wikiDir, store, noopDirty);
994
+ const wikiWrite = {
995
+ name: "wiki_write",
996
+ label: "Wiki Write",
997
+ description: "Write or update wiki pages. Supports create, replace, and append modes. " +
998
+ "Optionally update index.md and append to log.md.",
999
+ parameters: WikiWriteParams,
1000
+ execute: (_toolCallId, params) => writeExec(params),
1001
+ };
1002
+ const wikiRead = {
1003
+ name: "wiki_read",
1004
+ label: "Wiki Read",
1005
+ description: "Read a specific wiki page by path (e.g. 'projects/auth-refactor') or by title.",
1006
+ parameters: WikiReadParams,
1007
+ execute: (_toolCallId, params) => readExec(params),
1008
+ };
1009
+ const wikiQuery = {
1010
+ name: "wiki_query",
1011
+ label: "Wiki Query",
1012
+ description: "Search the wiki knowledge base with a natural language query. Returns relevant pages.",
1013
+ parameters: WikiQueryParams,
1014
+ execute: (_toolCallId, params) => queryExec(params),
1015
+ };
1016
+ const wikiMove = {
1017
+ name: "wiki_move",
1018
+ label: "Wiki Move",
1019
+ description: "Move a wiki page between PARA categories.",
1020
+ parameters: WikiMoveParams,
1021
+ execute: (_toolCallId, params) => moveExec(params),
1022
+ };
1023
+ return [wikiWrite, wikiRead, wikiQuery, wikiMove];
1024
+ }
1025
+ // -- Utilities ---------------------------------------------------------------
1026
+ function formatBytes(bytes) {
1027
+ if (bytes < 1024)
1028
+ return `${bytes}B`;
1029
+ if (bytes < 1024 * 1024)
1030
+ return `${(bytes / 1024).toFixed(1)}KB`;
1031
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1032
+ }
1033
+ //# sourceMappingURL=tools.js.map