pi-para 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +305 -0
  3. package/SETUP.md +171 -0
  4. package/dist/capture.d.ts +34 -0
  5. package/dist/capture.d.ts.map +1 -0
  6. package/dist/capture.js +461 -0
  7. package/dist/capture.js.map +1 -0
  8. package/dist/cli.d.ts +15 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +405 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +15 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +786 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/context.d.ts +61 -0
  17. package/dist/context.d.ts.map +1 -0
  18. package/dist/context.js +289 -0
  19. package/dist/context.js.map +1 -0
  20. package/dist/daemon.d.ts +49 -0
  21. package/dist/daemon.d.ts.map +1 -0
  22. package/dist/daemon.js +226 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/frontmatter.d.ts +59 -0
  25. package/dist/frontmatter.d.ts.map +1 -0
  26. package/dist/frontmatter.js +170 -0
  27. package/dist/frontmatter.js.map +1 -0
  28. package/dist/gepa/dataset.d.ts +36 -0
  29. package/dist/gepa/dataset.d.ts.map +1 -0
  30. package/dist/gepa/dataset.js +148 -0
  31. package/dist/gepa/dataset.js.map +1 -0
  32. package/dist/gepa/evaluator.d.ts +51 -0
  33. package/dist/gepa/evaluator.d.ts.map +1 -0
  34. package/dist/gepa/evaluator.js +317 -0
  35. package/dist/gepa/evaluator.js.map +1 -0
  36. package/dist/gepa/index.d.ts +66 -0
  37. package/dist/gepa/index.d.ts.map +1 -0
  38. package/dist/gepa/index.js +240 -0
  39. package/dist/gepa/index.js.map +1 -0
  40. package/dist/gepa/llm.d.ts +35 -0
  41. package/dist/gepa/llm.d.ts.map +1 -0
  42. package/dist/gepa/llm.js +268 -0
  43. package/dist/gepa/llm.js.map +1 -0
  44. package/dist/gepa/optimized/capture-prompt.txt +65 -0
  45. package/dist/gepa/optimizer.d.ts +29 -0
  46. package/dist/gepa/optimizer.d.ts.map +1 -0
  47. package/dist/gepa/optimizer.js +249 -0
  48. package/dist/gepa/optimizer.js.map +1 -0
  49. package/dist/gepa/targets.d.ts +39 -0
  50. package/dist/gepa/targets.d.ts.map +1 -0
  51. package/dist/gepa/targets.js +326 -0
  52. package/dist/gepa/targets.js.map +1 -0
  53. package/dist/gepa/types.d.ts +168 -0
  54. package/dist/gepa/types.d.ts.map +1 -0
  55. package/dist/gepa/types.js +124 -0
  56. package/dist/gepa/types.js.map +1 -0
  57. package/dist/index.d.ts +21 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +294 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/ingest.d.ts +88 -0
  62. package/dist/ingest.d.ts.map +1 -0
  63. package/dist/ingest.js +105 -0
  64. package/dist/ingest.js.map +1 -0
  65. package/dist/link-discovery.d.ts +40 -0
  66. package/dist/link-discovery.d.ts.map +1 -0
  67. package/dist/link-discovery.js +122 -0
  68. package/dist/link-discovery.js.map +1 -0
  69. package/dist/link-utils.d.ts +30 -0
  70. package/dist/link-utils.d.ts.map +1 -0
  71. package/dist/link-utils.js +145 -0
  72. package/dist/link-utils.js.map +1 -0
  73. package/dist/lint.d.ts +39 -0
  74. package/dist/lint.d.ts.map +1 -0
  75. package/dist/lint.js +827 -0
  76. package/dist/lint.js.map +1 -0
  77. package/dist/maintainer.d.ts +24 -0
  78. package/dist/maintainer.d.ts.map +1 -0
  79. package/dist/maintainer.js +358 -0
  80. package/dist/maintainer.js.map +1 -0
  81. package/dist/processor.d.ts +14 -0
  82. package/dist/processor.d.ts.map +1 -0
  83. package/dist/processor.js +361 -0
  84. package/dist/processor.js.map +1 -0
  85. package/dist/query.d.ts +69 -0
  86. package/dist/query.d.ts.map +1 -0
  87. package/dist/query.js +107 -0
  88. package/dist/query.js.map +1 -0
  89. package/dist/raw.d.ts +57 -0
  90. package/dist/raw.d.ts.map +1 -0
  91. package/dist/raw.js +219 -0
  92. package/dist/raw.js.map +1 -0
  93. package/dist/redact.d.ts +21 -0
  94. package/dist/redact.d.ts.map +1 -0
  95. package/dist/redact.js +143 -0
  96. package/dist/redact.js.map +1 -0
  97. package/dist/scope.d.ts +54 -0
  98. package/dist/scope.d.ts.map +1 -0
  99. package/dist/scope.js +161 -0
  100. package/dist/scope.js.map +1 -0
  101. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  102. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  103. package/dist/scripts/bulk-link-fix.js +106 -0
  104. package/dist/scripts/bulk-link-fix.js.map +1 -0
  105. package/dist/session-tools.d.ts +33 -0
  106. package/dist/session-tools.d.ts.map +1 -0
  107. package/dist/session-tools.js +211 -0
  108. package/dist/session-tools.js.map +1 -0
  109. package/dist/state.d.ts +54 -0
  110. package/dist/state.d.ts.map +1 -0
  111. package/dist/state.js +176 -0
  112. package/dist/state.js.map +1 -0
  113. package/dist/store.d.ts +56 -0
  114. package/dist/store.d.ts.map +1 -0
  115. package/dist/store.js +227 -0
  116. package/dist/store.js.map +1 -0
  117. package/dist/summarize.d.ts +56 -0
  118. package/dist/summarize.d.ts.map +1 -0
  119. package/dist/summarize.js +231 -0
  120. package/dist/summarize.js.map +1 -0
  121. package/dist/tag-registry.d.ts +56 -0
  122. package/dist/tag-registry.d.ts.map +1 -0
  123. package/dist/tag-registry.js +146 -0
  124. package/dist/tag-registry.js.map +1 -0
  125. package/dist/templates/prompts.d.ts +63 -0
  126. package/dist/templates/prompts.d.ts.map +1 -0
  127. package/dist/templates/prompts.js +260 -0
  128. package/dist/templates/prompts.js.map +1 -0
  129. package/dist/tools.d.ts +26 -0
  130. package/dist/tools.d.ts.map +1 -0
  131. package/dist/tools.js +1033 -0
  132. package/dist/tools.js.map +1 -0
  133. package/dist/watcher.d.ts +25 -0
  134. package/dist/watcher.d.ts.map +1 -0
  135. package/dist/watcher.js +84 -0
  136. package/dist/watcher.js.map +1 -0
  137. package/dist/webui/client/dist/index.html +1885 -0
  138. package/dist/webui/server.d.ts +31 -0
  139. package/dist/webui/server.d.ts.map +1 -0
  140. package/dist/webui/server.js +566 -0
  141. package/dist/webui/server.js.map +1 -0
  142. package/dist/wiki.d.ts +103 -0
  143. package/dist/wiki.d.ts.map +1 -0
  144. package/dist/wiki.js +428 -0
  145. package/dist/wiki.js.map +1 -0
  146. package/package.json +142 -0
  147. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  148. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  149. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  150. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  151. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  152. package/scripts/gepa/dataset.py +192 -0
  153. package/scripts/gepa/lm_providers.py +472 -0
  154. package/scripts/gepa/metric.py +279 -0
  155. package/scripts/gepa/optimize.py +367 -0
  156. package/scripts/gepa/program.py +99 -0
  157. package/scripts/gepa/pyproject.toml +19 -0
  158. package/scripts/gepa/run-all.sh +53 -0
  159. package/scripts/gepa/uv.lock +2241 -0
  160. package/scripts/gepa/wiki_reader.py +113 -0
  161. package/skills/para/SKILL.md +77 -0
  162. package/skills/setup/SKILL.md +20 -0
  163. package/src/capture.ts +563 -0
  164. package/src/cli.ts +416 -0
  165. package/src/commands.ts +878 -0
  166. package/src/context.ts +390 -0
  167. package/src/daemon.ts +288 -0
  168. package/src/frontmatter.ts +219 -0
  169. package/src/gepa/index.ts +287 -0
  170. package/src/gepa/optimized/capture-prompt.txt +65 -0
  171. package/src/index.ts +384 -0
  172. package/src/ingest.ts +177 -0
  173. package/src/link-discovery.ts +170 -0
  174. package/src/link-utils.ts +169 -0
  175. package/src/lint.ts +1018 -0
  176. package/src/maintainer.ts +404 -0
  177. package/src/processor.ts +415 -0
  178. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  179. package/src/python/minimax_lm.py +198 -0
  180. package/src/python/rlm_capture.py +215 -0
  181. package/src/query.ts +148 -0
  182. package/src/raw.ts +276 -0
  183. package/src/redact.ts +159 -0
  184. package/src/scope.ts +214 -0
  185. package/src/scripts/bulk-link-fix.ts +126 -0
  186. package/src/session-tools.ts +260 -0
  187. package/src/state.ts +271 -0
  188. package/src/store.ts +288 -0
  189. package/src/summarize.ts +273 -0
  190. package/src/tag-registry.ts +157 -0
  191. package/src/templates/index.md +17 -0
  192. package/src/templates/prompts.ts +274 -0
  193. package/src/templates/schema.md +116 -0
  194. package/src/tools.ts +1293 -0
  195. package/src/watcher.ts +101 -0
  196. package/src/webui/client/dist/index.html +1885 -0
  197. package/src/webui/server.ts +765 -0
  198. package/src/wiki.ts +550 -0
package/src/store.ts ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * qmd store lifecycle — manages the qmd-engine store instance.
3
+ *
4
+ * Handles store creation/opening, search with scope filtering,
5
+ * re-indexing after wiki changes, and embedding lifecycle.
6
+ */
7
+
8
+ import { readFileSync, existsSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { parse as parseYaml } from "yaml";
12
+ import { createStore, extractSnippet } from "qmd-engine";
13
+ import type { QMDStore, CollectionConfig } from "qmd-engine";
14
+ import { parseFrontmatter } from "./frontmatter.js";
15
+ import { matchesScope } from "./scope.js";
16
+ import type { PageFrontmatter, PageRef, ParaCategory } from "./wiki.js";
17
+ import type { ProjectScope } from "./scope.js";
18
+
19
+ // Re-export QMDStore for convenience
20
+ export type { QMDStore } from "qmd-engine";
21
+
22
+ // -- Types ------------------------------------------------------------------
23
+
24
+ export interface WikiSearchOptions {
25
+ scope?: ProjectScope;
26
+ category?: ParaCategory;
27
+ limit?: number;
28
+ includeArchives?: boolean; // default false
29
+ graphBoost?: boolean; // default true — 1-hop wikilink expansion
30
+ }
31
+
32
+ export interface WikiSearchResult {
33
+ page: PageRef;
34
+ score: number;
35
+ snippet: string;
36
+ frontmatter: PageFrontmatter;
37
+ }
38
+
39
+ // -- PARA context descriptions -----------------------------------------------
40
+
41
+ const PARA_CONTEXTS: Record<string, string> = {
42
+ "projects/": "Active projects with defined goals and end dates",
43
+ "areas/": "Ongoing responsibilities and standards",
44
+ "resources/": "Reference material and how-to guides",
45
+ "archives/": "Completed or deprecated items",
46
+ };
47
+
48
+ const RAW_CONTEXT = "Immutable source material: articles, documents, notes. Not synthesized.";
49
+
50
+ // -- Pending embed tracking --------------------------------------------------
51
+
52
+ const pendingEmbeds = new WeakMap<QMDStore, Promise<void>>();
53
+
54
+ // -- qmd config loading ------------------------------------------------------
55
+
56
+ /**
57
+ * Read providers from ~/.config/qmd/index.yml.
58
+ * Returns undefined if no config file or no providers configured.
59
+ */
60
+ function loadQmdProviders(): Record<string, unknown> | undefined {
61
+ const configPath = join(homedir(), ".config", "qmd", "index.yml");
62
+ if (!existsSync(configPath)) return undefined;
63
+
64
+ try {
65
+ const content = readFileSync(configPath, "utf-8");
66
+ const config = parseYaml(content) as { providers?: Record<string, unknown> };
67
+ return config?.providers;
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ // -- Functions ---------------------------------------------------------------
74
+
75
+ /**
76
+ * Open and configure a qmd store for the wiki directory.
77
+ *
78
+ * Creates the store with two collections:
79
+ * - wiki: all .md files except raw/
80
+ * - raw: .md files under raw/, not included in default queries
81
+ *
82
+ * Adds PARA category contexts, runs update() for BM25,
83
+ * and schedules embed() in the background.
84
+ */
85
+ export async function openStore(wikiDir: string): Promise<QMDStore> {
86
+ // Read providers from ~/.config/qmd/index.yml so external API providers
87
+ // (embed, chat, rerank) are used instead of local GGUF models.
88
+ const providers = loadQmdProviders();
89
+
90
+ const store = await createStore({
91
+ dbPath: join(wikiDir, ".qmd.sqlite"),
92
+ config: {
93
+ collections: {
94
+ wiki: {
95
+ path: wikiDir,
96
+ pattern: "**/*.md",
97
+ ignore: ["raw/**"],
98
+ },
99
+ raw: {
100
+ path: join(wikiDir, "raw"),
101
+ pattern: "**/*.md",
102
+ includeByDefault: false,
103
+ },
104
+ },
105
+ ...(providers ? { providers: providers as CollectionConfig["providers"] } : {}),
106
+ },
107
+ });
108
+
109
+ // Add PARA category contexts to the wiki collection
110
+ for (const [prefix, description] of Object.entries(PARA_CONTEXTS)) {
111
+ await store.addContext("wiki", prefix, description);
112
+ }
113
+
114
+ // Add context for the raw collection root
115
+ await store.addContext("raw", "/", RAW_CONTEXT);
116
+
117
+ // Sync filesystem -> BM25 index (fast, search ready immediately)
118
+ await store.update();
119
+
120
+ // Schedule embedding in background (non-blocking).
121
+ // BM25 search works immediately; hybrid search improves once embed completes.
122
+ // Suppress console.error from qmd's internal embed failures.
123
+ const origError = console.error;
124
+ const embedPromise = store.embed().then(() => {
125
+ pendingEmbeds.delete(store);
126
+ }).catch(() => {
127
+ // Embedding failures are non-fatal — hybrid search degrades to BM25
128
+ pendingEmbeds.delete(store);
129
+ }).finally(() => {
130
+ console.error = origError;
131
+ });
132
+ // Temporarily suppress embed error noise during background embed
133
+ console.error = (...args: unknown[]) => {
134
+ const msg = String(args[0] ?? "");
135
+ if (msg.includes("embed error") || msg.includes("fetch failed")) return;
136
+ origError(...args);
137
+ };
138
+ pendingEmbeds.set(store, embedPromise);
139
+
140
+ return store;
141
+ }
142
+
143
+ /**
144
+ * Close the store. Awaits any pending embed operation (with timeout) first.
145
+ */
146
+ export async function closeStore(store: QMDStore): Promise<void> {
147
+ const pending = pendingEmbeds.get(store);
148
+ if (pending) {
149
+ // Wait for pending embed with a short timeout — don't hang forever
150
+ const timeout = new Promise<void>((resolve) => setTimeout(resolve, 5_000));
151
+ await Promise.race([pending, timeout]);
152
+ pendingEmbeds.delete(store);
153
+ }
154
+ await store.close();
155
+ }
156
+
157
+ /**
158
+ * Search the wiki with scope and category filtering.
159
+ *
160
+ * Uses BM25 search (searchLex) which requires no LLM. Each result's
161
+ * frontmatter is parsed to apply scope/category/archive filters.
162
+ */
163
+ export async function searchWiki(
164
+ store: QMDStore,
165
+ query: string,
166
+ opts: WikiSearchOptions,
167
+ ): Promise<WikiSearchResult[]> {
168
+ const limit = opts.limit ?? 10;
169
+
170
+ // Build metadata filter for SQL-level filtering (qmd v2.2.0+)
171
+ // Note: we can only use metadata.category here, NOT scope.
172
+ // Scope filtering must remain as post-filter because:
173
+ // - Pages with scope:["global"] must match ANY project scope
174
+ // - Pages in the scope.include list need OR matching
175
+ // - The exclude list needs post-filter negation
176
+ // SQL-level scope filter would miss global pages.
177
+ const metadata: Record<string, string> = {};
178
+ if (opts.category) {
179
+ metadata.category = opts.category;
180
+ }
181
+
182
+ const fetchLimit = opts.scope ? limit * 3 : limit;
183
+
184
+ const rawResults = await store.searchLex(query, {
185
+ limit: fetchLimit,
186
+ collection: "wiki",
187
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
188
+ graphBoost: opts.graphBoost ?? true,
189
+ });
190
+
191
+ const results: WikiSearchResult[] = [];
192
+
193
+ for (const result of rawResults) {
194
+ // Parse category and slug from the display path (e.g. "projects/auth-refactor.md")
195
+ const parsed = parseDisplayPath(result.displayPath);
196
+ if (!parsed) continue;
197
+
198
+ const { category, slug } = parsed;
199
+
200
+ // Exclude archives unless explicitly requested (post-filter fallback
201
+ // for pages without frontmatter metadata)
202
+ if (category === "archives" && !opts.includeArchives) {
203
+ continue;
204
+ }
205
+
206
+ // Parse frontmatter from body
207
+ const body = result.body ?? "";
208
+ const { frontmatter } = parseFrontmatter(body);
209
+
210
+ // Scope post-filter fallback for pages without metadata
211
+ if (opts.scope && !matchesScope(frontmatter.scope, opts.scope)) {
212
+ continue;
213
+ }
214
+
215
+ // Extract a snippet for display
216
+ const snippetResult = extractSnippet(body, query);
217
+ const snippet = snippetResult.snippet;
218
+
219
+ results.push({
220
+ page: {
221
+ category,
222
+ slug,
223
+ title: frontmatter.title || result.title,
224
+ path: `${category}/${slug}.md`,
225
+ },
226
+ score: result.score,
227
+ snippet,
228
+ frontmatter,
229
+ });
230
+
231
+ if (results.length >= limit) break;
232
+ }
233
+
234
+ return results;
235
+ }
236
+
237
+ /**
238
+ * Re-index the store (fast filesystem scan). Makes new/changed
239
+ * content BM25-searchable immediately. Call after wiki writes.
240
+ */
241
+ export async function reindex(store: QMDStore): Promise<void> {
242
+ await store.update();
243
+ }
244
+
245
+ /**
246
+ * Generate vector embeddings for new/changed pages.
247
+ * Slow — call only at startup (via openStore) or shutdown.
248
+ */
249
+ export async function embedIfNeeded(store: QMDStore): Promise<void> {
250
+ await store.embed();
251
+ }
252
+
253
+ // -- Helpers -----------------------------------------------------------------
254
+
255
+ /** Valid PARA categories for path parsing. */
256
+ const VALID_CATEGORIES = new Set<string>([
257
+ "projects",
258
+ "areas",
259
+ "resources",
260
+ "archives",
261
+ ]);
262
+
263
+ /**
264
+ * Parse a qmd display path into category + slug.
265
+ * Display paths from searchLex include the collection prefix:
266
+ * "wiki/resources/ssl-certs.md" or "wiki/schema.md"
267
+ * Returns null for root-level files (schema.md, index.md, etc.) — they
268
+ * still appear in search results but aren't PageRef-able.
269
+ */
270
+ function parseDisplayPath(
271
+ displayPath: string,
272
+ ): { category: ParaCategory; slug: string } | null {
273
+ const parts = displayPath.split("/");
274
+
275
+ // Display paths are: collection/category/slug.md (3+ parts)
276
+ // or collection/file.md (2 parts, root-level files like schema.md)
277
+ if (parts.length < 3) return null;
278
+
279
+ // Skip collection prefix (parts[0] = "wiki")
280
+ const category = parts[1];
281
+ if (!VALID_CATEGORIES.has(category)) return null;
282
+
283
+ const filename = parts.slice(2).join("/");
284
+ if (!filename.endsWith(".md")) return null;
285
+
286
+ const slug = filename.slice(0, -3);
287
+ return { category: category as ParaCategory, slug };
288
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Wiki summarization — adapts pi's compaction approach for wiki knowledge.
3
+ *
4
+ * Produces structured wiki summaries in the standard format:
5
+ * Topic, Key Facts, Insights, Connections, Open Questions, Sources.
6
+ *
7
+ * Supports ingest, session, page, and iterative summarization modes.
8
+ *
9
+ * Functions here generate prompt strings — they do NOT call the LLM.
10
+ * The caller (capture.ts, tools.ts) passes the prompt to a standalone Agent.
11
+ */
12
+
13
+ import type { WikiPage } from "./wiki.js";
14
+ import type { ParaCategory } from "./wiki.js";
15
+ import type { ProjectScope } from "./scope.js";
16
+ import {
17
+ convertToLlm,
18
+ serializeConversation,
19
+ } from "@mariozechner/pi-coding-agent";
20
+ import {
21
+ SUMMARIZE_SYSTEM_PROMPT,
22
+ ITERATIVE_UPDATE_PROMPT,
23
+ OVERVIEW_PROMPT,
24
+ CAPTURE_PROMPT,
25
+ } from "./templates/prompts.js";
26
+
27
+ // -- Types ------------------------------------------------------------------
28
+
29
+ export interface SummarizeOptions {
30
+ mode: "ingest" | "session" | "page" | "iterative";
31
+ existingContent?: string; // for iterative mode
32
+ scope: ProjectScope;
33
+ category?: ParaCategory;
34
+ }
35
+
36
+ // -- Constants ---------------------------------------------------------------
37
+
38
+ /** Maximum characters for a tool result in serialized output. */
39
+ const TOOL_RESULT_MAX_CHARS = 2000;
40
+
41
+ // -- Fallback serialization --------------------------------------------------
42
+
43
+ /**
44
+ * Truncate text to a maximum character length for summarization.
45
+ * Keeps the beginning and appends a truncation marker.
46
+ */
47
+ function truncateForSummary(text: string, maxChars: number): string {
48
+ if (text.length <= maxChars) return text;
49
+ const truncatedChars = text.length - maxChars;
50
+ return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
51
+ }
52
+
53
+ /**
54
+ * Extract text content from a message content field.
55
+ * Handles both string and array-of-blocks shapes.
56
+ */
57
+ function extractTextContent(content: unknown): string {
58
+ if (typeof content === "string") return content;
59
+ if (!Array.isArray(content)) return "";
60
+ return content
61
+ .filter(
62
+ (c: unknown): c is { type: "text"; text: string } =>
63
+ typeof c === "object" &&
64
+ c !== null &&
65
+ "type" in c &&
66
+ (c as { type: string }).type === "text" &&
67
+ "text" in c,
68
+ )
69
+ .map((c) => c.text)
70
+ .join("");
71
+ }
72
+
73
+ /**
74
+ * Fallback serialization that mirrors pi's serializeConversation format.
75
+ * Used when @mariozechner/pi-coding-agent is not available.
76
+ *
77
+ * Formats: [User]:, [Assistant]:, [Assistant thinking]:,
78
+ * [Assistant tool calls]:, [Tool result]:
79
+ */
80
+ function serializeMessagesFallback(messages: unknown[]): string {
81
+ const parts: string[] = [];
82
+
83
+ for (const msg of messages) {
84
+ if (typeof msg !== "object" || msg === null || !("role" in msg)) continue;
85
+ const m = msg as Record<string, unknown>;
86
+
87
+ if (m.role === "user") {
88
+ const text = extractTextContent(m.content);
89
+ if (text) parts.push(`[User]: ${text}`);
90
+ } else if (m.role === "assistant") {
91
+ const content = m.content;
92
+ if (!Array.isArray(content)) continue;
93
+
94
+ const textParts: string[] = [];
95
+ const thinkingParts: string[] = [];
96
+ const toolCalls: string[] = [];
97
+
98
+ for (const block of content) {
99
+ if (typeof block !== "object" || block === null || !("type" in block)) continue;
100
+ const b = block as Record<string, unknown>;
101
+
102
+ if (b.type === "text" && typeof b.text === "string") {
103
+ textParts.push(b.text);
104
+ } else if (b.type === "thinking" && typeof b.thinking === "string") {
105
+ thinkingParts.push(b.thinking);
106
+ } else if (b.type === "toolCall" && typeof b.name === "string") {
107
+ const args = (b.arguments ?? {}) as Record<string, unknown>;
108
+ const argsStr = Object.entries(args)
109
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
110
+ .join(", ");
111
+ toolCalls.push(`${b.name}(${argsStr})`);
112
+ }
113
+ }
114
+
115
+ if (thinkingParts.length > 0) {
116
+ parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
117
+ }
118
+ if (textParts.length > 0) {
119
+ parts.push(`[Assistant]: ${textParts.join("\n")}`);
120
+ }
121
+ if (toolCalls.length > 0) {
122
+ parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
123
+ }
124
+ } else if (m.role === "toolResult") {
125
+ const text = extractTextContent(m.content);
126
+ if (text) {
127
+ parts.push(`[Tool result]: ${truncateForSummary(text, TOOL_RESULT_MAX_CHARS)}`);
128
+ }
129
+ }
130
+ }
131
+
132
+ return parts.join("\n\n");
133
+ }
134
+
135
+ // -- Public API --------------------------------------------------------------
136
+
137
+ /**
138
+ * Serialize session messages for wiki capture.
139
+ *
140
+ * Converts AgentMessage[] to a text representation that prevents the LLM
141
+ * from treating it as a conversation to continue. Uses pi's convertToLlm +
142
+ * serializeConversation when the messages contain pi-specific types
143
+ * (bashExecution, compactionSummary, branchSummary, etc.). Falls back to
144
+ * a compatible reimplementation for plain user/assistant/toolResult messages.
145
+ *
146
+ * @param messages - AgentMessage[] from @mariozechner/pi-agent-core
147
+ * @returns Serialized text in [User]: / [Assistant]: / [Tool result]: format
148
+ */
149
+ export function serializeSessionForWiki(
150
+ messages: unknown[], // AgentMessage[] — typed as unknown[] to avoid hard dep
151
+ ): string {
152
+ // Use pi's convertToLlm to handle custom message types (bashExecution,
153
+ // compactionSummary, branchSummary, etc.), then serialize the resulting
154
+ // standard LLM messages.
155
+ try {
156
+ const llmMessages = convertToLlm(messages as Parameters<typeof convertToLlm>[0]);
157
+ return serializeConversation(llmMessages);
158
+ } catch {
159
+ // If convertToLlm fails (e.g., unexpected message shapes), fall back
160
+ // to our reimplementation that handles the base roles.
161
+ return serializeMessagesFallback(messages);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Generate a wiki-format summary prompt from raw content.
167
+ *
168
+ * Returns the prompt string to send to the LLM. The caller passes this
169
+ * to a standalone Agent or includes it in a tool result for the session agent.
170
+ *
171
+ * @param content - Source text, serialized conversation, or page content
172
+ * @param options - Summarization mode and context
173
+ * @returns Prompt string for the LLM
174
+ */
175
+ export function generateSummary(
176
+ content: string,
177
+ options: SummarizeOptions,
178
+ ): string {
179
+ const scopeInfo = `Current scope: ${options.scope.name} (tags: ${options.scope.include.join(", ")})`;
180
+ const categoryHint = options.category ? `\nSuggested PARA category: ${options.category}` : "";
181
+
182
+ switch (options.mode) {
183
+ case "ingest":
184
+ return [
185
+ SUMMARIZE_SYSTEM_PROMPT,
186
+ "",
187
+ scopeInfo,
188
+ categoryHint,
189
+ "",
190
+ "Produce a wiki page from the following source material:",
191
+ "",
192
+ "<source-content>",
193
+ content,
194
+ "</source-content>",
195
+ ].join("\n");
196
+
197
+ case "session":
198
+ return [
199
+ CAPTURE_PROMPT,
200
+ content,
201
+ "</session-conversation>",
202
+ ].join("\n");
203
+
204
+ case "page":
205
+ return [
206
+ SUMMARIZE_SYSTEM_PROMPT,
207
+ "",
208
+ scopeInfo,
209
+ "",
210
+ "Summarize the following wiki page into a concise overview:",
211
+ "",
212
+ "<page-content>",
213
+ content,
214
+ "</page-content>",
215
+ ].join("\n");
216
+
217
+ case "iterative":
218
+ if (!options.existingContent) {
219
+ // No existing content — treat as a fresh page
220
+ return generateSummary(content, { ...options, mode: "ingest" });
221
+ }
222
+ return [
223
+ ITERATIVE_UPDATE_PROMPT,
224
+ "",
225
+ scopeInfo,
226
+ categoryHint,
227
+ "",
228
+ "<existing-page>",
229
+ options.existingContent,
230
+ "</existing-page>",
231
+ "",
232
+ "<new-content>",
233
+ content,
234
+ "</new-content>",
235
+ ].join("\n");
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Generate an overview prompt from multiple wiki pages.
241
+ *
242
+ * Used by /wiki-summarize and wiki_summarize tool to produce high-level
243
+ * overviews of a category or the entire wiki.
244
+ *
245
+ * @param pages - Wiki pages to summarize
246
+ * @param scope - Current project scope for context
247
+ * @returns Prompt string for the LLM
248
+ */
249
+ export function generateOverviewPrompt(
250
+ pages: WikiPage[],
251
+ scope: ProjectScope,
252
+ ): string {
253
+ const scopeInfo = `Current scope: ${scope.name} (tags: ${scope.include.join(", ")})`;
254
+
255
+ const pageEntries = pages.map((page) => {
256
+ const fm = page.frontmatter;
257
+ return [
258
+ `### ${fm.title} (${page.category}/${page.slug})`,
259
+ `PARA: ${fm.para} | Scope: ${fm.scope.join(", ")} | Tags: ${fm.tags.join(", ")}`,
260
+ "",
261
+ page.body,
262
+ ].join("\n");
263
+ });
264
+
265
+ return [
266
+ OVERVIEW_PROMPT,
267
+ "",
268
+ scopeInfo,
269
+ `Total pages: ${pages.length}`,
270
+ "",
271
+ ...pageEntries,
272
+ ].join("\n");
273
+ }