pi-para 0.5.2 → 0.5.5

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 (175) hide show
  1. package/ARCHITECTURE.md +160 -0
  2. package/CHANGELOG.md +29 -0
  3. package/CONFIGURATION.md +243 -0
  4. package/README.md +140 -237
  5. package/SETUP.md +196 -112
  6. package/STRUCTURE.md +140 -0
  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 +4 -4
  12. package/dist/capture.d.ts.map +1 -1
  13. package/dist/capture.js +1 -1
  14. package/dist/capture.js.map +1 -1
  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 +2 -9
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +135 -29
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands.d.ts +3 -3
  24. package/dist/commands.d.ts.map +1 -1
  25. package/dist/commands.js +189 -256
  26. package/dist/commands.js.map +1 -1
  27. package/dist/config.d.ts +104 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +242 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +4 -2
  32. package/dist/context.d.ts.map +1 -1
  33. package/dist/context.js +29 -7
  34. package/dist/context.js.map +1 -1
  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 +1 -1
  40. package/dist/daemon.d.ts.map +1 -1
  41. package/dist/doctor.d.ts +20 -0
  42. package/dist/doctor.d.ts.map +1 -0
  43. package/dist/doctor.js +171 -0
  44. package/dist/doctor.js.map +1 -0
  45. package/dist/gepa/index.js +1 -1
  46. package/dist/gepa/index.js.map +1 -1
  47. package/dist/index.d.ts +3 -3
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +102 -37
  50. package/dist/index.js.map +1 -1
  51. package/dist/maintainer.d.ts +2 -2
  52. package/dist/maintainer.d.ts.map +1 -1
  53. package/dist/maintainer.js +2 -2
  54. package/dist/maintainer.js.map +1 -1
  55. package/dist/model-resolver.d.ts +30 -0
  56. package/dist/model-resolver.d.ts.map +1 -0
  57. package/dist/model-resolver.js +71 -0
  58. package/dist/model-resolver.js.map +1 -0
  59. package/dist/paths.d.ts +20 -0
  60. package/dist/paths.d.ts.map +1 -0
  61. package/dist/paths.js +33 -0
  62. package/dist/paths.js.map +1 -0
  63. package/dist/processor.d.ts +2 -2
  64. package/dist/processor.d.ts.map +1 -1
  65. package/dist/processor.js +2 -2
  66. package/dist/processor.js.map +1 -1
  67. package/dist/qmd-providers.d.ts +12 -0
  68. package/dist/qmd-providers.d.ts.map +1 -0
  69. package/dist/qmd-providers.js +45 -0
  70. package/dist/qmd-providers.js.map +1 -0
  71. package/dist/query.d.ts +1 -1
  72. package/dist/query.d.ts.map +1 -1
  73. package/dist/query.js +14 -1
  74. package/dist/query.js.map +1 -1
  75. package/dist/repair.d.ts +11 -0
  76. package/dist/repair.d.ts.map +1 -0
  77. package/dist/repair.js +41 -0
  78. package/dist/repair.js.map +1 -0
  79. package/dist/scheduler/controls.d.ts +35 -0
  80. package/dist/scheduler/controls.d.ts.map +1 -0
  81. package/dist/scheduler/controls.js +98 -0
  82. package/dist/scheduler/controls.js.map +1 -0
  83. package/dist/scheduler/index.d.ts +48 -0
  84. package/dist/scheduler/index.d.ts.map +1 -0
  85. package/dist/scheduler/index.js +193 -0
  86. package/dist/scheduler/index.js.map +1 -0
  87. package/dist/scheduler/leases.d.ts +22 -0
  88. package/dist/scheduler/leases.d.ts.map +1 -0
  89. package/dist/scheduler/leases.js +74 -0
  90. package/dist/scheduler/leases.js.map +1 -0
  91. package/dist/scheduler/session-capture.d.ts +33 -0
  92. package/dist/scheduler/session-capture.d.ts.map +1 -0
  93. package/dist/scheduler/session-capture.js +108 -0
  94. package/dist/scheduler/session-capture.js.map +1 -0
  95. package/dist/scheduler/state.d.ts +56 -0
  96. package/dist/scheduler/state.d.ts.map +1 -0
  97. package/dist/scheduler/state.js +210 -0
  98. package/dist/scheduler/state.js.map +1 -0
  99. package/dist/session-tools.d.ts +1 -1
  100. package/dist/session-tools.d.ts.map +1 -1
  101. package/dist/settings.d.ts +19 -0
  102. package/dist/settings.d.ts.map +1 -0
  103. package/dist/settings.js +106 -0
  104. package/dist/settings.js.map +1 -0
  105. package/dist/setup.d.ts +29 -0
  106. package/dist/setup.d.ts.map +1 -0
  107. package/dist/setup.js +90 -0
  108. package/dist/setup.js.map +1 -0
  109. package/dist/status.d.ts +22 -0
  110. package/dist/status.d.ts.map +1 -0
  111. package/dist/status.js +93 -0
  112. package/dist/status.js.map +1 -0
  113. package/dist/store.d.ts +23 -8
  114. package/dist/store.d.ts.map +1 -1
  115. package/dist/store.js +89 -34
  116. package/dist/store.js.map +1 -1
  117. package/dist/summarize.d.ts +6 -2
  118. package/dist/summarize.d.ts.map +1 -1
  119. package/dist/summarize.js +60 -25
  120. package/dist/summarize.js.map +1 -1
  121. package/dist/templates/prompts.d.ts +1 -1
  122. package/dist/templates/prompts.d.ts.map +1 -1
  123. package/dist/templates/prompts.js +1 -1
  124. package/dist/tools.d.ts +5 -5
  125. package/dist/tools.d.ts.map +1 -1
  126. package/dist/tools.js +243 -78
  127. package/dist/tools.js.map +1 -1
  128. package/dist/wiki-tool-guidance.d.ts +6 -0
  129. package/dist/wiki-tool-guidance.d.ts.map +1 -0
  130. package/dist/wiki-tool-guidance.js +70 -0
  131. package/dist/wiki-tool-guidance.js.map +1 -0
  132. package/dist/wiki.d.ts +6 -1
  133. package/dist/wiki.d.ts.map +1 -1
  134. package/dist/wiki.js +88 -19
  135. package/dist/wiki.js.map +1 -1
  136. package/package.json +80 -15
  137. package/scripts/install.ps1 +44 -0
  138. package/scripts/install.sh +62 -0
  139. package/scripts/smoke-install.mjs +61 -0
  140. package/skills/para/SKILL.md +1 -1
  141. package/skills/setup/SKILL.md +1 -1
  142. package/src/atomic-write.ts +17 -0
  143. package/src/capture.ts +4 -4
  144. package/src/cli-legacy.ts +43 -0
  145. package/src/cli.ts +133 -28
  146. package/src/commands.ts +203 -243
  147. package/src/config.ts +355 -0
  148. package/src/context.ts +29 -14
  149. package/src/credentials.ts +100 -0
  150. package/src/daemon.ts +1 -1
  151. package/src/doctor.ts +204 -0
  152. package/src/gepa/index.ts +1 -1
  153. package/src/index.ts +119 -40
  154. package/src/maintainer.ts +4 -4
  155. package/src/model-resolver.ts +96 -0
  156. package/src/paths.ts +48 -0
  157. package/src/processor.ts +4 -4
  158. package/src/qmd-providers.ts +63 -0
  159. package/src/query.ts +15 -1
  160. package/src/repair.ts +49 -0
  161. package/src/scheduler/controls.ts +125 -0
  162. package/src/scheduler/index.ts +210 -0
  163. package/src/scheduler/leases.ts +98 -0
  164. package/src/scheduler/session-capture.ts +146 -0
  165. package/src/scheduler/state.ts +304 -0
  166. package/src/session-tools.ts +1 -1
  167. package/src/settings.ts +126 -0
  168. package/src/setup.ts +116 -0
  169. package/src/status.ts +115 -0
  170. package/src/store.ts +116 -31
  171. package/src/summarize.ts +72 -16
  172. package/src/templates/prompts.ts +1 -1
  173. package/src/tools.ts +280 -90
  174. package/src/wiki-tool-guidance.ts +84 -0
  175. package/src/wiki.ts +98 -26
package/dist/tools.js CHANGED
@@ -6,13 +6,12 @@
6
6
  *
7
7
  * Factory functions (createXxxExecute) close over wikiDir and store — no ctx dependency.
8
8
  */
9
- import { StringEnum } from "@mariozechner/pi-ai";
10
- import { Text } from "@mariozechner/pi-tui";
9
+ import { StringEnum } from "@earendil-works/pi-ai";
10
+ import { Text } from "@earendil-works/pi-tui";
11
11
  import { Type } from "typebox";
12
12
  import { resolveSource, truncateSource, detectSourceType } from "./ingest.js";
13
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";
14
+ import { readPage, writePage, movePage, listPages, listPageSlugs, readIndex, writeIndex, readSchema, appendLog, gitCommit, rebuildIndex, PARA_CATEGORIES, } from "./wiki.js";
16
15
  import { validateFrontmatter, parseFrontmatter, CURRENT_SCHEMA_VERSION, migrateToLatest, } from "./frontmatter.js";
17
16
  import { extractWikilinks, autoLinkSlugs, syncFrontmatterLinks } from "./link-utils.js";
18
17
  import { normalizeTags, normalizeScopes } from "./tag-registry.js";
@@ -20,6 +19,8 @@ import { redactSecrets } from "./redact.js";
20
19
  import { lintWiki } from "./lint.js";
21
20
  import { generateOverviewPrompt } from "./summarize.js";
22
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";
23
24
  // -- Parameter schemas -------------------------------------------------------
24
25
  const PARA_ENUM = StringEnum(["projects", "areas", "resources", "archives"], { description: "PARA category" });
25
26
  const PARA_ENUM_NO_ARCHIVES = StringEnum(["projects", "areas", "resources"], { description: "PARA category (archives excluded for new content)" });
@@ -64,6 +65,16 @@ const WikiWriteParams = Type.Object({
64
65
  indexContent: Type.Optional(Type.String({ description: "Updated index.md content (full replacement)" })),
65
66
  logSummary: Type.Optional(Type.String({ description: "One-line summary for log.md" })),
66
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
+ });
67
78
  const WikiReadParams = Type.Object({
68
79
  path: Type.String({
69
80
  description: "Page path (e.g. 'projects/auth-refactor') or page title",
@@ -189,15 +200,25 @@ function createQueryExecute(_wikiDir, store, getScope, getGraphBoost) {
189
200
  };
190
201
  };
191
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
+ }
192
212
  // -- Factory: wiki_write execute ---------------------------------------------
193
213
  function createWriteExecute(wikiDir, store, getScope, markDirty) {
194
214
  return async (params) => {
195
215
  const pagesWritten = [];
216
+ const pagesSkipped = [];
196
217
  const now = new Date().toISOString();
197
218
  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));
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);
201
222
  for (const pageSpec of params.pages) {
202
223
  // Sanitize slug
203
224
  const slug = pageSpec.slug
@@ -212,21 +233,38 @@ function createWriteExecute(wikiDir, store, getScope, markDirty) {
212
233
  const pagePath = `${pageSpec.category}/${slug}`;
213
234
  // Track this slug for auto-linking other pages in this batch
214
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
+ }
215
244
  if (pageSpec.mode === "edit" && existing) {
216
- // Edit mode: apply targeted oldText→newText replacements
245
+ // Legacy edit mode: keep for compatibility, but make it atomic like wiki_edit.
217
246
  const edits = pageSpec.edits ?? [];
218
247
  if (edits.length === 0) {
219
248
  continue; // no edits to apply
220
249
  }
221
- let editedBody = existing.body;
222
250
  const errors = [];
223
251
  for (const edit of edits) {
224
- const idx = editedBody.indexOf(edit.oldText);
225
- if (idx === -1) {
252
+ const first = existing.body.indexOf(edit.oldText);
253
+ const last = existing.body.lastIndexOf(edit.oldText);
254
+ if (first === -1) {
226
255
  errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
227
- continue;
228
256
  }
229
- editedBody = editedBody.slice(0, idx) + edit.newText + editedBody.slice(idx + edit.oldText.length);
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);
230
268
  }
231
269
  const linkedBody = redactSecrets(autoLinkSlugs(editedBody, allSlugs, slug)).text;
232
270
  const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : existing.frontmatter.scope;
@@ -336,8 +374,12 @@ function createWriteExecute(wikiDir, store, getScope, markDirty) {
336
374
  });
337
375
  logAppended = true;
338
376
  }
339
- // Re-index for BM25 (fast). Do NOT embed — deferred to shutdown.
340
- await reindex(store);
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
+ }
341
383
  // Mark context cache dirty so before_agent_start rebuilds
342
384
  markDirty();
343
385
  // Git auto-commit
@@ -347,9 +389,12 @@ function createWriteExecute(wikiDir, store, getScope, markDirty) {
347
389
  : `wiki: ${pagesWritten.join(", ")}`;
348
390
  await gitCommit(wikiDir, commitMsg);
349
391
  }
350
- const summary = pagesWritten.length > 0
351
- ? `Wrote ${pagesWritten.length} page(s): ${pagesWritten.join(", ")}`
352
- : "No pages written.";
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");
353
398
  return {
354
399
  content: [
355
400
  {
@@ -367,14 +412,112 @@ function createWriteExecute(wikiDir, store, getScope, markDirty) {
367
412
  pagesWritten,
368
413
  indexUpdated,
369
414
  logAppended,
415
+ pagesSkipped,
370
416
  },
371
417
  };
372
418
  };
373
419
  }
374
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
+ }
375
510
  function createReadExecute(wikiDir) {
376
511
  return async (params) => {
377
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
+ }
378
521
  // Try parsing as category/slug path
379
522
  const parsed = parsePagePath(params.path);
380
523
  if (parsed) {
@@ -457,9 +600,10 @@ function createMoveExecute(wikiDir, store, markDirty) {
457
600
  path: `${parsed.category}/${parsed.slug}.md`,
458
601
  };
459
602
  await movePage(wikiDir, ref, params.to);
460
- // Re-index and rebuild index.md
461
- await reindex(store);
603
+ // Rebuild index.md inline (fast, single-pass); defer QMD reindex to the
604
+ // debounced background maintenance queue like wiki_write/wiki_edit.
462
605
  await rebuildIndex(wikiDir);
606
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
463
607
  markDirty();
464
608
  // Git commit
465
609
  await gitCommit(wikiDir, `move: ${page.frontmatter.title} → ${params.to}`);
@@ -588,9 +732,11 @@ function createSummarizeExecute(wikiDir, getScope) {
588
732
  details: { target: params.target, pageCount: 0 },
589
733
  };
590
734
  }
591
- // Generate overview prompt for the LLM
592
- const overviewPrompt = generateOverviewPrompt(pages, scope);
593
- const depthNote = params.depth === "detailed"
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"
594
740
  ? "\nProvide a DETAILED summary with full Key Facts, Insights, and Connections for each theme."
595
741
  : "\nProvide a BRIEF overview — one paragraph per theme, focus on high-level patterns.";
596
742
  return {
@@ -647,13 +793,14 @@ function createMigrateExecute(wikiDir) {
647
793
  /**
648
794
  * Register all wiki tools with the pi extension API.
649
795
  *
650
- * Tools: wiki_ingest, wiki_query, wiki_write, wiki_read,
796
+ * Tools: wiki_ingest, wiki_query, wiki_write, wiki_edit, wiki_read,
651
797
  * wiki_move, wiki_lint, wiki_summarize
652
798
  */
653
799
  export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphBoost) {
654
800
  const ingestExec = createIngestExecute(wikiDir, store, getScope);
655
801
  const queryExec = createQueryExecute(wikiDir, store, getScope, getGraphBoost);
656
802
  const writeExec = createWriteExecute(wikiDir, store, getScope, markDirty);
803
+ const editExec = createEditExecute(wikiDir, store, markDirty);
657
804
  const readExec = createReadExecute(wikiDir);
658
805
  const moveExec = createMoveExecute(wikiDir, store, markDirty);
659
806
  const lintExec = createLintExecute(wikiDir);
@@ -663,13 +810,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
663
810
  pi.registerTool({
664
811
  name: "wiki_ingest",
665
812
  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
- ],
813
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_ingest,
814
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_ingest,
815
+ promptGuidelines: getWikiToolGuidelines("wiki_ingest"),
673
816
  parameters: WikiIngestParams,
674
817
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
675
818
  ctx.ui.setStatus("pi-para", "wiki: ingesting...");
@@ -709,15 +852,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
709
852
  pi.registerTool({
710
853
  name: "wiki_query",
711
854
  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
- ],
855
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_query,
856
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_query,
857
+ promptGuidelines: getWikiToolGuidelines("wiki_query"),
721
858
  parameters: WikiQueryParams,
722
859
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
723
860
  ctx.ui.setStatus("pi-para", "wiki: searching...");
@@ -747,20 +884,44 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
747
884
  theme.fg("dim", ` scope: ${details.scopeUsed}`), 0, 0);
748
885
  },
749
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
+ });
750
918
  // -- wiki_write --
751
919
  pi.registerTool({
752
920
  name: "wiki_write",
753
921
  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
- ],
922
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_write,
923
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_write,
924
+ promptGuidelines: getWikiToolGuidelines("wiki_write"),
764
925
  parameters: WikiWriteParams,
765
926
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
766
927
  ctx.ui.setStatus("pi-para", "wiki: writing...");
@@ -795,6 +956,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
795
956
  parts.push(theme.fg("success", `Wrote ${details.pagesWritten.length} page(s): `) +
796
957
  theme.fg("muted", details.pagesWritten.join(", ")));
797
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
+ }
798
962
  if (details.indexUpdated)
799
963
  parts.push(theme.fg("dim", "Updated index.md"));
800
964
  if (details.logAppended)
@@ -806,12 +970,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
806
970
  pi.registerTool({
807
971
  name: "wiki_read",
808
972
  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
- ],
973
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_read,
974
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_read,
975
+ promptGuidelines: getWikiToolGuidelines("wiki_read"),
815
976
  parameters: WikiReadParams,
816
977
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
817
978
  ctx.ui.setStatus("pi-para", "wiki: reading...");
@@ -838,9 +999,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
838
999
  pi.registerTool({
839
1000
  name: "wiki_move",
840
1001
  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",
1002
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_move,
1003
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_move,
1004
+ promptGuidelines: getWikiToolGuidelines("wiki_move"),
844
1005
  parameters: WikiMoveParams,
845
1006
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
846
1007
  ctx.ui.setStatus("pi-para", "wiki: moving...");
@@ -869,10 +1030,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
869
1030
  pi.registerTool({
870
1031
  name: "wiki_lint",
871
1032
  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",
1033
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_lint,
1034
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_lint,
1035
+ promptGuidelines: getWikiToolGuidelines("wiki_lint"),
876
1036
  parameters: WikiLintParams,
877
1037
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
878
1038
  ctx.ui.setStatus("pi-para", "wiki: linting...");
@@ -912,9 +1072,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
912
1072
  pi.registerTool({
913
1073
  name: "wiki_migrate",
914
1074
  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",
1075
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_migrate,
1076
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_migrate,
1077
+ promptGuidelines: getWikiToolGuidelines("wiki_migrate"),
918
1078
  parameters: WikiMigrateParams,
919
1079
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
920
1080
  ctx.ui.setStatus("pi-para", "wiki: migrating...");
@@ -944,11 +1104,9 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
944
1104
  pi.registerTool({
945
1105
  name: "wiki_summarize",
946
1106
  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",
1107
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_summarize,
1108
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_summarize,
1109
+ promptGuidelines: getWikiToolGuidelines("wiki_summarize"),
952
1110
  parameters: WikiSummarizeParams,
953
1111
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
954
1112
  ctx.ui.setStatus("pi-para", "wiki: summarizing...");
@@ -983,44 +1141,51 @@ export function registerTools(pi, wikiDir, store, getScope, markDirty, getGraphB
983
1141
  /**
984
1142
  * Create standalone AgentTool instances for the mini-agent (capture/lint/summarize).
985
1143
  * Returns only the tools needed for autonomous wiki operations:
986
- * wiki_write, wiki_read, wiki_query, wiki_move.
1144
+ * wiki_write, wiki_edit, wiki_read, wiki_query, wiki_move.
987
1145
  */
988
1146
  export function createStandaloneTools(wikiDir, store, getScope, markDirty) {
989
1147
  const noopDirty = markDirty ?? (() => { });
990
1148
  const writeExec = createWriteExecute(wikiDir, store, getScope, noopDirty);
1149
+ const editExec = createEditExecute(wikiDir, store, noopDirty);
991
1150
  const readExec = createReadExecute(wikiDir);
992
1151
  const queryExec = createQueryExecute(wikiDir, store, getScope);
993
1152
  const moveExec = createMoveExecute(wikiDir, store, noopDirty);
994
1153
  const wikiWrite = {
995
1154
  name: "wiki_write",
996
1155
  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.",
1156
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_write,
999
1157
  parameters: WikiWriteParams,
1000
1158
  execute: (_toolCallId, params) => writeExec(params),
1001
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
+ };
1002
1167
  const wikiRead = {
1003
1168
  name: "wiki_read",
1004
1169
  label: "Wiki Read",
1005
- description: "Read a specific wiki page by path (e.g. 'projects/auth-refactor') or by title.",
1170
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_read,
1006
1171
  parameters: WikiReadParams,
1007
1172
  execute: (_toolCallId, params) => readExec(params),
1008
1173
  };
1009
1174
  const wikiQuery = {
1010
1175
  name: "wiki_query",
1011
1176
  label: "Wiki Query",
1012
- description: "Search the wiki knowledge base with a natural language query. Returns relevant pages.",
1177
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_query,
1013
1178
  parameters: WikiQueryParams,
1014
1179
  execute: (_toolCallId, params) => queryExec(params),
1015
1180
  };
1016
1181
  const wikiMove = {
1017
1182
  name: "wiki_move",
1018
1183
  label: "Wiki Move",
1019
- description: "Move a wiki page between PARA categories.",
1184
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_move,
1020
1185
  parameters: WikiMoveParams,
1021
1186
  execute: (_toolCallId, params) => moveExec(params),
1022
1187
  };
1023
- return [wikiWrite, wikiRead, wikiQuery, wikiMove];
1188
+ return [wikiWrite, wikiEdit, wikiRead, wikiQuery, wikiMove];
1024
1189
  }
1025
1190
  // -- Utilities ---------------------------------------------------------------
1026
1191
  function formatBytes(bytes) {