pi-para 0.6.7 → 0.7.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 (133) hide show
  1. package/ARCHITECTURE.md +4 -6
  2. package/CHANGELOG.md +19 -0
  3. package/CONFIGURATION.md +1 -16
  4. package/README.md +2 -13
  5. package/SETUP.md +1 -7
  6. package/STRUCTURE.md +7 -18
  7. package/dist/cli-legacy.d.ts +2 -1
  8. package/dist/cli-legacy.d.ts.map +1 -1
  9. package/dist/cli-legacy.js +13 -25
  10. package/dist/cli-legacy.js.map +1 -1
  11. package/dist/cli.d.ts +2 -24
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +29 -271
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +1 -28
  17. package/dist/commands.js.map +1 -1
  18. package/dist/config.d.ts +1 -5
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +0 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -64
  24. package/dist/index.js.map +1 -1
  25. package/dist/link-utils.d.ts +1 -1
  26. package/dist/link-utils.js +1 -1
  27. package/dist/processor.d.ts +7 -0
  28. package/dist/processor.d.ts.map +1 -1
  29. package/dist/processor.js +29 -15
  30. package/dist/processor.js.map +1 -1
  31. package/dist/settings.d.ts +0 -3
  32. package/dist/settings.d.ts.map +1 -1
  33. package/dist/settings.js +0 -11
  34. package/dist/settings.js.map +1 -1
  35. package/dist/tools/edit-read.d.ts +18 -0
  36. package/dist/tools/edit-read.d.ts.map +1 -0
  37. package/dist/tools/edit-read.js +161 -0
  38. package/dist/tools/edit-read.js.map +1 -0
  39. package/dist/tools/ingest.d.ts +12 -0
  40. package/dist/tools/ingest.d.ts.map +1 -0
  41. package/dist/tools/ingest.js +48 -0
  42. package/dist/tools/ingest.js.map +1 -0
  43. package/dist/tools/lint.d.ts +10 -0
  44. package/dist/tools/lint.d.ts.map +1 -0
  45. package/dist/tools/lint.js +57 -0
  46. package/dist/tools/lint.js.map +1 -0
  47. package/dist/tools/migrate.d.ts +6 -0
  48. package/dist/tools/migrate.d.ts.map +1 -0
  49. package/dist/tools/migrate.js +36 -0
  50. package/dist/tools/migrate.js.map +1 -0
  51. package/dist/tools/move.d.ts +9 -0
  52. package/dist/tools/move.d.ts.map +1 -0
  53. package/dist/tools/move.js +61 -0
  54. package/dist/tools/move.js.map +1 -0
  55. package/dist/tools/query.d.ts +12 -0
  56. package/dist/tools/query.d.ts.map +1 -0
  57. package/dist/tools/query.js +47 -0
  58. package/dist/tools/query.js.map +1 -0
  59. package/dist/tools/schemas.d.ts +109 -0
  60. package/dist/tools/schemas.d.ts.map +1 -0
  61. package/dist/tools/schemas.js +80 -0
  62. package/dist/tools/schemas.js.map +1 -0
  63. package/dist/tools/shared.d.ts +14 -0
  64. package/dist/tools/shared.d.ts.map +1 -0
  65. package/dist/tools/shared.js +25 -0
  66. package/dist/tools/shared.js.map +1 -0
  67. package/dist/tools/summarize.d.ts +8 -0
  68. package/dist/tools/summarize.d.ts.map +1 -0
  69. package/dist/tools/summarize.js +82 -0
  70. package/dist/tools/summarize.js.map +1 -0
  71. package/dist/tools/write.d.ts +23 -0
  72. package/dist/tools/write.d.ts.map +1 -0
  73. package/dist/tools/write.js +215 -0
  74. package/dist/tools/write.js.map +1 -0
  75. package/dist/tools.d.ts +1 -8
  76. package/dist/tools.d.ts.map +1 -1
  77. package/dist/tools.js +13 -795
  78. package/dist/tools.js.map +1 -1
  79. package/dist/wiki.d.ts.map +1 -1
  80. package/dist/wiki.js +1 -0
  81. package/dist/wiki.js.map +1 -1
  82. package/package.json +2 -6
  83. package/src/cli-legacy.ts +14 -26
  84. package/src/cli.ts +29 -293
  85. package/src/commands.ts +0 -21
  86. package/src/config.ts +1 -6
  87. package/src/index.ts +5 -66
  88. package/src/link-utils.ts +1 -1
  89. package/src/processor.ts +28 -13
  90. package/src/settings.ts +0 -14
  91. package/src/tools/edit-read.ts +193 -0
  92. package/src/tools/ingest.ts +63 -0
  93. package/src/tools/lint.ts +74 -0
  94. package/src/tools/migrate.ts +44 -0
  95. package/src/tools/move.ts +79 -0
  96. package/src/tools/query.ts +63 -0
  97. package/src/tools/schemas.ts +157 -0
  98. package/src/tools/shared.ts +35 -0
  99. package/src/tools/summarize.ts +89 -0
  100. package/src/tools/write.ts +252 -0
  101. package/src/tools.ts +18 -1046
  102. package/src/wiki.ts +1 -0
  103. package/dist/daemon.d.ts +0 -49
  104. package/dist/daemon.d.ts.map +0 -1
  105. package/dist/daemon.js +0 -226
  106. package/dist/daemon.js.map +0 -1
  107. package/dist/link-discovery.d.ts +0 -40
  108. package/dist/link-discovery.d.ts.map +0 -1
  109. package/dist/link-discovery.js +0 -122
  110. package/dist/link-discovery.js.map +0 -1
  111. package/dist/maintainer.d.ts +0 -24
  112. package/dist/maintainer.d.ts.map +0 -1
  113. package/dist/maintainer.js +0 -358
  114. package/dist/maintainer.js.map +0 -1
  115. package/dist/watcher.d.ts +0 -25
  116. package/dist/watcher.d.ts.map +0 -1
  117. package/dist/watcher.js +0 -84
  118. package/dist/watcher.js.map +0 -1
  119. package/dist/webui/client/dist/index.html +0 -1885
  120. package/dist/webui/server.d.ts +0 -31
  121. package/dist/webui/server.d.ts.map +0 -1
  122. package/dist/webui/server.js +0 -566
  123. package/dist/webui/server.js.map +0 -1
  124. package/scripts/build-assets.mjs +0 -17
  125. package/src/daemon.ts +0 -288
  126. package/src/link-discovery.ts +0 -170
  127. package/src/maintainer.ts +0 -404
  128. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  129. package/src/python/minimax_lm.py +0 -198
  130. package/src/python/rlm_capture.py +0 -215
  131. package/src/watcher.ts +0 -101
  132. package/src/webui/client/dist/index.html +0 -1885
  133. package/src/webui/server.ts +0 -765
@@ -0,0 +1,193 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { QMDStore } from "qmd-engine";
3
+ import { formatFreshness } from "../query.js";
4
+ import { autoLinkSlugs, syncFrontmatterLinks } from "../link-utils.js";
5
+ import { normalizeTags, normalizeScopes } from "../tag-registry.js";
6
+ import { redactSecrets } from "../redact.js";
7
+ import { appendLog, gitCommit, listPages, readIndex, readPage, writePage, type WikiPage } from "../wiki.js";
8
+ import type { WikiReadDetails, WikiWriteDetails } from "./schemas.js";
9
+ import { parsePagePath, scheduleWikiMaintenance } from "./shared.js";
10
+
11
+ // -- Factory: wiki_read execute ----------------------------------------------
12
+
13
+ export function createEditExecute(
14
+ wikiDir: string,
15
+ store: QMDStore,
16
+ markDirty: () => void,
17
+ ) {
18
+ return async (
19
+ params: {
20
+ path: string;
21
+ edits: Array<{ oldText: string; newText: string }>;
22
+ title?: string;
23
+ scope?: string[];
24
+ tags?: string[];
25
+ logSummary?: string;
26
+ },
27
+ ): Promise<AgentToolResult<WikiWriteDetails>> => {
28
+ const parsed = parsePagePath(params.path);
29
+ if (!parsed) {
30
+ return {
31
+ content: [{ type: "text", text: `Invalid page path: ${params.path}` }],
32
+ details: { pagesWritten: [], pagesSkipped: [params.path], indexUpdated: false, logAppended: false },
33
+ };
34
+ }
35
+
36
+ const existing = await readPage(wikiDir, parsed.category, parsed.slug);
37
+ const pagePath = `${parsed.category}/${parsed.slug}`;
38
+ if (!existing) {
39
+ return {
40
+ content: [{ type: "text", text: `Page not found: ${pagePath}` }],
41
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
42
+ };
43
+ }
44
+
45
+ if (params.edits.length === 0) {
46
+ return {
47
+ content: [{ type: "text", text: "No edits supplied." }],
48
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
49
+ };
50
+ }
51
+
52
+ // Atomic validation: all oldText strings must exist exactly once before writing.
53
+ const errors: string[] = [];
54
+ for (const edit of params.edits) {
55
+ const first = existing.body.indexOf(edit.oldText);
56
+ const last = existing.body.lastIndexOf(edit.oldText);
57
+ if (first === -1) {
58
+ errors.push(`oldText not found: "${edit.oldText.slice(0, 60)}..."`);
59
+ } else if (first !== last) {
60
+ errors.push(`oldText is not unique: "${edit.oldText.slice(0, 60)}..."`);
61
+ }
62
+ }
63
+ if (errors.length > 0) {
64
+ return {
65
+ content: [{ type: "text", text: `No changes written.\n${errors.join("\n")}` }],
66
+ details: { pagesWritten: [], pagesSkipped: [pagePath], indexUpdated: false, logAppended: false },
67
+ };
68
+ }
69
+
70
+ let editedBody = existing.body;
71
+ for (const edit of params.edits) {
72
+ editedBody = editedBody.replace(edit.oldText, edit.newText);
73
+ }
74
+
75
+ // Surgical edit path: do not scan/autolink/rebuild/reindex the whole wiki.
76
+ // wiki_edit is often called mid-task and must stay fast; broader maintenance
77
+ // can run via wiki_lint/rebuildIndex or on startup.
78
+ const linkedBody = redactSecrets(editedBody).text;
79
+ const now = new Date().toISOString();
80
+ const rawScope = params.scope ?? existing.frontmatter.scope;
81
+ const newScope = normalizeScopes(rawScope);
82
+ const rawTags = params.tags ?? existing.frontmatter.tags;
83
+ const updatedPage: WikiPage = {
84
+ category: parsed.category,
85
+ slug: parsed.slug,
86
+ frontmatter: {
87
+ ...existing.frontmatter,
88
+ title: params.title ?? existing.frontmatter.title,
89
+ scope: newScope,
90
+ tags: normalizeTags(rawTags, newScope),
91
+ updated: now,
92
+ links: syncFrontmatterLinks(linkedBody),
93
+ },
94
+ body: linkedBody,
95
+ };
96
+
97
+ await writePage(wikiDir, updatedPage);
98
+ if (params.logSummary) {
99
+ await appendLog(wikiDir, {
100
+ date: now.split("T")[0],
101
+ operation: "edit",
102
+ summary: params.logSummary,
103
+ pages: [pagePath],
104
+ });
105
+ }
106
+ markDirty();
107
+ await gitCommit(wikiDir, params.logSummary ?? `wiki_edit: ${pagePath}`);
108
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
109
+
110
+ return {
111
+ content: [{ type: "text", text: `Edited ${pagePath}. Scheduled background index/search refresh.` }],
112
+ details: {
113
+ pagesWritten: [pagePath],
114
+ pagesSkipped: [],
115
+ indexUpdated: false,
116
+ logAppended: !!params.logSummary,
117
+ },
118
+ };
119
+ };
120
+ }
121
+
122
+ export function createReadExecute(wikiDir: string) {
123
+ return async (
124
+ params: { path: string },
125
+ ): Promise<AgentToolResult<WikiReadDetails>> => {
126
+ const now = Date.now();
127
+ const normalizedPath = params.path.trim().toLowerCase().replace(/^\/+/, "");
128
+
129
+ if (normalizedPath === "index" || normalizedPath === "index.md" || normalizedPath === "wiki-index") {
130
+ const index = await readIndex(wikiDir);
131
+ return {
132
+ content: [{ type: "text", text: index }],
133
+ details: { found: true, path: "index.md" },
134
+ };
135
+ }
136
+
137
+ // Try parsing as category/slug path
138
+ const parsed = parsePagePath(params.path);
139
+ if (parsed) {
140
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
141
+ if (page) {
142
+ const fm = page.frontmatter;
143
+ const freshness = formatFreshness(fm.updated, now);
144
+ const header = [
145
+ `# ${fm.title}`,
146
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
147
+ `Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
148
+ fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
149
+ fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
150
+ ]
151
+ .filter(Boolean)
152
+ .join("\n");
153
+
154
+ return {
155
+ content: [{ type: "text", text: `${header}\n\n${page.body}` }],
156
+ details: { found: true, path: `${parsed.category}/${parsed.slug}` },
157
+ };
158
+ }
159
+ }
160
+
161
+ // Try finding by title (case-insensitive search across all pages)
162
+ const allPages = await listPages(wikiDir);
163
+ const titleLower = params.path.toLowerCase();
164
+ for (const ref of allPages) {
165
+ if (ref.title.toLowerCase() === titleLower || ref.slug === titleLower) {
166
+ const page = await readPage(wikiDir, ref.category, ref.slug);
167
+ if (page) {
168
+ const fm = page.frontmatter;
169
+ const freshness = formatFreshness(fm.updated, now);
170
+ const header = [
171
+ `# ${fm.title}`,
172
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
173
+ `Created: ${fm.created} | Updated: ${fm.updated} | ${freshness}`,
174
+ fm.sources.length > 0 ? `Sources: ${fm.sources.join(", ")}` : "",
175
+ fm.links.length > 0 ? `Links: ${fm.links.map((l) => `[[${l}]]`).join(", ")}` : "",
176
+ ]
177
+ .filter(Boolean)
178
+ .join("\n");
179
+
180
+ return {
181
+ content: [{ type: "text", text: `${header}\n\n${page.body}` }],
182
+ details: { found: true, path: `${ref.category}/${ref.slug}` },
183
+ };
184
+ }
185
+ }
186
+ }
187
+
188
+ return {
189
+ content: [{ type: "text", text: `Page not found: "${params.path}"` }],
190
+ details: { found: false },
191
+ };
192
+ };
193
+ }
@@ -0,0 +1,63 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { QMDStore } from "qmd-engine";
3
+ import type { ProjectScope } from "../scope.js";
4
+ import type { ParaCategory } from "../wiki.js";
5
+ import { resolveSource, truncateSource } from "../ingest.js";
6
+ import { INGEST_PROMPT } from "../templates/prompts.js";
7
+ import type { WikiIngestDetails } from "./schemas.js";
8
+
9
+ // -- Factory: wiki_ingest execute --------------------------------------------
10
+
11
+ export function createIngestExecute(
12
+ wikiDir: string,
13
+ _store: QMDStore,
14
+ getScope: () => ProjectScope,
15
+ ) {
16
+ return async (
17
+ params: { source: string; sourceType?: "url" | "file" | "text"; category?: ParaCategory; scope?: string[] },
18
+ ): Promise<AgentToolResult<WikiIngestDetails>> => {
19
+ const scope = getScope();
20
+ const resolved = await resolveSource(wikiDir, {
21
+ source: params.source,
22
+ sourceType: params.sourceType,
23
+ scope: params.scope,
24
+ category: params.category,
25
+ }, scope);
26
+
27
+ const categoryHint = resolved.categoryHint
28
+ ? `\nSuggested PARA category: ${resolved.categoryHint}`
29
+ : "";
30
+
31
+ // Assemble the tool result with instructions for the LLM
32
+ const toolResultText = [
33
+ INGEST_PROMPT,
34
+ "",
35
+ `Current scope: ${resolved.scopeName} (tags: ${resolved.scopeTags.join(", ")})`,
36
+ categoryHint,
37
+ resolved.rawPath ? `Raw source saved to: ${resolved.rawPath}` : "",
38
+ "",
39
+ "<wiki-schema>",
40
+ resolved.schema,
41
+ "</wiki-schema>",
42
+ "",
43
+ "<wiki-index>",
44
+ resolved.index,
45
+ "</wiki-index>",
46
+ "",
47
+ "<source-content>",
48
+ truncateSource(resolved.content),
49
+ "</source-content>",
50
+ "",
51
+ "Now analyze the source content and use wiki_write to create/update pages, update the index, and log the operation.",
52
+ ].join("\n");
53
+
54
+ return {
55
+ content: [{ type: "text", text: toolResultText }],
56
+ details: {
57
+ sourceType: resolved.sourceType,
58
+ rawPath: resolved.rawPath,
59
+ sourceLength: resolved.content.length,
60
+ },
61
+ };
62
+ };
63
+ }
@@ -0,0 +1,74 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { QMDStore } from "qmd-engine";
3
+ import { lintWiki } from "../lint.js";
4
+ import { PARA_CATEGORIES } from "../wiki.js";
5
+ import { LINT_PROMPT } from "../templates/prompts.js";
6
+ import type { WikiLintDetails } from "./schemas.js";
7
+ import { scheduleWikiMaintenance } from "./shared.js";
8
+
9
+ // -- Factory: wiki_lint execute ----------------------------------------------
10
+
11
+ export function createLintExecute(
12
+ wikiDir: string,
13
+ store: QMDStore,
14
+ markDirty: () => void,
15
+ getLintConfig?: () => { autoFix: boolean; staleDays: number },
16
+ ) {
17
+ return async (
18
+ params: { autoFix?: boolean },
19
+ ): Promise<AgentToolResult<WikiLintDetails>> => {
20
+ const configured = getLintConfig?.();
21
+ const report = await lintWiki(wikiDir, {
22
+ autoFix: params.autoFix ?? configured?.autoFix ?? true,
23
+ staleDays: configured?.staleDays ?? 90,
24
+ });
25
+ if (report.fixed.length > 0) {
26
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
27
+ }
28
+
29
+ const { issues, fixed, stats } = report;
30
+
31
+ // Format report for the LLM
32
+ const parts: string[] = [];
33
+
34
+ parts.push("## Wiki Lint Report\n");
35
+ parts.push(`Total pages: ${stats.totalPages}`);
36
+ parts.push(
37
+ `By category: ${PARA_CATEGORIES.map((c) => `${c}: ${stats.byCategory[c]}`).join(", ")}`,
38
+ );
39
+ parts.push(`Total links: ${stats.totalLinks} (broken: ${stats.brokenLinks})`);
40
+ parts.push(`Orphan pages: ${stats.orphanPages}`);
41
+ if (stats.oldestPage) parts.push(`Oldest page: ${stats.oldestPage}`);
42
+ if (stats.newestPage) parts.push(`Newest page: ${stats.newestPage}`);
43
+ if (stats.lastIngest) parts.push(`Last ingest: ${stats.lastIngest}`);
44
+
45
+ if (fixed.length > 0) {
46
+ parts.push(`\n## Auto-fixed (${fixed.length})\n`);
47
+ for (const f of fixed) {
48
+ parts.push(`- [${f.severity}] ${f.page ?? ""}: ${f.message}`);
49
+ }
50
+ }
51
+
52
+ if (issues.length > 0) {
53
+ parts.push(`\n## Remaining Issues (${issues.length})\n`);
54
+ for (const issue of issues) {
55
+ parts.push(
56
+ `- [${issue.severity}] ${issue.category}${issue.page ? ` (${issue.page})` : ""}: ${issue.message}`,
57
+ );
58
+ }
59
+ parts.push("");
60
+ parts.push(LINT_PROMPT);
61
+ } else {
62
+ parts.push("\nNo remaining issues. Wiki is healthy.");
63
+ }
64
+
65
+ return {
66
+ content: [{ type: "text", text: parts.join("\n") }],
67
+ details: {
68
+ issueCount: issues.length,
69
+ fixedCount: fixed.length,
70
+ stats,
71
+ },
72
+ };
73
+ };
74
+ }
@@ -0,0 +1,44 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { CURRENT_SCHEMA_VERSION, migrateToLatest } from "../frontmatter.js";
3
+ import { listPages, readPage, writePage, type PageFrontmatter } from "../wiki.js";
4
+
5
+ // -- Factory: wiki_migrate execute -------------------------------------------
6
+
7
+ export function createMigrateExecute(
8
+ wikiDir: string,
9
+ ) {
10
+ return async (): Promise<AgentToolResult<{ migratedCount: number; totalPages: number }>> => {
11
+ const allRefs = await listPages(wikiDir);
12
+ let migratedCount = 0;
13
+
14
+ for (const ref of allRefs) {
15
+ const page = await readPage(wikiDir, ref.category, ref.slug);
16
+ if (!page) continue;
17
+
18
+ const version = page.frontmatter.schemaVersion ?? 1;
19
+ if (version >= CURRENT_SCHEMA_VERSION) continue;
20
+
21
+ const rawObj = page.frontmatter as unknown as Record<string, unknown>;
22
+ const result = migrateToLatest(rawObj, page.body);
23
+
24
+ const migratedFm = result.fm as unknown as PageFrontmatter;
25
+ migratedFm.updated = new Date().toISOString();
26
+ await writePage(wikiDir, {
27
+ category: ref.category,
28
+ slug: ref.slug,
29
+ frontmatter: migratedFm,
30
+ body: result.body,
31
+ });
32
+ migratedCount++;
33
+ }
34
+
35
+ const summary = migratedCount > 0
36
+ ? `Migrated ${migratedCount} page(s) to schema version ${CURRENT_SCHEMA_VERSION}.`
37
+ : `All ${allRefs.length} page(s) already at schema version ${CURRENT_SCHEMA_VERSION}.`;
38
+
39
+ return {
40
+ content: [{ type: "text", text: summary }],
41
+ details: { migratedCount, totalPages: allRefs.length },
42
+ };
43
+ };
44
+ }
@@ -0,0 +1,79 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { QMDStore } from "qmd-engine";
3
+ import { appendLog, gitCommit, movePage, readPage, rebuildIndex, type PageRef, type ParaCategory } from "../wiki.js";
4
+ import type { WikiMoveDetails } from "./schemas.js";
5
+ import { parsePagePath, scheduleWikiMaintenance } from "./shared.js";
6
+
7
+ // -- Factory: wiki_move execute ----------------------------------------------
8
+
9
+ export function createMoveExecute(
10
+ wikiDir: string,
11
+ store: QMDStore,
12
+ markDirty: () => void,
13
+ ) {
14
+ return async (
15
+ params: { path: string; to: ParaCategory },
16
+ ): Promise<AgentToolResult<WikiMoveDetails>> => {
17
+ const parsed = parsePagePath(params.path);
18
+ if (!parsed) {
19
+ throw new Error(`Invalid page path: "${params.path}". Expected format: category/slug`);
20
+ }
21
+
22
+ const page = await readPage(wikiDir, parsed.category, parsed.slug);
23
+ if (!page) {
24
+ throw new Error(`Page not found: "${params.path}"`);
25
+ }
26
+
27
+ if (parsed.category === params.to) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: `Page "${params.path}" is already in ${params.to}. No move needed.`,
33
+ },
34
+ ],
35
+ details: { from: params.path, to: `${params.to}/${parsed.slug}` },
36
+ };
37
+ }
38
+
39
+ const ref: PageRef = {
40
+ category: parsed.category,
41
+ slug: parsed.slug,
42
+ title: page.frontmatter.title,
43
+ path: `${parsed.category}/${parsed.slug}.md`,
44
+ };
45
+
46
+ await movePage(wikiDir, ref, params.to);
47
+
48
+ // Rebuild index.md inline (fast, single-pass); defer QMD reindex to the
49
+ // debounced background maintenance queue like wiki_write/wiki_edit.
50
+ await rebuildIndex(wikiDir);
51
+ scheduleWikiMaintenance(wikiDir, store, markDirty);
52
+ markDirty();
53
+
54
+ // Git commit
55
+ await gitCommit(wikiDir, `move: ${page.frontmatter.title} → ${params.to}`);
56
+
57
+ // Log the move
58
+ const now = new Date().toISOString();
59
+ await appendLog(wikiDir, {
60
+ date: now.split("T")[0],
61
+ operation: "move",
62
+ summary: `Moved ${page.frontmatter.title} from ${parsed.category} to ${params.to}`,
63
+ pages: [`${params.to}/${parsed.slug}`],
64
+ });
65
+
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: `Moved "${page.frontmatter.title}" from ${parsed.category}/ to ${params.to}/`,
71
+ },
72
+ ],
73
+ details: {
74
+ from: `${parsed.category}/${parsed.slug}`,
75
+ to: `${params.to}/${parsed.slug}`,
76
+ },
77
+ };
78
+ };
79
+ }
@@ -0,0 +1,63 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { QMDStore } from "qmd-engine";
3
+ import type { ProjectScope } from "../scope.js";
4
+ import type { ParaCategory } from "../wiki.js";
5
+ import { queryWiki as queryWikiLib, formatQueryResults } from "../query.js";
6
+ import { QUERY_PROMPT } from "../templates/prompts.js";
7
+ import type { WikiQueryDetails } from "./schemas.js";
8
+
9
+ // -- Factory: wiki_query execute ---------------------------------------------
10
+
11
+ export function createQueryExecute(
12
+ _wikiDir: string,
13
+ store: QMDStore,
14
+ getScope: () => ProjectScope,
15
+ getGraphBoost?: () => boolean,
16
+ ) {
17
+ return async (
18
+ params: { query: string; global?: boolean; category?: ParaCategory; limit?: number },
19
+ ): Promise<AgentToolResult<WikiQueryDetails>> => {
20
+ const scope = getScope();
21
+ const result = await queryWikiLib(store, {
22
+ query: params.query,
23
+ global: params.global,
24
+ category: params.category,
25
+ limit: params.limit,
26
+ graphBoost: getGraphBoost?.() ?? true,
27
+ }, scope);
28
+
29
+ if (result.results.length === 0) {
30
+ return {
31
+ content: [
32
+ {
33
+ type: "text",
34
+ text: `No wiki pages found for "${params.query}". The wiki may not have content on this topic yet.`,
35
+ },
36
+ ],
37
+ details: {
38
+ resultCount: 0,
39
+ scopeUsed: result.wasGlobal ? "global" : scope.name,
40
+ },
41
+ };
42
+ }
43
+
44
+ const formatted = formatQueryResults(result.results);
45
+ const toolResultText = [
46
+ QUERY_PROMPT,
47
+ "",
48
+ `Query: "${params.query}"`,
49
+ `Scope: ${result.wasGlobal ? "global (all scopes)" : scope.name}`,
50
+ `Results: ${result.results.length}`,
51
+ "",
52
+ formatted,
53
+ ].join("\n");
54
+
55
+ return {
56
+ content: [{ type: "text", text: toolResultText }],
57
+ details: {
58
+ resultCount: result.results.length,
59
+ scopeUsed: result.wasGlobal ? "global" : scope.name,
60
+ },
61
+ };
62
+ };
63
+ }
@@ -0,0 +1,157 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { Type } from "typebox";
3
+ import type { LintReport } from "../lint.js";
4
+
5
+ // -- Parameter schemas -------------------------------------------------------
6
+
7
+ export const PARA_ENUM = StringEnum(
8
+ ["projects", "areas", "resources", "archives"] as const,
9
+ { description: "PARA category" },
10
+ );
11
+
12
+ export const PARA_ENUM_NO_ARCHIVES = StringEnum(
13
+ ["projects", "areas", "resources"] as const,
14
+ { description: "PARA category (archives excluded for new content)" },
15
+ );
16
+
17
+ export const WikiIngestParams = Type.Object({
18
+ source: Type.String({ description: "URL, file path, or raw text to ingest" }),
19
+ sourceType: Type.Optional(
20
+ StringEnum(["url", "file", "text"] as const, {
21
+ description: "Source type (auto-detected if omitted)",
22
+ }),
23
+ ),
24
+ category: Type.Optional(PARA_ENUM_NO_ARCHIVES),
25
+ scope: Type.Optional(
26
+ Type.Array(Type.String(), { description: "Scope tags for the ingested content" }),
27
+ ),
28
+ });
29
+
30
+ export const WikiQueryParams = Type.Object({
31
+ query: Type.String({ description: "Natural language search query" }),
32
+ global: Type.Optional(
33
+ Type.Boolean({ description: "Search all scopes, not just current project" }),
34
+ ),
35
+ category: Type.Optional(PARA_ENUM),
36
+ limit: Type.Optional(
37
+ Type.Number({ description: "Max results (default 10)" }),
38
+ ),
39
+ });
40
+
41
+ export 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
+
46
+ export const WikiWritePageSchema = Type.Object({
47
+ category: PARA_ENUM,
48
+ slug: Type.String({ description: "Page slug (lowercase, hyphens)" }),
49
+ title: Type.String({ description: "Page title" }),
50
+ scope: Type.Array(Type.String(), { description: "Scope tags" }),
51
+ tags: Type.Array(Type.String(), { description: "Topic tags" }),
52
+ body: Type.String({ description: "Page body in markdown (wiki summary format)" }),
53
+ mode: StringEnum(["create", "replace", "append", "edit"] as const, {
54
+ description: "Write mode: create new, replace existing, append to existing, or edit specific sections",
55
+ }),
56
+ edits: Type.Optional(Type.Array(WikiEditSchema, {
57
+ description: "For mode=edit: targeted text replacements (oldText→newText). Page must exist.",
58
+ })),
59
+ });
60
+
61
+ export const WikiWriteParams = Type.Object({
62
+ pages: Type.Array(WikiWritePageSchema, {
63
+ description: "Pages to write or update",
64
+ }),
65
+ // DEPRECATED: indexContent is ignored. Index is auto-rebuilt from all pages on disk.
66
+ // Kept in schema to avoid breaking existing callers.
67
+ indexContent: Type.Optional(
68
+ Type.String({ description: "Updated index.md content (full replacement)" }),
69
+ ),
70
+ logSummary: Type.Optional(
71
+ Type.String({ description: "One-line summary for log.md" }),
72
+ ),
73
+ });
74
+
75
+ export const WikiEditParams = Type.Object({
76
+ path: Type.String({ description: "Existing page path (e.g. 'resources/ssl-cert-gotchas')" }),
77
+ edits: Type.Array(WikiEditSchema, {
78
+ description: "Atomic exact text replacements. Every oldText must appear exactly once or no changes are written.",
79
+ }),
80
+ title: Type.Optional(Type.String({ description: "Optional replacement page title" })),
81
+ scope: Type.Optional(Type.Array(Type.String(), { description: "Optional replacement scope tags" })),
82
+ tags: Type.Optional(Type.Array(Type.String(), { description: "Optional replacement topic tags" })),
83
+ logSummary: Type.Optional(Type.String({ description: "One-line summary for log.md" })),
84
+ });
85
+
86
+ export const WikiReadParams = Type.Object({
87
+ path: Type.String({
88
+ description: "Page path (e.g. 'projects/auth-refactor') or page title",
89
+ }),
90
+ });
91
+
92
+ export const WikiMoveParams = Type.Object({
93
+ path: Type.String({
94
+ description: "Current page path (e.g. 'projects/auth-refactor')",
95
+ }),
96
+ to: PARA_ENUM,
97
+ });
98
+
99
+ export const WikiLintParams = Type.Object({
100
+ autoFix: Type.Optional(
101
+ Type.Boolean({ description: "Auto-fix simple issues (default true)" }),
102
+ ),
103
+ });
104
+
105
+ export const WikiMigrateParams = Type.Object({});
106
+
107
+ export const WikiSummarizeParams = Type.Object({
108
+ target: Type.String({
109
+ description: "Page path, category name (e.g. 'projects'), or 'all'",
110
+ }),
111
+ depth: Type.Optional(
112
+ StringEnum(["brief", "detailed"] as const, {
113
+ description: "Summary depth (default brief)",
114
+ }),
115
+ ),
116
+ });
117
+
118
+ // -- Detail types for tool results -------------------------------------------
119
+
120
+ export interface WikiIngestDetails {
121
+ sourceType: "url" | "file" | "text";
122
+ rawPath?: string;
123
+ sourceLength: number;
124
+ }
125
+
126
+ export interface WikiQueryDetails {
127
+ resultCount: number;
128
+ scopeUsed: string;
129
+ }
130
+
131
+ export interface WikiWriteDetails {
132
+ pagesWritten: string[];
133
+ indexUpdated: boolean;
134
+ logAppended: boolean;
135
+ pagesSkipped?: string[];
136
+ }
137
+
138
+ export interface WikiReadDetails {
139
+ found: boolean;
140
+ path?: string;
141
+ }
142
+
143
+ export interface WikiMoveDetails {
144
+ from: string;
145
+ to: string;
146
+ }
147
+
148
+ export interface WikiLintDetails {
149
+ issueCount: number;
150
+ fixedCount: number;
151
+ stats: LintReport["stats"];
152
+ }
153
+
154
+ export interface WikiSummarizeDetails {
155
+ target: string;
156
+ pageCount: number;
157
+ }