pi-para 0.6.0 → 0.6.1

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 (208) hide show
  1. package/ARCHITECTURE.md +0 -9
  2. package/CHANGELOG.md +13 -0
  3. package/CONFIGURATION.md +0 -23
  4. package/README.md +1 -29
  5. package/SETUP.md +1 -1
  6. package/STRUCTURE.md +0 -9
  7. package/dist/atomic-write.d.ts +2 -0
  8. package/dist/atomic-write.d.ts.map +1 -0
  9. package/dist/atomic-write.js +18 -0
  10. package/dist/atomic-write.js.map +1 -0
  11. package/dist/capture.d.ts +34 -0
  12. package/dist/capture.d.ts.map +1 -0
  13. package/dist/capture.js +461 -0
  14. package/dist/capture.js.map +1 -0
  15. package/dist/cli-legacy.d.ts +4 -0
  16. package/dist/cli-legacy.d.ts.map +1 -0
  17. package/dist/cli-legacy.js +41 -0
  18. package/dist/cli-legacy.js.map +1 -0
  19. package/dist/cli.d.ts +8 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +427 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/commands.d.ts +15 -0
  24. package/dist/commands.d.ts.map +1 -0
  25. package/dist/commands.js +719 -0
  26. package/dist/commands.js.map +1 -0
  27. package/dist/config.d.ts +94 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +228 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +63 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +311 -0
  34. package/dist/context.js.map +1 -0
  35. package/dist/credentials.d.ts +27 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +85 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/daemon.d.ts +49 -0
  40. package/dist/daemon.d.ts.map +1 -0
  41. package/dist/daemon.js +226 -0
  42. package/dist/daemon.js.map +1 -0
  43. package/dist/doctor.d.ts +20 -0
  44. package/dist/doctor.d.ts.map +1 -0
  45. package/dist/doctor.js +176 -0
  46. package/dist/doctor.js.map +1 -0
  47. package/dist/frontmatter.d.ts +59 -0
  48. package/dist/frontmatter.d.ts.map +1 -0
  49. package/dist/frontmatter.js +170 -0
  50. package/dist/frontmatter.js.map +1 -0
  51. package/dist/index.d.ts +21 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +342 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/ingest.d.ts +88 -0
  56. package/dist/ingest.d.ts.map +1 -0
  57. package/dist/ingest.js +105 -0
  58. package/dist/ingest.js.map +1 -0
  59. package/dist/link-discovery.d.ts +40 -0
  60. package/dist/link-discovery.d.ts.map +1 -0
  61. package/dist/link-discovery.js +122 -0
  62. package/dist/link-discovery.js.map +1 -0
  63. package/dist/link-utils.d.ts +30 -0
  64. package/dist/link-utils.d.ts.map +1 -0
  65. package/dist/link-utils.js +145 -0
  66. package/dist/link-utils.js.map +1 -0
  67. package/dist/lint.d.ts +39 -0
  68. package/dist/lint.d.ts.map +1 -0
  69. package/dist/lint.js +827 -0
  70. package/dist/lint.js.map +1 -0
  71. package/dist/maintainer.d.ts +24 -0
  72. package/dist/maintainer.d.ts.map +1 -0
  73. package/dist/maintainer.js +358 -0
  74. package/dist/maintainer.js.map +1 -0
  75. package/dist/model-resolver.d.ts +30 -0
  76. package/dist/model-resolver.d.ts.map +1 -0
  77. package/dist/model-resolver.js +71 -0
  78. package/dist/model-resolver.js.map +1 -0
  79. package/dist/paths.d.ts +20 -0
  80. package/dist/paths.d.ts.map +1 -0
  81. package/dist/paths.js +33 -0
  82. package/dist/paths.js.map +1 -0
  83. package/dist/processor.d.ts +14 -0
  84. package/dist/processor.d.ts.map +1 -0
  85. package/dist/processor.js +361 -0
  86. package/dist/processor.js.map +1 -0
  87. package/dist/qmd-providers.d.ts +12 -0
  88. package/dist/qmd-providers.d.ts.map +1 -0
  89. package/dist/qmd-providers.js +45 -0
  90. package/dist/qmd-providers.js.map +1 -0
  91. package/dist/query.d.ts +69 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +120 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/raw.d.ts +57 -0
  96. package/dist/raw.d.ts.map +1 -0
  97. package/dist/raw.js +219 -0
  98. package/dist/raw.js.map +1 -0
  99. package/dist/redact.d.ts +21 -0
  100. package/dist/redact.d.ts.map +1 -0
  101. package/dist/redact.js +143 -0
  102. package/dist/redact.js.map +1 -0
  103. package/dist/repair.d.ts +11 -0
  104. package/dist/repair.d.ts.map +1 -0
  105. package/dist/repair.js +41 -0
  106. package/dist/repair.js.map +1 -0
  107. package/dist/scheduler/controls.d.ts +35 -0
  108. package/dist/scheduler/controls.d.ts.map +1 -0
  109. package/dist/scheduler/controls.js +98 -0
  110. package/dist/scheduler/controls.js.map +1 -0
  111. package/dist/scheduler/index.d.ts +48 -0
  112. package/dist/scheduler/index.d.ts.map +1 -0
  113. package/dist/scheduler/index.js +193 -0
  114. package/dist/scheduler/index.js.map +1 -0
  115. package/dist/scheduler/leases.d.ts +22 -0
  116. package/dist/scheduler/leases.d.ts.map +1 -0
  117. package/dist/scheduler/leases.js +74 -0
  118. package/dist/scheduler/leases.js.map +1 -0
  119. package/dist/scheduler/session-capture.d.ts +33 -0
  120. package/dist/scheduler/session-capture.d.ts.map +1 -0
  121. package/dist/scheduler/session-capture.js +108 -0
  122. package/dist/scheduler/session-capture.js.map +1 -0
  123. package/dist/scheduler/state.d.ts +56 -0
  124. package/dist/scheduler/state.d.ts.map +1 -0
  125. package/dist/scheduler/state.js +210 -0
  126. package/dist/scheduler/state.js.map +1 -0
  127. package/dist/scope.d.ts +54 -0
  128. package/dist/scope.d.ts.map +1 -0
  129. package/dist/scope.js +161 -0
  130. package/dist/scope.js.map +1 -0
  131. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  132. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  133. package/dist/scripts/bulk-link-fix.js +106 -0
  134. package/dist/scripts/bulk-link-fix.js.map +1 -0
  135. package/dist/session-tools.d.ts +33 -0
  136. package/dist/session-tools.d.ts.map +1 -0
  137. package/dist/session-tools.js +211 -0
  138. package/dist/session-tools.js.map +1 -0
  139. package/dist/settings.d.ts +19 -0
  140. package/dist/settings.d.ts.map +1 -0
  141. package/dist/settings.js +106 -0
  142. package/dist/settings.js.map +1 -0
  143. package/dist/setup.d.ts +29 -0
  144. package/dist/setup.d.ts.map +1 -0
  145. package/dist/setup.js +90 -0
  146. package/dist/setup.js.map +1 -0
  147. package/dist/state.d.ts +54 -0
  148. package/dist/state.d.ts.map +1 -0
  149. package/dist/state.js +176 -0
  150. package/dist/state.js.map +1 -0
  151. package/dist/status.d.ts +22 -0
  152. package/dist/status.d.ts.map +1 -0
  153. package/dist/status.js +93 -0
  154. package/dist/status.js.map +1 -0
  155. package/dist/store.d.ts +71 -0
  156. package/dist/store.d.ts.map +1 -0
  157. package/dist/store.js +282 -0
  158. package/dist/store.js.map +1 -0
  159. package/dist/summarize.d.ts +60 -0
  160. package/dist/summarize.d.ts.map +1 -0
  161. package/dist/summarize.js +266 -0
  162. package/dist/summarize.js.map +1 -0
  163. package/dist/tag-registry.d.ts +56 -0
  164. package/dist/tag-registry.d.ts.map +1 -0
  165. package/dist/tag-registry.js +146 -0
  166. package/dist/tag-registry.js.map +1 -0
  167. package/dist/templates/prompts.d.ts +51 -0
  168. package/dist/templates/prompts.d.ts.map +1 -0
  169. package/dist/templates/prompts.js +203 -0
  170. package/dist/templates/prompts.js.map +1 -0
  171. package/dist/tools.d.ts +26 -0
  172. package/dist/tools.d.ts.map +1 -0
  173. package/dist/tools.js +1198 -0
  174. package/dist/tools.js.map +1 -0
  175. package/dist/watcher.d.ts +25 -0
  176. package/dist/watcher.d.ts.map +1 -0
  177. package/dist/watcher.js +84 -0
  178. package/dist/watcher.js.map +1 -0
  179. package/dist/webui/client/dist/index.html +1885 -0
  180. package/dist/webui/server.d.ts +31 -0
  181. package/dist/webui/server.d.ts.map +1 -0
  182. package/dist/webui/server.js +566 -0
  183. package/dist/webui/server.js.map +1 -0
  184. package/dist/wiki-tool-guidance.d.ts +6 -0
  185. package/dist/wiki-tool-guidance.d.ts.map +1 -0
  186. package/dist/wiki-tool-guidance.js +70 -0
  187. package/dist/wiki-tool-guidance.js.map +1 -0
  188. package/dist/wiki.d.ts +108 -0
  189. package/dist/wiki.d.ts.map +1 -0
  190. package/dist/wiki.js +497 -0
  191. package/dist/wiki.js.map +1 -0
  192. package/package.json +2 -1
  193. package/scripts/build-assets.mjs +1 -13
  194. package/skills/setup/SKILL.md +1 -1
  195. package/src/cli.ts +0 -70
  196. package/src/config.ts +0 -25
  197. package/src/index.ts +1 -38
  198. package/src/templates/prompts.ts +0 -56
  199. package/scripts/gepa/dataset.py +0 -192
  200. package/scripts/gepa/lm_providers.py +0 -472
  201. package/scripts/gepa/metric.py +0 -279
  202. package/scripts/gepa/optimize.py +0 -367
  203. package/scripts/gepa/program.py +0 -99
  204. package/scripts/gepa/pyproject.toml +0 -19
  205. package/scripts/gepa/run-all.sh +0 -53
  206. package/scripts/gepa/wiki_reader.py +0 -113
  207. package/src/gepa/index.ts +0 -287
  208. package/src/gepa/optimized/capture-prompt.txt +0 -65
package/dist/tools.js ADDED
@@ -0,0 +1,1198 @@
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 "@earendil-works/pi-ai";
10
+ import { Text } from "@earendil-works/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 { readPage, writePage, movePage, listPages, listPageSlugs, readIndex, writeIndex, readSchema, appendLog, gitCommit, rebuildIndex, PARA_CATEGORIES, } from "./wiki.js";
15
+ import { validateFrontmatter, parseFrontmatter, CURRENT_SCHEMA_VERSION, migrateToLatest, } from "./frontmatter.js";
16
+ import { extractWikilinks, autoLinkSlugs, syncFrontmatterLinks } from "./link-utils.js";
17
+ import { normalizeTags, normalizeScopes } from "./tag-registry.js";
18
+ import { redactSecrets } from "./redact.js";
19
+ import { lintWiki } from "./lint.js";
20
+ import { generateOverviewPrompt } from "./summarize.js";
21
+ import { INGEST_PROMPT, QUERY_PROMPT, LINT_PROMPT, } from "./templates/prompts.js";
22
+ import { enqueueWikiMaintenance } from "./scheduler/index.js";
23
+ import { WIKI_TOOL_DESCRIPTIONS, WIKI_TOOL_SNIPPETS, getWikiToolGuidelines, } from "./wiki-tool-guidance.js";
24
+ // -- Parameter schemas -------------------------------------------------------
25
+ const PARA_ENUM = StringEnum(["projects", "areas", "resources", "archives"], { description: "PARA category" });
26
+ const PARA_ENUM_NO_ARCHIVES = StringEnum(["projects", "areas", "resources"], { description: "PARA category (archives excluded for new content)" });
27
+ const WikiIngestParams = Type.Object({
28
+ source: Type.String({ description: "URL, file path, or raw text to ingest" }),
29
+ sourceType: Type.Optional(StringEnum(["url", "file", "text"], {
30
+ description: "Source type (auto-detected if omitted)",
31
+ })),
32
+ category: Type.Optional(PARA_ENUM_NO_ARCHIVES),
33
+ scope: Type.Optional(Type.Array(Type.String(), { description: "Scope tags for the ingested content" })),
34
+ });
35
+ const WikiQueryParams = Type.Object({
36
+ query: Type.String({ description: "Natural language search query" }),
37
+ global: Type.Optional(Type.Boolean({ description: "Search all scopes, not just current project" })),
38
+ category: Type.Optional(PARA_ENUM),
39
+ limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
40
+ });
41
+ const WikiEditSchema = Type.Object({
42
+ oldText: Type.String({ description: "Exact text to find in the page body" }),
43
+ newText: Type.String({ description: "Replacement text" }),
44
+ });
45
+ const WikiWritePageSchema = Type.Object({
46
+ category: PARA_ENUM,
47
+ slug: Type.String({ description: "Page slug (lowercase, hyphens)" }),
48
+ title: Type.String({ description: "Page title" }),
49
+ scope: Type.Array(Type.String(), { description: "Scope tags" }),
50
+ tags: Type.Array(Type.String(), { description: "Topic tags" }),
51
+ body: Type.String({ description: "Page body in markdown (wiki summary format)" }),
52
+ mode: StringEnum(["create", "replace", "append", "edit"], {
53
+ description: "Write mode: create new, replace existing, append to existing, or edit specific sections",
54
+ }),
55
+ edits: Type.Optional(Type.Array(WikiEditSchema, {
56
+ description: "For mode=edit: targeted text replacements (oldText→newText). Page must exist.",
57
+ })),
58
+ });
59
+ const WikiWriteParams = Type.Object({
60
+ pages: Type.Array(WikiWritePageSchema, {
61
+ description: "Pages to write or update",
62
+ }),
63
+ // DEPRECATED: indexContent is ignored. Index is auto-rebuilt from all pages on disk.
64
+ // Kept in schema to avoid breaking existing callers.
65
+ indexContent: Type.Optional(Type.String({ description: "Updated index.md content (full replacement)" })),
66
+ logSummary: Type.Optional(Type.String({ description: "One-line summary for log.md" })),
67
+ });
68
+ const WikiEditParams = Type.Object({
69
+ path: Type.String({ description: "Existing page path (e.g. 'resources/ssl-cert-gotchas')" }),
70
+ edits: Type.Array(WikiEditSchema, {
71
+ description: "Atomic exact text replacements. Every oldText must appear exactly once or no changes are written.",
72
+ }),
73
+ title: Type.Optional(Type.String({ description: "Optional replacement page title" })),
74
+ scope: Type.Optional(Type.Array(Type.String(), { description: "Optional replacement scope tags" })),
75
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Optional replacement topic tags" })),
76
+ logSummary: Type.Optional(Type.String({ description: "One-line summary for log.md" })),
77
+ });
78
+ const WikiReadParams = Type.Object({
79
+ path: Type.String({
80
+ description: "Page path (e.g. 'projects/auth-refactor') or page title",
81
+ }),
82
+ });
83
+ const WikiMoveParams = Type.Object({
84
+ path: Type.String({
85
+ description: "Current page path (e.g. 'projects/auth-refactor')",
86
+ }),
87
+ to: PARA_ENUM,
88
+ });
89
+ const WikiLintParams = Type.Object({
90
+ autoFix: Type.Optional(Type.Boolean({ description: "Auto-fix simple issues (default true)" })),
91
+ });
92
+ const WikiMigrateParams = Type.Object({});
93
+ const WikiSummarizeParams = Type.Object({
94
+ target: Type.String({
95
+ description: "Page path, category name (e.g. 'projects'), or 'all'",
96
+ }),
97
+ depth: Type.Optional(StringEnum(["brief", "detailed"], {
98
+ description: "Summary depth (default brief)",
99
+ })),
100
+ });
101
+ // -- Source resolution helpers -----------------------------------------------
102
+ /** Resolve a page path like "projects/auth-refactor" into category + slug. */
103
+ function parsePagePath(path) {
104
+ const parts = path.split("/");
105
+ if (parts.length === 2) {
106
+ const cat = parts[0];
107
+ if (PARA_CATEGORIES.includes(cat)) {
108
+ const slug = parts[1].replace(/\.md$/, "");
109
+ return { category: cat, slug };
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+ // -- Factory: wiki_ingest execute --------------------------------------------
115
+ function createIngestExecute(wikiDir, _store, getScope) {
116
+ return async (params) => {
117
+ const scope = getScope();
118
+ const resolved = await resolveSource(wikiDir, {
119
+ source: params.source,
120
+ sourceType: params.sourceType,
121
+ scope: params.scope,
122
+ category: params.category,
123
+ }, scope);
124
+ const categoryHint = resolved.categoryHint
125
+ ? `\nSuggested PARA category: ${resolved.categoryHint}`
126
+ : "";
127
+ // Assemble the tool result with instructions for the LLM
128
+ const toolResultText = [
129
+ INGEST_PROMPT,
130
+ "",
131
+ `Current scope: ${resolved.scopeName} (tags: ${resolved.scopeTags.join(", ")})`,
132
+ categoryHint,
133
+ resolved.rawPath ? `Raw source saved to: ${resolved.rawPath}` : "",
134
+ "",
135
+ "<wiki-schema>",
136
+ resolved.schema,
137
+ "</wiki-schema>",
138
+ "",
139
+ "<wiki-index>",
140
+ resolved.index,
141
+ "</wiki-index>",
142
+ "",
143
+ "<source-content>",
144
+ truncateSource(resolved.content),
145
+ "</source-content>",
146
+ "",
147
+ "Now analyze the source content and use wiki_write to create/update pages, update the index, and log the operation.",
148
+ ].join("\n");
149
+ return {
150
+ content: [{ type: "text", text: toolResultText }],
151
+ details: {
152
+ sourceType: resolved.sourceType,
153
+ rawPath: resolved.rawPath,
154
+ sourceLength: resolved.content.length,
155
+ },
156
+ };
157
+ };
158
+ }
159
+ // -- Factory: wiki_query execute ---------------------------------------------
160
+ function createQueryExecute(_wikiDir, store, getScope, getGraphBoost) {
161
+ return async (params) => {
162
+ const scope = getScope();
163
+ const result = await queryWikiLib(store, {
164
+ query: params.query,
165
+ global: params.global,
166
+ category: params.category,
167
+ limit: params.limit,
168
+ graphBoost: getGraphBoost?.() ?? true,
169
+ }, scope);
170
+ if (result.results.length === 0) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `No wiki pages found for "${params.query}". The wiki may not have content on this topic yet.`,
176
+ },
177
+ ],
178
+ details: {
179
+ resultCount: 0,
180
+ scopeUsed: result.wasGlobal ? "global" : scope.name,
181
+ },
182
+ };
183
+ }
184
+ const formatted = formatQueryResults(result.results);
185
+ const toolResultText = [
186
+ QUERY_PROMPT,
187
+ "",
188
+ `Query: "${params.query}"`,
189
+ `Scope: ${result.wasGlobal ? "global (all scopes)" : scope.name}`,
190
+ `Results: ${result.results.length}`,
191
+ "",
192
+ formatted,
193
+ ].join("\n");
194
+ return {
195
+ content: [{ type: "text", text: toolResultText }],
196
+ details: {
197
+ resultCount: result.results.length,
198
+ scopeUsed: result.wasGlobal ? "global" : scope.name,
199
+ },
200
+ };
201
+ };
202
+ }
203
+ // -- Background maintenance --------------------------------------------------
204
+ /**
205
+ * Schedule index rebuild + QMD reindex after latency-sensitive edits.
206
+ * Debounced per wiki directory and coordinated through the scheduler queue so
207
+ * multiple Pi processes do not run the same maintenance task concurrently.
208
+ */
209
+ function scheduleWikiMaintenance(wikiDir, store, markDirty) {
210
+ enqueueWikiMaintenance(wikiDir, store, markDirty);
211
+ }
212
+ // -- Factory: wiki_write execute ---------------------------------------------
213
+ function createWriteExecute(wikiDir, store, getScope, markDirty) {
214
+ return async (params) => {
215
+ const pagesWritten = [];
216
+ const pagesSkipped = [];
217
+ const now = new Date().toISOString();
218
+ const scope = getScope();
219
+ // Gather all existing slugs for auto-linking (slug-only readdir scan;
220
+ // reading/parsing every page here made wiki_write scale poorly)
221
+ const allSlugs = await listPageSlugs(wikiDir);
222
+ for (const pageSpec of params.pages) {
223
+ // Sanitize slug
224
+ const slug = pageSpec.slug
225
+ .toLowerCase()
226
+ .replace(/[^a-z0-9-]/g, "-")
227
+ .replace(/-+/g, "-")
228
+ .replace(/^-|-$/g, "");
229
+ if (!slug) {
230
+ continue; // skip invalid slugs
231
+ }
232
+ const existing = await readPage(wikiDir, pageSpec.category, slug);
233
+ const pagePath = `${pageSpec.category}/${slug}`;
234
+ // Track this slug for auto-linking other pages in this batch
235
+ allSlugs.add(slug);
236
+ if (pageSpec.mode === "create" && existing) {
237
+ pagesSkipped.push(`${pagePath} (already exists; use mode=replace, mode=append, or wiki_edit)`);
238
+ continue;
239
+ }
240
+ if (pageSpec.mode === "edit" && !existing) {
241
+ pagesSkipped.push(`${pagePath} (missing; wiki_write mode=edit requires an existing page)`);
242
+ continue;
243
+ }
244
+ if (pageSpec.mode === "edit" && existing) {
245
+ // Legacy edit mode: keep for compatibility, but make it atomic like wiki_edit.
246
+ const edits = pageSpec.edits ?? [];
247
+ if (edits.length === 0) {
248
+ continue; // no edits to apply
249
+ }
250
+ const errors = [];
251
+ for (const edit of edits) {
252
+ const first = existing.body.indexOf(edit.oldText);
253
+ const last = existing.body.lastIndexOf(edit.oldText);
254
+ if (first === -1) {
255
+ errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
256
+ }
257
+ else if (first !== last) {
258
+ errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
259
+ }
260
+ }
261
+ if (errors.length > 0) {
262
+ pagesSkipped.push(`${pagePath} (${errors.join("; ")})`);
263
+ continue;
264
+ }
265
+ let editedBody = existing.body;
266
+ for (const edit of edits) {
267
+ editedBody = editedBody.replace(edit.oldText, edit.newText);
268
+ }
269
+ const linkedBody = redactSecrets(autoLinkSlugs(editedBody, allSlugs, slug)).text;
270
+ const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : existing.frontmatter.scope;
271
+ const newScope = normalizeScopes(rawScope);
272
+ const newTags = pageSpec.tags.length > 0 ? pageSpec.tags : existing.frontmatter.tags;
273
+ const updatedPage = {
274
+ category: pageSpec.category,
275
+ slug,
276
+ frontmatter: {
277
+ ...existing.frontmatter,
278
+ title: pageSpec.title || existing.frontmatter.title,
279
+ scope: newScope,
280
+ tags: normalizeTags(newTags, newScope),
281
+ updated: now,
282
+ links: syncFrontmatterLinks(linkedBody),
283
+ },
284
+ body: linkedBody,
285
+ };
286
+ await writePage(wikiDir, updatedPage);
287
+ pagesWritten.push(pagePath + (errors.length > 0 ? ` (${errors.length} edit(s) failed)` : ""));
288
+ }
289
+ else if (pageSpec.mode === "append" && existing) {
290
+ // Append to existing page body
291
+ const combinedBody = existing.body.trimEnd() + "\n\n" + pageSpec.body;
292
+ const linkedBody = redactSecrets(autoLinkSlugs(combinedBody, allSlugs, slug)).text;
293
+ const mergedTags = [...new Set([...existing.frontmatter.tags, ...pageSpec.tags])];
294
+ const mergedScope = normalizeScopes([...new Set([...existing.frontmatter.scope, ...pageSpec.scope])]);
295
+ const updatedPage = {
296
+ ...existing,
297
+ body: linkedBody,
298
+ frontmatter: {
299
+ ...existing.frontmatter,
300
+ updated: now,
301
+ tags: normalizeTags(mergedTags, mergedScope),
302
+ scope: mergedScope,
303
+ links: syncFrontmatterLinks(linkedBody),
304
+ },
305
+ };
306
+ await writePage(wikiDir, updatedPage);
307
+ pagesWritten.push(pagePath);
308
+ }
309
+ else if (pageSpec.mode === "replace" && existing) {
310
+ // Replace existing page body, update frontmatter
311
+ const linkedBody = redactSecrets(autoLinkSlugs(pageSpec.body, allSlugs, slug)).text;
312
+ const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : existing.frontmatter.scope;
313
+ const newScope = normalizeScopes(rawScope);
314
+ const newTags = pageSpec.tags.length > 0 ? pageSpec.tags : existing.frontmatter.tags;
315
+ const updatedPage = {
316
+ category: pageSpec.category,
317
+ slug,
318
+ frontmatter: {
319
+ ...existing.frontmatter,
320
+ title: pageSpec.title,
321
+ para: pageSpec.category,
322
+ scope: newScope,
323
+ tags: normalizeTags(newTags, newScope),
324
+ updated: now,
325
+ links: syncFrontmatterLinks(linkedBody),
326
+ },
327
+ body: linkedBody,
328
+ };
329
+ await writePage(wikiDir, updatedPage);
330
+ pagesWritten.push(pagePath);
331
+ }
332
+ else {
333
+ // Create new page (or mode=create/replace on non-existing)
334
+ const linkedBody = redactSecrets(autoLinkSlugs(pageSpec.body, allSlugs, slug)).text;
335
+ const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : [scope.name];
336
+ const newScope = normalizeScopes(rawScope);
337
+ const frontmatter = validateFrontmatter({
338
+ title: pageSpec.title,
339
+ para: pageSpec.category,
340
+ scope: newScope,
341
+ tags: normalizeTags(pageSpec.tags, newScope),
342
+ sources: [],
343
+ created: now,
344
+ updated: now,
345
+ links: syncFrontmatterLinks(linkedBody),
346
+ });
347
+ const newPage = {
348
+ category: pageSpec.category,
349
+ slug,
350
+ frontmatter,
351
+ body: linkedBody,
352
+ };
353
+ await writePage(wikiDir, newPage);
354
+ pagesWritten.push(pagePath);
355
+ }
356
+ }
357
+ // Always auto-rebuild index from disk.
358
+ // The LLM's indexContent is unreliable — it only knows about pages
359
+ // in its context, so it produces partial indexes that overwrite
360
+ // the full one. indexContent parameter is deprecated and ignored.
361
+ let indexUpdated = false;
362
+ if (pagesWritten.length > 0) {
363
+ await rebuildIndex(wikiDir);
364
+ indexUpdated = true;
365
+ }
366
+ // Append to log.md if summary provided
367
+ let logAppended = false;
368
+ if (params.logSummary && pagesWritten.length > 0) {
369
+ await appendLog(wikiDir, {
370
+ date: now.split("T")[0],
371
+ operation: "ingest",
372
+ summary: params.logSummary,
373
+ pages: pagesWritten,
374
+ });
375
+ logAppended = true;
376
+ }
377
+ // Defer QMD BM25 reindex to the debounced background maintenance queue
378
+ // (same as wiki_edit). Immediate post-write queries are safe: queryWiki
379
+ // retries with a fresh store.update() before returning zero results.
380
+ if (pagesWritten.length > 0) {
381
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
382
+ }
383
+ // Mark context cache dirty so before_agent_start rebuilds
384
+ markDirty();
385
+ // Git auto-commit
386
+ if (pagesWritten.length > 0) {
387
+ const commitMsg = params.logSummary
388
+ ? params.logSummary
389
+ : `wiki: ${pagesWritten.join(", ")}`;
390
+ await gitCommit(wikiDir, commitMsg);
391
+ }
392
+ const summary = [
393
+ pagesWritten.length > 0
394
+ ? `Wrote ${pagesWritten.length} page(s): ${pagesWritten.join(", ")}`
395
+ : "No pages written.",
396
+ pagesSkipped.length > 0 ? `Skipped ${pagesSkipped.length} page(s): ${pagesSkipped.join(", ")}` : "",
397
+ ].filter(Boolean).join("\n");
398
+ return {
399
+ content: [
400
+ {
401
+ type: "text",
402
+ text: [
403
+ summary,
404
+ indexUpdated ? "Updated index.md." : "",
405
+ logAppended ? `Logged: ${params.logSummary}` : "",
406
+ ]
407
+ .filter(Boolean)
408
+ .join("\n"),
409
+ },
410
+ ],
411
+ details: {
412
+ pagesWritten,
413
+ indexUpdated,
414
+ logAppended,
415
+ pagesSkipped,
416
+ },
417
+ };
418
+ };
419
+ }
420
+ // -- Factory: wiki_read execute ----------------------------------------------
421
+ function createEditExecute(wikiDir, store, markDirty) {
422
+ return async (params) => {
423
+ const parsed = parsePagePath(params.path);
424
+ if (!parsed) {
425
+ return {
426
+ content: [{ type: "text", text: `Invalid page path: ${params.path}` }],
427
+ details: { pagesWritten: [], pagesSkipped: [params.path], indexUpdated: false, logAppended: false },
428
+ };
429
+ }
430
+ const existing = await readPage(wikiDir, parsed.category, parsed.slug);
431
+ const pagePath = `${parsed.category}/${parsed.slug}`;
432
+ if (!existing) {
433
+ return {
434
+ content: [{ type: "text", text: `Page not found: ${pagePath}` }],
435
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
436
+ };
437
+ }
438
+ if (params.edits.length === 0) {
439
+ return {
440
+ content: [{ type: "text", text: "No edits supplied." }],
441
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
442
+ };
443
+ }
444
+ // Atomic validation: all oldText strings must exist exactly once before writing.
445
+ const errors = [];
446
+ for (const edit of params.edits) {
447
+ const first = existing.body.indexOf(edit.oldText);
448
+ const last = existing.body.lastIndexOf(edit.oldText);
449
+ if (first === -1) {
450
+ errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
451
+ }
452
+ else if (first !== last) {
453
+ errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
454
+ }
455
+ }
456
+ if (errors.length > 0) {
457
+ return {
458
+ content: [{ type: "text", text: `No changes written.\n${errors.join("\n")}` }],
459
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
460
+ };
461
+ }
462
+ let editedBody = existing.body;
463
+ for (const edit of params.edits) {
464
+ editedBody = editedBody.replace(edit.oldText, edit.newText);
465
+ }
466
+ // Surgical edit path: do not scan/autolink/rebuild/reindex the whole wiki.
467
+ // wiki_edit is often called mid-task and must stay fast; broader maintenance
468
+ // can run via wiki_lint/rebuildIndex or on startup.
469
+ const linkedBody = redactSecrets(editedBody).text;
470
+ const now = new Date().toISOString();
471
+ const rawScope = params.scope ?? existing.frontmatter.scope;
472
+ const newScope = normalizeScopes(rawScope);
473
+ const rawTags = params.tags ?? existing.frontmatter.tags;
474
+ const updatedPage = {
475
+ category: parsed.category,
476
+ slug: parsed.slug,
477
+ frontmatter: {
478
+ ...existing.frontmatter,
479
+ title: params.title ?? existing.frontmatter.title,
480
+ scope: newScope,
481
+ tags: normalizeTags(rawTags, newScope),
482
+ updated: now,
483
+ links: syncFrontmatterLinks(linkedBody),
484
+ },
485
+ body: linkedBody,
486
+ };
487
+ await writePage(wikiDir, updatedPage);
488
+ if (params.logSummary) {
489
+ await appendLog(wikiDir, {
490
+ date: now.split("T")[0],
491
+ operation: "edit",
492
+ summary: params.logSummary,
493
+ pages: [pagePath],
494
+ });
495
+ }
496
+ markDirty();
497
+ await gitCommit(wikiDir, params.logSummary ?? `wiki_edit: ${pagePath}`);
498
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
499
+ return {
500
+ content: [{ type: "text", text: `Edited ${pagePath}. Scheduled background index/search refresh.` }],
501
+ details: {
502
+ pagesWritten: [pagePath],
503
+ pagesSkipped: [],
504
+ indexUpdated: false,
505
+ logAppended: !!params.logSummary,
506
+ },
507
+ };
508
+ };
509
+ }
510
+ function createReadExecute(wikiDir) {
511
+ return async (params) => {
512
+ const now = Date.now();
513
+ const normalizedPath = params.path.trim().toLowerCase().replace(/^\/+/, "");
514
+ if (normalizedPath === "index" || normalizedPath === "index.md" || normalizedPath === "wiki-index") {
515
+ const index = await readIndex(wikiDir);
516
+ return {
517
+ content: [{ type: "text", text: index }],
518
+ details: { found: true, path: "index.md" },
519
+ };
520
+ }
521
+ // Try parsing as category/slug path
522
+ const parsed = parsePagePath(params.path);
523
+ if (parsed) {
524
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
525
+ if (page) {
526
+ const fm = page.frontmatter;
527
+ const freshness = formatFreshness(fm.updated, now);
528
+ const header = [
529
+ `# ${fm.title}`,
530
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
531
+ `Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
532
+ fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
533
+ fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
534
+ ]
535
+ .filter(Boolean)
536
+ .join("\n");
537
+ return {
538
+ content: [{ type: "text", text: `${header}\n\n${page.body}` }],
539
+ details: { found: true, path: `${parsed.category}/${parsed.slug}` },
540
+ };
541
+ }
542
+ }
543
+ // Try finding by title (case-insensitive search across all pages)
544
+ const allPages = await listPages(wikiDir);
545
+ const titleLower = params.path.toLowerCase();
546
+ for (const ref of allPages) {
547
+ if (ref.title.toLowerCase() === titleLower || ref.slug === titleLower) {
548
+ const page = await readPage(wikiDir, ref.category, ref.slug);
549
+ if (page) {
550
+ const fm = page.frontmatter;
551
+ const freshness = formatFreshness(fm.updated, now);
552
+ const header = [
553
+ `# ${fm.title}`,
554
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
555
+ `Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
556
+ fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
557
+ fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
558
+ ]
559
+ .filter(Boolean)
560
+ .join("\n");
561
+ return {
562
+ content: [{ type: "text", text: `${header}\n\n${page.body}` }],
563
+ details: { found: true, path: `${ref.category}/${ref.slug}` },
564
+ };
565
+ }
566
+ }
567
+ }
568
+ return {
569
+ content: [{ type: "text", text: `Page not found: "${params.path}"` }],
570
+ details: { found: false },
571
+ };
572
+ };
573
+ }
574
+ // -- Factory: wiki_move execute ----------------------------------------------
575
+ function createMoveExecute(wikiDir, store, markDirty) {
576
+ return async (params) => {
577
+ const parsed = parsePagePath(params.path);
578
+ if (!parsed) {
579
+ throw new Error(`Invalid page path: "${params.path}". Expected format: category/slug`);
580
+ }
581
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
582
+ if (!page) {
583
+ throw new Error(`Page not found: "${params.path}"`);
584
+ }
585
+ if (parsed.category === params.to) {
586
+ return {
587
+ content: [
588
+ {
589
+ type: "text",
590
+ text: `Page "${params.path}" is already in ${params.to}. No move needed.`,
591
+ },
592
+ ],
593
+ details: { from: params.path, to: `${params.to}/${parsed.slug}` },
594
+ };
595
+ }
596
+ const ref = {
597
+ category: parsed.category,
598
+ slug: parsed.slug,
599
+ title: page.frontmatter.title,
600
+ path: `${parsed.category}/${parsed.slug}.md`,
601
+ };
602
+ await movePage(wikiDir, ref, params.to);
603
+ // Rebuild index.md inline (fast, single-pass); defer QMD reindex to the
604
+ // debounced background maintenance queue like wiki_write/wiki_edit.
605
+ await rebuildIndex(wikiDir);
606
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
607
+ markDirty();
608
+ // Git commit
609
+ await gitCommit(wikiDir, `move: ${page.frontmatter.title} → ${params.to}`);
610
+ // Log the move
611
+ const now = new Date().toISOString();
612
+ await appendLog(wikiDir, {
613
+ date: now.split("T")[0],
614
+ operation: "move",
615
+ summary: `Moved ${page.frontmatter.title} from ${parsed.category} to ${params.to}`,
616
+ pages: [`${params.to}/${parsed.slug}`],
617
+ });
618
+ return {
619
+ content: [
620
+ {
621
+ type: "text",
622
+ text: `Moved "${page.frontmatter.title}" from ${parsed.category}/ to ${params.to}/`,
623
+ },
624
+ ],
625
+ details: {
626
+ from: `${parsed.category}/${parsed.slug}`,
627
+ to: `${params.to}/${parsed.slug}`,
628
+ },
629
+ };
630
+ };
631
+ }
632
+ // -- Factory: wiki_lint execute ----------------------------------------------
633
+ function createLintExecute(wikiDir) {
634
+ return async (params) => {
635
+ const report = await lintWiki(wikiDir, {
636
+ autoFix: params.autoFix ?? true,
637
+ });
638
+ const { issues, fixed, stats } = report;
639
+ // Format report for the LLM
640
+ const parts = [];
641
+ parts.push("## Wiki Lint Report\n");
642
+ parts.push(`Total pages: ${stats.totalPages}`);
643
+ parts.push(`By category: ${PARA_CATEGORIES.map((c) => `${c}: ${stats.byCategory[c]}`).join(", ")}`);
644
+ parts.push(`Total links: ${stats.totalLinks} (broken: ${stats.brokenLinks})`);
645
+ parts.push(`Orphan pages: ${stats.orphanPages}`);
646
+ if (stats.oldestPage)
647
+ parts.push(`Oldest page: ${stats.oldestPage}`);
648
+ if (stats.newestPage)
649
+ parts.push(`Newest page: ${stats.newestPage}`);
650
+ if (stats.lastIngest)
651
+ parts.push(`Last ingest: ${stats.lastIngest}`);
652
+ if (fixed.length > 0) {
653
+ parts.push(`\n## Auto-fixed (${fixed.length})\n`);
654
+ for (const f of fixed) {
655
+ parts.push(`- [${f.severity}] ${f.page ?? ""}: ${f.message}`);
656
+ }
657
+ }
658
+ if (issues.length > 0) {
659
+ parts.push(`\n## Remaining Issues (${issues.length})\n`);
660
+ for (const issue of issues) {
661
+ parts.push(`- [${issue.severity}] ${issue.category}${issue.page ? ` (${issue.page})` : ""}: ${issue.message}`);
662
+ }
663
+ parts.push("");
664
+ parts.push(LINT_PROMPT);
665
+ }
666
+ else {
667
+ parts.push("\nNo remaining issues. Wiki is healthy.");
668
+ }
669
+ return {
670
+ content: [{ type: "text", text: parts.join("\n") }],
671
+ details: {
672
+ issueCount: issues.length,
673
+ fixedCount: fixed.length,
674
+ stats,
675
+ },
676
+ };
677
+ };
678
+ }
679
+ // -- Factory: wiki_summarize execute -----------------------------------------
680
+ function createSummarizeExecute(wikiDir, getScope) {
681
+ return async (params) => {
682
+ const scope = getScope();
683
+ const pages = [];
684
+ if (params.target === "all") {
685
+ // Summarize entire wiki
686
+ const refs = await listPages(wikiDir);
687
+ for (const ref of refs) {
688
+ const page = await readPage(wikiDir, ref.category, ref.slug);
689
+ if (page)
690
+ pages.push(page);
691
+ }
692
+ }
693
+ else if (PARA_CATEGORIES.includes(params.target)) {
694
+ // Summarize a category
695
+ const refs = await listPages(wikiDir, params.target);
696
+ for (const ref of refs) {
697
+ const page = await readPage(wikiDir, ref.category, ref.slug);
698
+ if (page)
699
+ pages.push(page);
700
+ }
701
+ }
702
+ else {
703
+ // Summarize a single page
704
+ const parsed = parsePagePath(params.target);
705
+ if (parsed) {
706
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
707
+ if (page)
708
+ pages.push(page);
709
+ }
710
+ else {
711
+ // Try finding by slug alone across categories
712
+ const allPages = await listPages(wikiDir);
713
+ const targetLower = params.target.toLowerCase();
714
+ for (const ref of allPages) {
715
+ if (ref.slug === targetLower || ref.title.toLowerCase() === targetLower) {
716
+ const page = await readPage(wikiDir, ref.category, ref.slug);
717
+ if (page)
718
+ pages.push(page);
719
+ break;
720
+ }
721
+ }
722
+ }
723
+ }
724
+ if (pages.length === 0) {
725
+ return {
726
+ content: [
727
+ {
728
+ type: "text",
729
+ text: `No pages found for target "${params.target}". Nothing to summarize.`,
730
+ },
731
+ ],
732
+ details: { target: params.target, pageCount: 0 },
733
+ };
734
+ }
735
+ // Generate a bounded overview prompt for the LLM. For large targets like
736
+ // `all`, brief mode must not dump every full page body into model context.
737
+ const depth = params.depth ?? "brief";
738
+ const overviewPrompt = generateOverviewPrompt(pages, scope, { depth });
739
+ const depthNote = depth === "detailed"
740
+ ? "\nProvide a DETAILED summary with full Key Facts, Insights, and Connections for each theme."
741
+ : "\nProvide a BRIEF overview — one paragraph per theme, focus on high-level patterns.";
742
+ return {
743
+ content: [
744
+ {
745
+ type: "text",
746
+ text: overviewPrompt + depthNote +
747
+ "\n\nYou may optionally create a summary page via wiki_write if the overview is worth preserving.",
748
+ },
749
+ ],
750
+ details: {
751
+ target: params.target,
752
+ pageCount: pages.length,
753
+ },
754
+ };
755
+ };
756
+ }
757
+ // -- Factory: wiki_migrate execute -------------------------------------------
758
+ function createMigrateExecute(wikiDir) {
759
+ return async () => {
760
+ const allRefs = await listPages(wikiDir);
761
+ let migratedCount = 0;
762
+ for (const ref of allRefs) {
763
+ const page = await readPage(wikiDir, ref.category, ref.slug);
764
+ if (!page)
765
+ continue;
766
+ const version = page.frontmatter.schemaVersion ?? 1;
767
+ if (version >= CURRENT_SCHEMA_VERSION)
768
+ continue;
769
+ const rawObj = page.frontmatter;
770
+ const result = migrateToLatest(rawObj, page.body);
771
+ const migratedFm = result.fm;
772
+ migratedFm.updated = new Date().toISOString();
773
+ await writePage(wikiDir, {
774
+ category: ref.category,
775
+ slug: ref.slug,
776
+ frontmatter: migratedFm,
777
+ body: result.body,
778
+ });
779
+ migratedCount++;
780
+ }
781
+ const summary = migratedCount > 0
782
+ ? `Migrated ${migratedCount} page(s) to schema version ${CURRENT_SCHEMA_VERSION}.`
783
+ : `All ${allRefs.length} page(s) already at schema version ${CURRENT_SCHEMA_VERSION}.`;
784
+ return {
785
+ content: [{ type: "text", text: summary }],
786
+ details: { migratedCount, totalPages: allRefs.length },
787
+ };
788
+ };
789
+ }
790
+ // -- Wikilink extraction helper (re-exported from link-utils) ----------------
791
+ // extractWikilinks, autoLinkSlugs, syncFrontmatterLinks imported above
792
+ // -- Public API: register tools for pi session -------------------------------
793
+ /**
794
+ * Register all wiki tools with the pi extension API.
795
+ *
796
+ * Tools: wiki_ingest, wiki_query, wiki_write, wiki_edit, wiki_read,
797
+ * wiki_move, wiki_lint, wiki_summarize
798
+ */
799
+ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphBoost) {
800
+ const ingestExec = createIngestExecute(wikiDir, store, getScope);
801
+ const queryExec = createQueryExecute(wikiDir, store, getScope, getGraphBoost);
802
+ const writeExec = createWriteExecute(wikiDir, store, getScope, markDirty);
803
+ const editExec = createEditExecute(wikiDir, store, markDirty);
804
+ const readExec = createReadExecute(wikiDir);
805
+ const moveExec = createMoveExecute(wikiDir, store, markDirty);
806
+ const lintExec = createLintExecute(wikiDir);
807
+ const migrateExec = createMigrateExecute(wikiDir);
808
+ const summarizeExec = createSummarizeExecute(wikiDir, getScope);
809
+ // -- wiki_ingest --
810
+ pi.registerTool({
811
+ name: "wiki_ingest",
812
+ label: "Wiki Ingest",
813
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_ingest,
814
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_ingest,
815
+ promptGuidelines: getWikiToolGuidelines("wiki_ingest"),
816
+ parameters: WikiIngestParams,
817
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
818
+ ctx.ui.setStatus("pi-para", "wiki: ingesting...");
819
+ try {
820
+ return await ingestExec(params);
821
+ }
822
+ finally {
823
+ ctx.ui.setStatus("pi-para", undefined);
824
+ }
825
+ },
826
+ renderCall(args, theme) {
827
+ const srcType = args.sourceType ?? detectSourceType(args.source);
828
+ const src = args.source.length > 60
829
+ ? args.source.slice(0, 57) + "..."
830
+ : args.source;
831
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_ingest ")) +
832
+ theme.fg("muted", `[${srcType}] `) +
833
+ theme.fg("dim", src), 0, 0);
834
+ },
835
+ renderResult(result, _options, theme) {
836
+ const details = result.details;
837
+ if (!details) {
838
+ const t = result.content[0];
839
+ return new Text(t?.type === "text" ? t.text.slice(0, 80) : "", 0, 0);
840
+ }
841
+ const parts = [
842
+ theme.fg("success", "Fetched ") +
843
+ theme.fg("muted", `${details.sourceType} (${formatBytes(details.sourceLength)})`),
844
+ ];
845
+ if (details.rawPath) {
846
+ parts.push(theme.fg("dim", `Saved: ${details.rawPath}`));
847
+ }
848
+ return new Text(parts.join("\n"), 0, 0);
849
+ },
850
+ });
851
+ // -- wiki_query --
852
+ pi.registerTool({
853
+ name: "wiki_query",
854
+ label: "Wiki Query",
855
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_query,
856
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_query,
857
+ promptGuidelines: getWikiToolGuidelines("wiki_query"),
858
+ parameters: WikiQueryParams,
859
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
860
+ ctx.ui.setStatus("pi-para", "wiki: searching...");
861
+ try {
862
+ return await queryExec(params);
863
+ }
864
+ finally {
865
+ ctx.ui.setStatus("pi-para", undefined);
866
+ }
867
+ },
868
+ renderCall(args, theme) {
869
+ const q = args.query.length > 50 ? args.query.slice(0, 47) + "..." : args.query;
870
+ let text = theme.fg("toolTitle", theme.bold("wiki_query ")) +
871
+ theme.fg("dim", `"${q}"`);
872
+ if (args.global)
873
+ text += theme.fg("muted", " (global)");
874
+ if (args.category)
875
+ text += theme.fg("muted", ` [${args.category}]`);
876
+ return new Text(text, 0, 0);
877
+ },
878
+ renderResult(result, _options, theme) {
879
+ const details = result.details;
880
+ if (!details) {
881
+ return new Text(theme.fg("dim", "No results"), 0, 0);
882
+ }
883
+ return new Text(theme.fg("success", `${details.resultCount} result(s)`) +
884
+ theme.fg("dim", ` scope: ${details.scopeUsed}`), 0, 0);
885
+ },
886
+ });
887
+ // -- wiki_edit --
888
+ pi.registerTool({
889
+ name: "wiki_edit",
890
+ label: "Wiki Edit",
891
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_edit,
892
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_edit,
893
+ promptGuidelines: getWikiToolGuidelines("wiki_edit"),
894
+ parameters: WikiEditParams,
895
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
896
+ ctx.ui.setStatus("pi-para", "wiki: editing...");
897
+ try {
898
+ return await editExec(params);
899
+ }
900
+ finally {
901
+ ctx.ui.setStatus("pi-para", undefined);
902
+ }
903
+ },
904
+ renderCall(args, theme) {
905
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_edit ")) +
906
+ theme.fg("muted", args.path ?? "") +
907
+ theme.fg("dim", ` (${args.edits?.length ?? 0} edit(s))`), 0, 0);
908
+ },
909
+ renderResult(result, _options, theme) {
910
+ const details = result.details;
911
+ if (details?.pagesWritten?.length) {
912
+ return new Text(theme.fg("success", `Edited ${details.pagesWritten.join(", ")}`), 0, 0);
913
+ }
914
+ const t = result.content[0];
915
+ return new Text(theme.fg("warning", t?.type === "text" ? t.text : "No changes"), 0, 0);
916
+ },
917
+ });
918
+ // -- wiki_write --
919
+ pi.registerTool({
920
+ name: "wiki_write",
921
+ label: "Wiki Write",
922
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_write,
923
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_write,
924
+ promptGuidelines: getWikiToolGuidelines("wiki_write"),
925
+ parameters: WikiWriteParams,
926
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
927
+ ctx.ui.setStatus("pi-para", "wiki: writing...");
928
+ try {
929
+ return await writeExec(params);
930
+ }
931
+ finally {
932
+ ctx.ui.setStatus("pi-para", undefined);
933
+ }
934
+ },
935
+ renderCall(args, theme) {
936
+ const count = args.pages?.length ?? 0;
937
+ const slugs = (args.pages ?? [])
938
+ .slice(0, 3)
939
+ .map((p) => p.slug)
940
+ .join(", ");
941
+ let text = theme.fg("toolTitle", theme.bold("wiki_write ")) +
942
+ theme.fg("muted", `${count} page(s)`);
943
+ if (slugs)
944
+ text += theme.fg("dim", ` [${slugs}]`);
945
+ text += theme.fg("muted", " +index"); // always auto-rebuilds index
946
+ return new Text(text, 0, 0);
947
+ },
948
+ renderResult(result, _options, theme) {
949
+ const details = result.details;
950
+ if (!details) {
951
+ const t = result.content[0];
952
+ return new Text(t?.type === "text" ? t.text : "", 0, 0);
953
+ }
954
+ const parts = [];
955
+ if (details.pagesWritten.length > 0) {
956
+ parts.push(theme.fg("success", `Wrote ${details.pagesWritten.length} page(s): `) +
957
+ theme.fg("muted", details.pagesWritten.join(", ")));
958
+ }
959
+ if (details.pagesSkipped?.length) {
960
+ parts.push(theme.fg("warning", `Skipped ${details.pagesSkipped.length} page(s): `) + theme.fg("muted", details.pagesSkipped.join(", ")));
961
+ }
962
+ if (details.indexUpdated)
963
+ parts.push(theme.fg("dim", "Updated index.md"));
964
+ if (details.logAppended)
965
+ parts.push(theme.fg("dim", "Logged operation"));
966
+ return new Text(parts.join("\n") || theme.fg("dim", "No changes"), 0, 0);
967
+ },
968
+ });
969
+ // -- wiki_read --
970
+ pi.registerTool({
971
+ name: "wiki_read",
972
+ label: "Wiki Read",
973
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_read,
974
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_read,
975
+ promptGuidelines: getWikiToolGuidelines("wiki_read"),
976
+ parameters: WikiReadParams,
977
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
978
+ ctx.ui.setStatus("pi-para", "wiki: reading...");
979
+ try {
980
+ return await readExec(params);
981
+ }
982
+ finally {
983
+ ctx.ui.setStatus("pi-para", undefined);
984
+ }
985
+ },
986
+ renderCall(args, theme) {
987
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_read ")) +
988
+ theme.fg("dim", args.path), 0, 0);
989
+ },
990
+ renderResult(result, _options, theme) {
991
+ const details = result.details;
992
+ if (!details || !details.found) {
993
+ return new Text(theme.fg("error", "Page not found"), 0, 0);
994
+ }
995
+ return new Text(theme.fg("success", "Found ") + theme.fg("muted", details.path ?? ""), 0, 0);
996
+ },
997
+ });
998
+ // -- wiki_move --
999
+ pi.registerTool({
1000
+ name: "wiki_move",
1001
+ label: "Wiki Move",
1002
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_move,
1003
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_move,
1004
+ promptGuidelines: getWikiToolGuidelines("wiki_move"),
1005
+ parameters: WikiMoveParams,
1006
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1007
+ ctx.ui.setStatus("pi-para", "wiki: moving...");
1008
+ try {
1009
+ return await moveExec(params);
1010
+ }
1011
+ finally {
1012
+ ctx.ui.setStatus("pi-para", undefined);
1013
+ }
1014
+ },
1015
+ renderCall(args, theme) {
1016
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_move ")) +
1017
+ theme.fg("dim", `${args.path} -> ${args.to}`), 0, 0);
1018
+ },
1019
+ renderResult(result, _options, theme) {
1020
+ const details = result.details;
1021
+ if (!details) {
1022
+ const t = result.content[0];
1023
+ return new Text(t?.type === "text" ? t.text : "", 0, 0);
1024
+ }
1025
+ return new Text(theme.fg("success", "Moved ") +
1026
+ theme.fg("muted", `${details.from} -> ${details.to}`), 0, 0);
1027
+ },
1028
+ });
1029
+ // -- wiki_lint --
1030
+ pi.registerTool({
1031
+ name: "wiki_lint",
1032
+ label: "Wiki Lint",
1033
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_lint,
1034
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_lint,
1035
+ promptGuidelines: getWikiToolGuidelines("wiki_lint"),
1036
+ parameters: WikiLintParams,
1037
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1038
+ ctx.ui.setStatus("pi-para", "wiki: linting...");
1039
+ try {
1040
+ return await lintExec(params);
1041
+ }
1042
+ finally {
1043
+ ctx.ui.setStatus("pi-para", undefined);
1044
+ }
1045
+ },
1046
+ renderCall(args, theme) {
1047
+ const mode = args.autoFix === false ? "report-only" : "auto-fix";
1048
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_lint ")) +
1049
+ theme.fg("muted", `[${mode}]`), 0, 0);
1050
+ },
1051
+ renderResult(result, _options, theme) {
1052
+ const details = result.details;
1053
+ if (!details) {
1054
+ return new Text(theme.fg("dim", "Lint complete"), 0, 0);
1055
+ }
1056
+ const parts = [
1057
+ theme.fg("muted", `${details.stats.totalPages} pages`),
1058
+ ];
1059
+ if (details.fixedCount > 0) {
1060
+ parts.push(theme.fg("success", `${details.fixedCount} fixed`));
1061
+ }
1062
+ if (details.issueCount > 0) {
1063
+ parts.push(theme.fg("warning", `${details.issueCount} remaining`));
1064
+ }
1065
+ else {
1066
+ parts.push(theme.fg("success", "healthy"));
1067
+ }
1068
+ return new Text(parts.join(" | "), 0, 0);
1069
+ },
1070
+ });
1071
+ // -- wiki_migrate --
1072
+ pi.registerTool({
1073
+ name: "wiki_migrate",
1074
+ label: "Wiki Migrate",
1075
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_migrate,
1076
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_migrate,
1077
+ promptGuidelines: getWikiToolGuidelines("wiki_migrate"),
1078
+ parameters: WikiMigrateParams,
1079
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1080
+ ctx.ui.setStatus("pi-para", "wiki: migrating...");
1081
+ try {
1082
+ return await migrateExec();
1083
+ }
1084
+ finally {
1085
+ ctx.ui.setStatus("pi-para", undefined);
1086
+ }
1087
+ },
1088
+ renderCall(_args, theme) {
1089
+ return new Text(theme.fg("toolTitle", theme.bold("wiki_migrate ")) +
1090
+ theme.fg("muted", `→ v${CURRENT_SCHEMA_VERSION}`), 0, 0);
1091
+ },
1092
+ renderResult(result, _options, theme) {
1093
+ const details = result.details;
1094
+ if (!details) {
1095
+ return new Text(theme.fg("dim", "Migration complete"), 0, 0);
1096
+ }
1097
+ if (details.migratedCount === 0) {
1098
+ return new Text(theme.fg("success", `All ${details.totalPages} pages at v${CURRENT_SCHEMA_VERSION}`), 0, 0);
1099
+ }
1100
+ return new Text(theme.fg("success", `Migrated ${details.migratedCount}/${details.totalPages} page(s)`), 0, 0);
1101
+ },
1102
+ });
1103
+ // -- wiki_summarize --
1104
+ pi.registerTool({
1105
+ name: "wiki_summarize",
1106
+ label: "Wiki Summarize",
1107
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_summarize,
1108
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_summarize,
1109
+ promptGuidelines: getWikiToolGuidelines("wiki_summarize"),
1110
+ parameters: WikiSummarizeParams,
1111
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1112
+ ctx.ui.setStatus("pi-para", "wiki: summarizing...");
1113
+ try {
1114
+ return await summarizeExec(params);
1115
+ }
1116
+ finally {
1117
+ ctx.ui.setStatus("pi-para", undefined);
1118
+ }
1119
+ },
1120
+ renderCall(args, theme) {
1121
+ let text = theme.fg("toolTitle", theme.bold("wiki_summarize ")) +
1122
+ theme.fg("dim", args.target);
1123
+ if (args.depth)
1124
+ text += theme.fg("muted", ` [${args.depth}]`);
1125
+ return new Text(text, 0, 0);
1126
+ },
1127
+ renderResult(result, _options, theme) {
1128
+ const details = result.details;
1129
+ if (!details) {
1130
+ return new Text(theme.fg("dim", "Summary generated"), 0, 0);
1131
+ }
1132
+ if (details.pageCount === 0) {
1133
+ return new Text(theme.fg("warning", "No pages found"), 0, 0);
1134
+ }
1135
+ return new Text(theme.fg("success", `Summarizing ${details.pageCount} page(s)`) +
1136
+ theme.fg("dim", ` target: ${details.target}`), 0, 0);
1137
+ },
1138
+ });
1139
+ }
1140
+ // -- Public API: standalone tools for mini-agent -----------------------------
1141
+ /**
1142
+ * Create standalone AgentTool instances for the mini-agent (capture/lint/summarize).
1143
+ * Returns only the tools needed for autonomous wiki operations:
1144
+ * wiki_write, wiki_edit, wiki_read, wiki_query, wiki_move.
1145
+ */
1146
+ export function createStandaloneTools(wikiDir, store, getScope, markDirty) {
1147
+ const noopDirty = markDirty ?? (() => { });
1148
+ const writeExec = createWriteExecute(wikiDir, store, getScope, noopDirty);
1149
+ const editExec = createEditExecute(wikiDir, store, noopDirty);
1150
+ const readExec = createReadExecute(wikiDir);
1151
+ const queryExec = createQueryExecute(wikiDir, store, getScope);
1152
+ const moveExec = createMoveExecute(wikiDir, store, noopDirty);
1153
+ const wikiWrite = {
1154
+ name: "wiki_write",
1155
+ label: "Wiki Write",
1156
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_write,
1157
+ parameters: WikiWriteParams,
1158
+ execute: (_toolCallId, params) => writeExec(params),
1159
+ };
1160
+ const wikiEdit = {
1161
+ name: "wiki_edit",
1162
+ label: "Wiki Edit",
1163
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_edit,
1164
+ parameters: WikiEditParams,
1165
+ execute: (_toolCallId, params) => editExec(params),
1166
+ };
1167
+ const wikiRead = {
1168
+ name: "wiki_read",
1169
+ label: "Wiki Read",
1170
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_read,
1171
+ parameters: WikiReadParams,
1172
+ execute: (_toolCallId, params) => readExec(params),
1173
+ };
1174
+ const wikiQuery = {
1175
+ name: "wiki_query",
1176
+ label: "Wiki Query",
1177
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_query,
1178
+ parameters: WikiQueryParams,
1179
+ execute: (_toolCallId, params) => queryExec(params),
1180
+ };
1181
+ const wikiMove = {
1182
+ name: "wiki_move",
1183
+ label: "Wiki Move",
1184
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_move,
1185
+ parameters: WikiMoveParams,
1186
+ execute: (_toolCallId, params) => moveExec(params),
1187
+ };
1188
+ return [wikiWrite, wikiEdit, wikiRead, wikiQuery, wikiMove];
1189
+ }
1190
+ // -- Utilities ---------------------------------------------------------------
1191
+ function formatBytes(bytes) {
1192
+ if (bytes < 1024)
1193
+ return `${bytes}B`;
1194
+ if (bytes < 1024 * 1024)
1195
+ return `${(bytes / 1024).toFixed(1)}KB`;
1196
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1197
+ }
1198
+ //# sourceMappingURL=tools.js.map