pi-para 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/ARCHITECTURE.md +160 -0
  2. package/CHANGELOG.md +51 -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/package.json +86 -18
  8. package/scripts/build-assets.mjs +29 -0
  9. package/scripts/install.ps1 +55 -0
  10. package/scripts/install.sh +63 -0
  11. package/scripts/smoke-install.mjs +63 -0
  12. package/skills/para/SKILL.md +1 -1
  13. package/skills/setup/SKILL.md +1 -1
  14. package/src/atomic-write.ts +17 -0
  15. package/src/capture.ts +4 -4
  16. package/src/cli-legacy.ts +43 -0
  17. package/src/cli.ts +133 -28
  18. package/src/commands.ts +203 -243
  19. package/src/config.ts +355 -0
  20. package/src/context.ts +29 -14
  21. package/src/credentials.ts +100 -0
  22. package/src/daemon.ts +1 -1
  23. package/src/doctor.ts +209 -0
  24. package/src/gepa/index.ts +1 -1
  25. package/src/index.ts +119 -40
  26. package/src/maintainer.ts +4 -4
  27. package/src/model-resolver.ts +96 -0
  28. package/src/paths.ts +48 -0
  29. package/src/processor.ts +4 -4
  30. package/src/qmd-providers.ts +63 -0
  31. package/src/query.ts +15 -1
  32. package/src/repair.ts +49 -0
  33. package/src/scheduler/controls.ts +125 -0
  34. package/src/scheduler/index.ts +210 -0
  35. package/src/scheduler/leases.ts +98 -0
  36. package/src/scheduler/session-capture.ts +146 -0
  37. package/src/scheduler/state.ts +304 -0
  38. package/src/session-tools.ts +1 -1
  39. package/src/settings.ts +126 -0
  40. package/src/setup.ts +116 -0
  41. package/src/status.ts +115 -0
  42. package/src/store.ts +116 -31
  43. package/src/summarize.ts +72 -16
  44. package/src/templates/prompts.ts +1 -1
  45. package/src/tools.ts +280 -90
  46. package/src/wiki-tool-guidance.ts +84 -0
  47. package/src/wiki.ts +98 -26
  48. package/dist/capture.d.ts +0 -34
  49. package/dist/capture.d.ts.map +0 -1
  50. package/dist/capture.js +0 -461
  51. package/dist/capture.js.map +0 -1
  52. package/dist/cli.d.ts +0 -15
  53. package/dist/cli.d.ts.map +0 -1
  54. package/dist/cli.js +0 -405
  55. package/dist/cli.js.map +0 -1
  56. package/dist/commands.d.ts +0 -15
  57. package/dist/commands.d.ts.map +0 -1
  58. package/dist/commands.js +0 -786
  59. package/dist/commands.js.map +0 -1
  60. package/dist/context.d.ts +0 -61
  61. package/dist/context.d.ts.map +0 -1
  62. package/dist/context.js +0 -289
  63. package/dist/context.js.map +0 -1
  64. package/dist/daemon.d.ts +0 -49
  65. package/dist/daemon.d.ts.map +0 -1
  66. package/dist/daemon.js +0 -226
  67. package/dist/daemon.js.map +0 -1
  68. package/dist/frontmatter.d.ts +0 -59
  69. package/dist/frontmatter.d.ts.map +0 -1
  70. package/dist/frontmatter.js +0 -170
  71. package/dist/frontmatter.js.map +0 -1
  72. package/dist/gepa/dataset.d.ts +0 -36
  73. package/dist/gepa/dataset.d.ts.map +0 -1
  74. package/dist/gepa/dataset.js +0 -148
  75. package/dist/gepa/dataset.js.map +0 -1
  76. package/dist/gepa/evaluator.d.ts +0 -51
  77. package/dist/gepa/evaluator.d.ts.map +0 -1
  78. package/dist/gepa/evaluator.js +0 -317
  79. package/dist/gepa/evaluator.js.map +0 -1
  80. package/dist/gepa/index.d.ts +0 -66
  81. package/dist/gepa/index.d.ts.map +0 -1
  82. package/dist/gepa/index.js +0 -240
  83. package/dist/gepa/index.js.map +0 -1
  84. package/dist/gepa/llm.d.ts +0 -35
  85. package/dist/gepa/llm.d.ts.map +0 -1
  86. package/dist/gepa/llm.js +0 -268
  87. package/dist/gepa/llm.js.map +0 -1
  88. package/dist/gepa/optimized/capture-prompt.txt +0 -65
  89. package/dist/gepa/optimizer.d.ts +0 -29
  90. package/dist/gepa/optimizer.d.ts.map +0 -1
  91. package/dist/gepa/optimizer.js +0 -249
  92. package/dist/gepa/optimizer.js.map +0 -1
  93. package/dist/gepa/targets.d.ts +0 -39
  94. package/dist/gepa/targets.d.ts.map +0 -1
  95. package/dist/gepa/targets.js +0 -326
  96. package/dist/gepa/targets.js.map +0 -1
  97. package/dist/gepa/types.d.ts +0 -168
  98. package/dist/gepa/types.d.ts.map +0 -1
  99. package/dist/gepa/types.js +0 -124
  100. package/dist/gepa/types.js.map +0 -1
  101. package/dist/index.d.ts +0 -21
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -294
  104. package/dist/index.js.map +0 -1
  105. package/dist/ingest.d.ts +0 -88
  106. package/dist/ingest.d.ts.map +0 -1
  107. package/dist/ingest.js +0 -105
  108. package/dist/ingest.js.map +0 -1
  109. package/dist/link-discovery.d.ts +0 -40
  110. package/dist/link-discovery.d.ts.map +0 -1
  111. package/dist/link-discovery.js +0 -122
  112. package/dist/link-discovery.js.map +0 -1
  113. package/dist/link-utils.d.ts +0 -30
  114. package/dist/link-utils.d.ts.map +0 -1
  115. package/dist/link-utils.js +0 -145
  116. package/dist/link-utils.js.map +0 -1
  117. package/dist/lint.d.ts +0 -39
  118. package/dist/lint.d.ts.map +0 -1
  119. package/dist/lint.js +0 -827
  120. package/dist/lint.js.map +0 -1
  121. package/dist/maintainer.d.ts +0 -24
  122. package/dist/maintainer.d.ts.map +0 -1
  123. package/dist/maintainer.js +0 -358
  124. package/dist/maintainer.js.map +0 -1
  125. package/dist/processor.d.ts +0 -14
  126. package/dist/processor.d.ts.map +0 -1
  127. package/dist/processor.js +0 -361
  128. package/dist/processor.js.map +0 -1
  129. package/dist/query.d.ts +0 -69
  130. package/dist/query.d.ts.map +0 -1
  131. package/dist/query.js +0 -107
  132. package/dist/query.js.map +0 -1
  133. package/dist/raw.d.ts +0 -57
  134. package/dist/raw.d.ts.map +0 -1
  135. package/dist/raw.js +0 -219
  136. package/dist/raw.js.map +0 -1
  137. package/dist/redact.d.ts +0 -21
  138. package/dist/redact.d.ts.map +0 -1
  139. package/dist/redact.js +0 -143
  140. package/dist/redact.js.map +0 -1
  141. package/dist/scope.d.ts +0 -54
  142. package/dist/scope.d.ts.map +0 -1
  143. package/dist/scope.js +0 -161
  144. package/dist/scope.js.map +0 -1
  145. package/dist/scripts/bulk-link-fix.d.ts +0 -9
  146. package/dist/scripts/bulk-link-fix.d.ts.map +0 -1
  147. package/dist/scripts/bulk-link-fix.js +0 -106
  148. package/dist/scripts/bulk-link-fix.js.map +0 -1
  149. package/dist/session-tools.d.ts +0 -33
  150. package/dist/session-tools.d.ts.map +0 -1
  151. package/dist/session-tools.js +0 -211
  152. package/dist/session-tools.js.map +0 -1
  153. package/dist/state.d.ts +0 -54
  154. package/dist/state.d.ts.map +0 -1
  155. package/dist/state.js +0 -176
  156. package/dist/state.js.map +0 -1
  157. package/dist/store.d.ts +0 -56
  158. package/dist/store.d.ts.map +0 -1
  159. package/dist/store.js +0 -227
  160. package/dist/store.js.map +0 -1
  161. package/dist/summarize.d.ts +0 -56
  162. package/dist/summarize.d.ts.map +0 -1
  163. package/dist/summarize.js +0 -231
  164. package/dist/summarize.js.map +0 -1
  165. package/dist/tag-registry.d.ts +0 -56
  166. package/dist/tag-registry.d.ts.map +0 -1
  167. package/dist/tag-registry.js +0 -146
  168. package/dist/tag-registry.js.map +0 -1
  169. package/dist/templates/prompts.d.ts +0 -63
  170. package/dist/templates/prompts.d.ts.map +0 -1
  171. package/dist/templates/prompts.js +0 -260
  172. package/dist/templates/prompts.js.map +0 -1
  173. package/dist/tools.d.ts +0 -26
  174. package/dist/tools.d.ts.map +0 -1
  175. package/dist/tools.js +0 -1033
  176. package/dist/tools.js.map +0 -1
  177. package/dist/watcher.d.ts +0 -25
  178. package/dist/watcher.d.ts.map +0 -1
  179. package/dist/watcher.js +0 -84
  180. package/dist/watcher.js.map +0 -1
  181. package/dist/webui/client/dist/index.html +0 -1885
  182. package/dist/webui/server.d.ts +0 -31
  183. package/dist/webui/server.d.ts.map +0 -1
  184. package/dist/webui/server.js +0 -566
  185. package/dist/webui/server.js.map +0 -1
  186. package/dist/wiki.d.ts +0 -103
  187. package/dist/wiki.d.ts.map +0 -1
  188. package/dist/wiki.js +0 -428
  189. package/dist/wiki.js.map +0 -1
  190. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  191. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  192. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  193. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  194. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  195. package/scripts/gepa/uv.lock +0 -2241
package/src/tools.ts CHANGED
@@ -7,22 +7,22 @@
7
7
  * Factory functions (createXxxExecute) close over wikiDir and store — no ctx dependency.
8
8
  */
9
9
 
10
- import { StringEnum } from "@mariozechner/pi-ai";
11
- import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
12
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
- import { Text } from "@mariozechner/pi-tui";
10
+ import { StringEnum } from "@earendil-works/pi-ai";
11
+ import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { Text } from "@earendil-works/pi-tui";
14
14
  import type { QMDStore } from "qmd-engine";
15
15
  import { Type } from "typebox";
16
16
 
17
17
  import type { ProjectScope } from "./scope.js";
18
18
  import { resolveSource, truncateSource, detectSourceType } from "./ingest.js";
19
19
  import { queryWiki as queryWikiLib, formatQueryResults, formatFreshness } from "./query.js";
20
- import { reindex } from "./store.js";
21
20
  import {
22
21
  readPage,
23
22
  writePage,
24
23
  movePage,
25
24
  listPages,
25
+ listPageSlugs,
26
26
  readIndex,
27
27
  writeIndex,
28
28
  readSchema,
@@ -53,6 +53,12 @@ import {
53
53
  QUERY_PROMPT,
54
54
  LINT_PROMPT,
55
55
  } from "./templates/prompts.js";
56
+ import { enqueueWikiMaintenance } from "./scheduler/index.js";
57
+ import {
58
+ WIKI_TOOL_DESCRIPTIONS,
59
+ WIKI_TOOL_SNIPPETS,
60
+ getWikiToolGuidelines,
61
+ } from "./wiki-tool-guidance.js";
56
62
 
57
63
  // -- Parameter schemas -------------------------------------------------------
58
64
 
@@ -124,6 +130,17 @@ const WikiWriteParams = Type.Object({
124
130
  ),
125
131
  });
126
132
 
133
+ const WikiEditParams = Type.Object({
134
+ path: Type.String({ description: "Existing page path (e.g. 'resources/ssl-cert-gotchas')" }),
135
+ edits: Type.Array(WikiEditSchema, {
136
+ description: "Atomic exact text replacements. Every oldText must appear exactly once or no changes are written.",
137
+ }),
138
+ title: Type.Optional(Type.String({ description: "Optional replacement page title" })),
139
+ scope: Type.Optional(Type.Array(Type.String(), { description: "Optional replacement scope tags" })),
140
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Optional replacement topic tags" })),
141
+ logSummary: Type.Optional(Type.String({ description: "One-line summary for log.md" })),
142
+ });
143
+
127
144
  const WikiReadParams = Type.Object({
128
145
  path: Type.String({
129
146
  description: "Page path (e.g. 'projects/auth-refactor') or page title",
@@ -173,6 +190,7 @@ interface WikiWriteDetails {
173
190
  pagesWritten: string[];
174
191
  indexUpdated: boolean;
175
192
  logAppended: boolean;
193
+ pagesSkipped?: string[];
176
194
  }
177
195
 
178
196
  interface WikiReadDetails {
@@ -325,6 +343,21 @@ function createQueryExecute(
325
343
  };
326
344
  }
327
345
 
346
+ // -- Background maintenance --------------------------------------------------
347
+
348
+ /**
349
+ * Schedule index rebuild + QMD reindex after latency-sensitive edits.
350
+ * Debounced per wiki directory and coordinated through the scheduler queue so
351
+ * multiple Pi processes do not run the same maintenance task concurrently.
352
+ */
353
+ function scheduleWikiMaintenance(
354
+ wikiDir: string,
355
+ store: QMDStore,
356
+ markDirty: () => void,
357
+ ): void {
358
+ enqueueWikiMaintenance(wikiDir, store, markDirty);
359
+ }
360
+
328
361
  // -- Factory: wiki_write execute ---------------------------------------------
329
362
 
330
363
  function createWriteExecute(
@@ -350,12 +383,13 @@ function createWriteExecute(
350
383
  },
351
384
  ): Promise<AgentToolResult<WikiWriteDetails>> => {
352
385
  const pagesWritten: string[] = [];
386
+ const pagesSkipped: string[] = [];
353
387
  const now = new Date().toISOString();
354
388
  const scope = getScope();
355
389
 
356
- // Gather all existing slugs for auto-linking
357
- const allRefs = await listPages(wikiDir);
358
- const allSlugs = new Set(allRefs.map(r => r.slug));
390
+ // Gather all existing slugs for auto-linking (slug-only readdir scan;
391
+ // reading/parsing every page here made wiki_write scale poorly)
392
+ const allSlugs = await listPageSlugs(wikiDir);
359
393
 
360
394
  for (const pageSpec of params.pages) {
361
395
  // Sanitize slug
@@ -375,21 +409,39 @@ function createWriteExecute(
375
409
  // Track this slug for auto-linking other pages in this batch
376
410
  allSlugs.add(slug);
377
411
 
412
+ if (pageSpec.mode === "create" && existing) {
413
+ pagesSkipped.push(`${pagePath} (already exists; use mode=replace, mode=append, or wiki_edit)`);
414
+ continue;
415
+ }
416
+
417
+ if (pageSpec.mode === "edit" && !existing) {
418
+ pagesSkipped.push(`${pagePath} (missing; wiki_write mode=edit requires an existing page)`);
419
+ continue;
420
+ }
421
+
378
422
  if (pageSpec.mode === "edit" && existing) {
379
- // Edit mode: apply targeted oldText→newText replacements
423
+ // Legacy edit mode: keep for compatibility, but make it atomic like wiki_edit.
380
424
  const edits = pageSpec.edits ?? [];
381
425
  if (edits.length === 0) {
382
426
  continue; // no edits to apply
383
427
  }
384
- let editedBody = existing.body;
385
428
  const errors: string[] = [];
386
429
  for (const edit of edits) {
387
- const idx = editedBody.indexOf(edit.oldText);
388
- if (idx === -1) {
430
+ const first = existing.body.indexOf(edit.oldText);
431
+ const last = existing.body.lastIndexOf(edit.oldText);
432
+ if (first === -1) {
389
433
  errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
390
- continue;
434
+ } else if (first !== last) {
435
+ errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
391
436
  }
392
- editedBody = editedBody.slice(0, idx) + edit.newText + editedBody.slice(idx + edit.oldText.length);
437
+ }
438
+ if (errors.length > 0) {
439
+ pagesSkipped.push(`${pagePath} (${errors.join("; ")})`);
440
+ continue;
441
+ }
442
+ let editedBody = existing.body;
443
+ for (const edit of edits) {
444
+ editedBody = editedBody.replace(edit.oldText, edit.newText);
393
445
  }
394
446
  const linkedBody = redactSecrets(autoLinkSlugs(editedBody, allSlugs, slug)).text;
395
447
  const rawScope = pageSpec.scope.length > 0 ? pageSpec.scope : existing.frontmatter.scope;
@@ -500,8 +552,12 @@ function createWriteExecute(
500
552
  logAppended = true;
501
553
  }
502
554
 
503
- // Re-index for BM25 (fast). Do NOT embed — deferred to shutdown.
504
- await reindex(store);
555
+ // Defer QMD BM25 reindex to the debounced background maintenance queue
556
+ // (same as wiki_edit). Immediate post-write queries are safe: queryWiki
557
+ // retries with a fresh store.update() before returning zero results.
558
+ if (pagesWritten.length > 0) {
559
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
560
+ }
505
561
 
506
562
  // Mark context cache dirty so before_agent_start rebuilds
507
563
  markDirty();
@@ -514,9 +570,12 @@ function createWriteExecute(
514
570
  await gitCommit(wikiDir, commitMsg);
515
571
  }
516
572
 
517
- const summary = pagesWritten.length > 0
518
- ? `Wrote ${pagesWritten.length} page(s): ${pagesWritten.join(", ")}`
519
- : "No pages written.";
573
+ const summary = [
574
+ pagesWritten.length > 0
575
+ ? `Wrote ${pagesWritten.length} page(s): ${pagesWritten.join(", ")}`
576
+ : "No pages written.",
577
+ pagesSkipped.length > 0 ? `Skipped ${pagesSkipped.length} page(s): ${pagesSkipped.join(", ")}` : "",
578
+ ].filter(Boolean).join("\n");
520
579
 
521
580
  return {
522
581
  content: [
@@ -535,6 +594,7 @@ function createWriteExecute(
535
594
  pagesWritten,
536
595
  indexUpdated,
537
596
  logAppended,
597
+ pagesSkipped,
538
598
  },
539
599
  };
540
600
  };
@@ -542,11 +602,129 @@ function createWriteExecute(
542
602
 
543
603
  // -- Factory: wiki_read execute ----------------------------------------------
544
604
 
605
+ function createEditExecute(
606
+ wikiDir: string,
607
+ store: QMDStore,
608
+ markDirty: () => void,
609
+ ) {
610
+ return async (
611
+ params: {
612
+ path: string;
613
+ edits: Array<{ oldText: string; newText: string }>;
614
+ title?: string;
615
+ scope?: string[];
616
+ tags?: string[];
617
+ logSummary?: string;
618
+ },
619
+ ): Promise<AgentToolResult<WikiWriteDetails>> => {
620
+ const parsed = parsePagePath(params.path);
621
+ if (!parsed) {
622
+ return {
623
+ content: [{ type: "text", text: `Invalid page path: ${params.path}` }],
624
+ details: { pagesWritten: [], pagesSkipped: [params.path], indexUpdated: false, logAppended: false },
625
+ };
626
+ }
627
+
628
+ const existing = await readPage(wikiDir, parsed.category, parsed.slug);
629
+ const pagePath = `${parsed.category}/${parsed.slug}`;
630
+ if (!existing) {
631
+ return {
632
+ content: [{ type: "text", text: `Page not found: ${pagePath}` }],
633
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
634
+ };
635
+ }
636
+
637
+ if (params.edits.length === 0) {
638
+ return {
639
+ content: [{ type: "text", text: "No edits supplied." }],
640
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
641
+ };
642
+ }
643
+
644
+ // Atomic validation: all oldText strings must exist exactly once before writing.
645
+ const errors: string[] = [];
646
+ for (const edit of params.edits) {
647
+ const first = existing.body.indexOf(edit.oldText);
648
+ const last = existing.body.lastIndexOf(edit.oldText);
649
+ if (first === -1) {
650
+ errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
651
+ } else if (first !== last) {
652
+ errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
653
+ }
654
+ }
655
+ if (errors.length > 0) {
656
+ return {
657
+ content: [{ type: "text", text: `No changes written.\n${errors.join("\n")}` }],
658
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
659
+ };
660
+ }
661
+
662
+ let editedBody = existing.body;
663
+ for (const edit of params.edits) {
664
+ editedBody = editedBody.replace(edit.oldText, edit.newText);
665
+ }
666
+
667
+ // Surgical edit path: do not scan/autolink/rebuild/reindex the whole wiki.
668
+ // wiki_edit is often called mid-task and must stay fast; broader maintenance
669
+ // can run via wiki_lint/rebuildIndex or on startup.
670
+ const linkedBody = redactSecrets(editedBody).text;
671
+ const now = new Date().toISOString();
672
+ const rawScope = params.scope ?? existing.frontmatter.scope;
673
+ const newScope = normalizeScopes(rawScope);
674
+ const rawTags = params.tags ?? existing.frontmatter.tags;
675
+ const updatedPage: WikiPage = {
676
+ category: parsed.category,
677
+ slug: parsed.slug,
678
+ frontmatter: {
679
+ ...existing.frontmatter,
680
+ title: params.title ?? existing.frontmatter.title,
681
+ scope: newScope,
682
+ tags: normalizeTags(rawTags, newScope),
683
+ updated: now,
684
+ links: syncFrontmatterLinks(linkedBody),
685
+ },
686
+ body: linkedBody,
687
+ };
688
+
689
+ await writePage(wikiDir, updatedPage);
690
+ if (params.logSummary) {
691
+ await appendLog(wikiDir, {
692
+ date: now.split("T")[0],
693
+ operation: "edit",
694
+ summary: params.logSummary,
695
+ pages: [pagePath],
696
+ });
697
+ }
698
+ markDirty();
699
+ await gitCommit(wikiDir, params.logSummary ?? `wiki_edit: ${pagePath}`);
700
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
701
+
702
+ return {
703
+ content: [{ type: "text", text: `Edited ${pagePath}. Scheduled background index/search refresh.` }],
704
+ details: {
705
+ pagesWritten: [pagePath],
706
+ pagesSkipped: [],
707
+ indexUpdated: false,
708
+ logAppended: !!params.logSummary,
709
+ },
710
+ };
711
+ };
712
+ }
713
+
545
714
  function createReadExecute(wikiDir: string) {
546
715
  return async (
547
716
  params: { path: string },
548
717
  ): Promise<AgentToolResult<WikiReadDetails>> => {
549
718
  const now = Date.now();
719
+ const normalizedPath = params.path.trim().toLowerCase().replace(/^\/+/, "");
720
+
721
+ if (normalizedPath === "index" || normalizedPath === "index.md" || normalizedPath === "wiki-index") {
722
+ const index = await readIndex(wikiDir);
723
+ return {
724
+ content: [{ type: "text", text: index }],
725
+ details: { found: true, path: "index.md" },
726
+ };
727
+ }
550
728
 
551
729
  // Try parsing as category/slug path
552
730
  const parsed = parsePagePath(params.path);
@@ -647,9 +825,10 @@ function createMoveExecute(
647
825
 
648
826
  await movePage(wikiDir, ref, params.to);
649
827
 
650
- // Re-index and rebuild index.md
651
- await reindex(store);
828
+ // Rebuild index.md inline (fast, single-pass); defer QMD reindex to the
829
+ // debounced background maintenance queue like wiki_write/wiki_edit.
652
830
  await rebuildIndex(wikiDir);
831
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
653
832
  markDirty();
654
833
 
655
834
  // Git commit
@@ -794,10 +973,12 @@ function createSummarizeExecute(
794
973
  };
795
974
  }
796
975
 
797
- // Generate overview prompt for the LLM
798
- const overviewPrompt = generateOverviewPrompt(pages, scope);
976
+ // Generate a bounded overview prompt for the LLM. For large targets like
977
+ // `all`, brief mode must not dump every full page body into model context.
978
+ const depth = params.depth ?? "brief";
979
+ const overviewPrompt = generateOverviewPrompt(pages, scope, { depth });
799
980
  const depthNote =
800
- params.depth === "detailed"
981
+ depth === "detailed"
801
982
  ? "\nProvide a DETAILED summary with full Key Facts, Insights, and Connections for each theme."
802
983
  : "\nProvide a BRIEF overview — one paragraph per theme, focus on high-level patterns.";
803
984
 
@@ -866,7 +1047,7 @@ function createMigrateExecute(
866
1047
  /**
867
1048
  * Register all wiki tools with the pi extension API.
868
1049
  *
869
- * Tools: wiki_ingest, wiki_query, wiki_write, wiki_read,
1050
+ * Tools: wiki_ingest, wiki_query, wiki_write, wiki_edit, wiki_read,
870
1051
  * wiki_move, wiki_lint, wiki_summarize
871
1052
  */
872
1053
  export function registerTools(
@@ -880,6 +1061,7 @@ export function registerTools(
880
1061
  const ingestExec = createIngestExecute(wikiDir, store, getScope);
881
1062
  const queryExec = createQueryExecute(wikiDir, store, getScope, getGraphBoost);
882
1063
  const writeExec = createWriteExecute(wikiDir, store, getScope, markDirty);
1064
+ const editExec = createEditExecute(wikiDir, store, markDirty);
883
1065
  const readExec = createReadExecute(wikiDir);
884
1066
  const moveExec = createMoveExecute(wikiDir, store, markDirty);
885
1067
  const lintExec = createLintExecute(wikiDir);
@@ -890,14 +1072,9 @@ export function registerTools(
890
1072
  pi.registerTool({
891
1073
  name: "wiki_ingest",
892
1074
  label: "Wiki Ingest",
893
- description:
894
- "Ingest a source (URL, file path, or raw text) into the wiki. " +
895
- "Fetches the content, saves a raw copy, and returns it with schema and index " +
896
- "so you can synthesize wiki pages via wiki_write.",
897
- promptSnippet: "Ingest a URL, file, or text into the PARA wiki knowledge base",
898
- promptGuidelines: [
899
- "Use wiki_ingest when the user provides a URL, file path, or text to add to the knowledge base.",
900
- ],
1075
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_ingest,
1076
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_ingest,
1077
+ promptGuidelines: getWikiToolGuidelines("wiki_ingest"),
901
1078
  parameters: WikiIngestParams,
902
1079
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
903
1080
  ctx.ui.setStatus("pi-para", "wiki: ingesting...");
@@ -938,16 +1115,9 @@ export function registerTools(
938
1115
  pi.registerTool({
939
1116
  name: "wiki_query",
940
1117
  label: "Wiki Query",
941
- description:
942
- "Search the wiki knowledge base with a natural language query. " +
943
- "Returns relevant pages with content snippets. " +
944
- "Use global=true to search across all project scopes.",
945
- promptSnippet: "Search the PARA wiki for relevant knowledge pages",
946
- promptGuidelines: [
947
- "Use wiki_query BEFORE answering questions that might have relevant context in the wiki — architecture decisions, past debugging solutions, project conventions, or domain knowledge.",
948
- "Use wiki_query when the user asks about something you previously discussed or captured in the wiki.",
949
- "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').",
950
- ],
1118
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_query,
1119
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_query,
1120
+ promptGuidelines: getWikiToolGuidelines("wiki_query"),
951
1121
  parameters: WikiQueryParams,
952
1122
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
953
1123
  ctx.ui.setStatus("pi-para", "wiki: searching...");
@@ -977,21 +1147,44 @@ export function registerTools(
977
1147
  },
978
1148
  });
979
1149
 
1150
+ // -- wiki_edit --
1151
+ pi.registerTool({
1152
+ name: "wiki_edit",
1153
+ label: "Wiki Edit",
1154
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_edit,
1155
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_edit,
1156
+ promptGuidelines: getWikiToolGuidelines("wiki_edit"),
1157
+ parameters: WikiEditParams,
1158
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1159
+ ctx.ui.setStatus("pi-para", "wiki: editing...");
1160
+ try { return await editExec(params); } finally { ctx.ui.setStatus("pi-para", undefined); }
1161
+ },
1162
+ renderCall(args, theme) {
1163
+ return new Text(
1164
+ theme.fg("toolTitle", theme.bold("wiki_edit ")) +
1165
+ theme.fg("muted", args.path ?? "") +
1166
+ theme.fg("dim", ` (${args.edits?.length ?? 0} edit(s))`),
1167
+ 0,
1168
+ 0,
1169
+ );
1170
+ },
1171
+ renderResult(result, _options, theme) {
1172
+ const details = result.details as WikiWriteDetails | undefined;
1173
+ if (details?.pagesWritten?.length) {
1174
+ return new Text(theme.fg("success", `Edited ${details.pagesWritten.join(", ")}`), 0, 0);
1175
+ }
1176
+ const t = result.content[0];
1177
+ return new Text(theme.fg("warning", t?.type === "text" ? t.text : "No changes"), 0, 0);
1178
+ },
1179
+ });
1180
+
980
1181
  // -- wiki_write --
981
1182
  pi.registerTool({
982
1183
  name: "wiki_write",
983
1184
  label: "Wiki Write",
984
- description:
985
- "Write or update wiki pages. Supports create, replace, append, and edit modes. " +
986
- "Optionally update index.md and append to log.md. " +
987
- "Pages are automatically re-indexed for search after writing.",
988
- promptSnippet: "Write/update PARA wiki pages with frontmatter and markdown body. Use mode=edit with edits[{oldText,newText}] to surgically update sections.",
989
- promptGuidelines: [
990
- "Use wiki_write after wiki_ingest to create/update pages based on the ingested content.",
991
- "Use wiki_write to save valuable answers or synthesis back into the wiki when they add new knowledge.",
992
- "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.",
993
- "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.",
994
- ],
1185
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_write,
1186
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_write,
1187
+ promptGuidelines: getWikiToolGuidelines("wiki_write"),
995
1188
  parameters: WikiWriteParams,
996
1189
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
997
1190
  ctx.ui.setStatus("pi-para", "wiki: writing...");
@@ -1023,6 +1216,9 @@ export function registerTools(
1023
1216
  theme.fg("muted", details.pagesWritten.join(", ")),
1024
1217
  );
1025
1218
  }
1219
+ if (details.pagesSkipped?.length) {
1220
+ parts.push(theme.fg("warning", `Skipped ${details.pagesSkipped.length} page(s): `) + theme.fg("muted", details.pagesSkipped.join(", ")));
1221
+ }
1026
1222
  if (details.indexUpdated) parts.push(theme.fg("dim", "Updated index.md"));
1027
1223
  if (details.logAppended) parts.push(theme.fg("dim", "Logged operation"));
1028
1224
  return new Text(parts.join("\n") || theme.fg("dim", "No changes"), 0, 0);
@@ -1033,13 +1229,9 @@ export function registerTools(
1033
1229
  pi.registerTool({
1034
1230
  name: "wiki_read",
1035
1231
  label: "Wiki Read",
1036
- description:
1037
- "Read a specific wiki page by path (e.g. 'projects/auth-refactor') " +
1038
- "or by title. Returns the full page content with frontmatter metadata and a freshness indicator.",
1039
- promptSnippet: "Read a wiki page by path or title. Includes freshness indicator — verify STALE pages against actual code before trusting.",
1040
- promptGuidelines: [
1041
- "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').",
1042
- ],
1232
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_read,
1233
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_read,
1234
+ promptGuidelines: getWikiToolGuidelines("wiki_read"),
1043
1235
  parameters: WikiReadParams,
1044
1236
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1045
1237
  ctx.ui.setStatus("pi-para", "wiki: reading...");
@@ -1070,10 +1262,9 @@ export function registerTools(
1070
1262
  pi.registerTool({
1071
1263
  name: "wiki_move",
1072
1264
  label: "Wiki Move",
1073
- description:
1074
- "Move a wiki page between PARA categories (e.g. move a completed project to archives). " +
1075
- "Updates frontmatter and re-indexes.",
1076
- promptSnippet: "Move a wiki page between PARA categories",
1265
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_move,
1266
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_move,
1267
+ promptGuidelines: getWikiToolGuidelines("wiki_move"),
1077
1268
  parameters: WikiMoveParams,
1078
1269
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1079
1270
  ctx.ui.setStatus("pi-para", "wiki: moving...");
@@ -1106,11 +1297,9 @@ export function registerTools(
1106
1297
  pi.registerTool({
1107
1298
  name: "wiki_lint",
1108
1299
  label: "Wiki Lint",
1109
- description:
1110
- "Run wiki health checks: orphan pages, broken links, stale pages, " +
1111
- "scope drift, archive candidates, missing pages, frontmatter issues, " +
1112
- "index drift, and duplicate slugs. Auto-fixes simple issues by default.",
1113
- promptSnippet: "Run wiki health checks and auto-fix issues",
1300
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_lint,
1301
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_lint,
1302
+ promptGuidelines: getWikiToolGuidelines("wiki_lint"),
1114
1303
  parameters: WikiLintParams,
1115
1304
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1116
1305
  ctx.ui.setStatus("pi-para", "wiki: linting...");
@@ -1149,10 +1338,9 @@ export function registerTools(
1149
1338
  pi.registerTool({
1150
1339
  name: "wiki_migrate",
1151
1340
  label: "Wiki Migrate",
1152
- description:
1153
- "Migrate all wiki pages to the current schema version. " +
1154
- "Runs pending schema migrations on any pages with an older schemaVersion.",
1155
- promptSnippet: "Batch-migrate all wiki pages to the current schema version",
1341
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_migrate,
1342
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_migrate,
1343
+ promptGuidelines: getWikiToolGuidelines("wiki_migrate"),
1156
1344
  parameters: WikiMigrateParams,
1157
1345
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1158
1346
  ctx.ui.setStatus("pi-para", "wiki: migrating...");
@@ -1190,12 +1378,9 @@ export function registerTools(
1190
1378
  pi.registerTool({
1191
1379
  name: "wiki_summarize",
1192
1380
  label: "Wiki Summarize",
1193
- description:
1194
- "Summarize a page, category, or the entire wiki. " +
1195
- "Target can be a page path (e.g. 'projects/auth-refactor'), " +
1196
- "a category name (e.g. 'projects'), or 'all'. " +
1197
- "Returns content with a summary prompt for you to synthesize.",
1198
- promptSnippet: "Summarize wiki pages, categories, or the entire wiki",
1381
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_summarize,
1382
+ promptSnippet: WIKI_TOOL_SNIPPETS.wiki_summarize,
1383
+ promptGuidelines: getWikiToolGuidelines("wiki_summarize"),
1199
1384
  parameters: WikiSummarizeParams,
1200
1385
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1201
1386
  ctx.ui.setStatus("pi-para", "wiki: summarizing...");
@@ -1231,7 +1416,7 @@ export function registerTools(
1231
1416
  /**
1232
1417
  * Create standalone AgentTool instances for the mini-agent (capture/lint/summarize).
1233
1418
  * Returns only the tools needed for autonomous wiki operations:
1234
- * wiki_write, wiki_read, wiki_query, wiki_move.
1419
+ * wiki_write, wiki_edit, wiki_read, wiki_query, wiki_move.
1235
1420
  */
1236
1421
  export function createStandaloneTools(
1237
1422
  wikiDir: string,
@@ -1241,6 +1426,7 @@ export function createStandaloneTools(
1241
1426
  ): AgentTool[] {
1242
1427
  const noopDirty = markDirty ?? (() => {});
1243
1428
  const writeExec = createWriteExecute(wikiDir, store, getScope, noopDirty);
1429
+ const editExec = createEditExecute(wikiDir, store, noopDirty);
1244
1430
  const readExec = createReadExecute(wikiDir);
1245
1431
  const queryExec = createQueryExecute(wikiDir, store, getScope);
1246
1432
  const moveExec = createMoveExecute(wikiDir, store, noopDirty);
@@ -1248,18 +1434,23 @@ export function createStandaloneTools(
1248
1434
  const wikiWrite: AgentTool<typeof WikiWriteParams> = {
1249
1435
  name: "wiki_write",
1250
1436
  label: "Wiki Write",
1251
- description:
1252
- "Write or update wiki pages. Supports create, replace, and append modes. " +
1253
- "Optionally update index.md and append to log.md.",
1437
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_write,
1254
1438
  parameters: WikiWriteParams,
1255
1439
  execute: (_toolCallId, params) => writeExec(params),
1256
1440
  };
1257
1441
 
1442
+ const wikiEdit: AgentTool<typeof WikiEditParams> = {
1443
+ name: "wiki_edit",
1444
+ label: "Wiki Edit",
1445
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_edit,
1446
+ parameters: WikiEditParams,
1447
+ execute: (_toolCallId, params) => editExec(params),
1448
+ };
1449
+
1258
1450
  const wikiRead: AgentTool<typeof WikiReadParams> = {
1259
1451
  name: "wiki_read",
1260
1452
  label: "Wiki Read",
1261
- description:
1262
- "Read a specific wiki page by path (e.g. 'projects/auth-refactor') or by title.",
1453
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_read,
1263
1454
  parameters: WikiReadParams,
1264
1455
  execute: (_toolCallId, params) => readExec(params),
1265
1456
  };
@@ -1267,8 +1458,7 @@ export function createStandaloneTools(
1267
1458
  const wikiQuery: AgentTool<typeof WikiQueryParams> = {
1268
1459
  name: "wiki_query",
1269
1460
  label: "Wiki Query",
1270
- description:
1271
- "Search the wiki knowledge base with a natural language query. Returns relevant pages.",
1461
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_query,
1272
1462
  parameters: WikiQueryParams,
1273
1463
  execute: (_toolCallId, params) => queryExec(params),
1274
1464
  };
@@ -1276,12 +1466,12 @@ export function createStandaloneTools(
1276
1466
  const wikiMove: AgentTool<typeof WikiMoveParams> = {
1277
1467
  name: "wiki_move",
1278
1468
  label: "Wiki Move",
1279
- description: "Move a wiki page between PARA categories.",
1469
+ description: WIKI_TOOL_DESCRIPTIONS.wiki_move,
1280
1470
  parameters: WikiMoveParams,
1281
1471
  execute: (_toolCallId, params) => moveExec(params),
1282
1472
  };
1283
1473
 
1284
- return [wikiWrite, wikiRead, wikiQuery, wikiMove];
1474
+ return [wikiWrite, wikiEdit, wikiRead, wikiQuery, wikiMove];
1285
1475
  }
1286
1476
 
1287
1477
  // -- Utilities ---------------------------------------------------------------