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/wiki.ts ADDED
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Wiki filesystem operations — manages ~/.pi/wiki/ directory structure.
3
+ *
4
+ * Handles CRUD for wiki pages (markdown + YAML frontmatter), index/log
5
+ * maintenance, page moves between PARA categories, and wiki initialization.
6
+ */
7
+
8
+ import {
9
+ readFile,
10
+ writeFile,
11
+ mkdir,
12
+ unlink,
13
+ readdir,
14
+ access,
15
+ } from "node:fs/promises";
16
+ import { join } from "node:path";
17
+ import { execFile } from "node:child_process";
18
+ import {
19
+ parseFrontmatter,
20
+ serializeFrontmatter,
21
+ } from "./frontmatter.js";
22
+ import type { StateDB } from "./state.js";
23
+
24
+ // -- Types ------------------------------------------------------------------
25
+
26
+ export type ParaCategory = "projects" | "areas" | "resources" | "archives";
27
+
28
+ export interface PageFrontmatter {
29
+ title: string;
30
+ para: ParaCategory;
31
+ scope: string[];
32
+ tags: string[];
33
+ sources: string[];
34
+ created: string; // ISO date
35
+ updated: string; // ISO date
36
+ links: string[]; // outgoing [[wikilinks]]
37
+ schemaVersion: number; // schema version for migration tracking
38
+ }
39
+
40
+ export interface WikiPage {
41
+ category: ParaCategory;
42
+ slug: string;
43
+ frontmatter: PageFrontmatter;
44
+ body: string;
45
+ }
46
+
47
+ export interface PageRef {
48
+ category: ParaCategory;
49
+ slug: string;
50
+ title: string;
51
+ path: string; // relative path within wiki dir
52
+ }
53
+
54
+ export interface LogEntry {
55
+ date: string;
56
+ operation: "ingest" | "query" | "lint" | "capture" | "move" | "archive";
57
+ summary: string;
58
+ pages: string[];
59
+ }
60
+
61
+ // -- Constants ---------------------------------------------------------------
62
+
63
+ export const PARA_CATEGORIES: readonly ParaCategory[] = [
64
+ "projects",
65
+ "areas",
66
+ "resources",
67
+ "archives",
68
+ ];
69
+
70
+ const DEFAULT_SCHEMA = `# Wiki Schema
71
+
72
+ ## Page Format
73
+
74
+ Every wiki page is a markdown file with YAML frontmatter:
75
+
76
+ \`\`\`yaml
77
+ ---
78
+ title: Page Title
79
+ para: projects | areas | resources | archives
80
+ scope:
81
+ - project-name
82
+ - global
83
+ tags:
84
+ - topic-tag
85
+ sources:
86
+ - https://example.com
87
+ - session:~/.pi/agent/sessions/.../file.jsonl
88
+ created: "2026-01-01"
89
+ updated: "2026-01-01"
90
+ links:
91
+ - other-page-slug
92
+ ---
93
+ \`\`\`
94
+
95
+ ## PARA Categories
96
+
97
+ - **projects/**: Active, goal-defined work with an end date. Default scope: current project name.
98
+ - **areas/**: Ongoing responsibilities with no end date. Default scope: \`["global"]\`.
99
+ - **resources/**: Reference material, how-tos, patterns. Scope assigned by content analysis.
100
+ - **archives/**: Completed, deprecated, or inactive items. Moved from other categories.
101
+
102
+ ## Naming Conventions
103
+
104
+ - Slugs: lowercase, hyphens, no special characters (e.g., \`ssl-cert-gotchas\`)
105
+ - One concept per page
106
+ - Use [[wikilinks]] for cross-references: \`[[slug]]\`
107
+
108
+ ## Wiki Summary Format
109
+
110
+ \`\`\`markdown
111
+ ## Topic
112
+ [What this page covers]
113
+
114
+ ## Key Facts
115
+ - [Established knowledge points]
116
+
117
+ ## Insights
118
+ - [Non-obvious findings, patterns, implications]
119
+
120
+ ## Connections
121
+ - [[related-page]] — how this relates
122
+
123
+ ## Open Questions
124
+ - [Gaps in knowledge, unresolved contradictions]
125
+
126
+ ## Sources
127
+ - [Source URLs, file paths, session references]
128
+ \`\`\`
129
+
130
+ ## Index Format
131
+
132
+ \`index.md\` is organized by PARA category with one-line summaries per page.
133
+
134
+ ## Log Format
135
+
136
+ \`log.md\` uses the heading format: \`## [YYYY-MM-DD] operation | summary\`
137
+
138
+ ## Tone
139
+
140
+ Technical, concise, factual. No fluff.
141
+
142
+ ## Updates vs. New Pages
143
+
144
+ - Update an existing page when new information relates to the same concept
145
+ - Create a new page when the concept is distinct enough to stand alone
146
+ - When in doubt, create a new page and add [[wikilinks]]
147
+
148
+ ## Archiving
149
+
150
+ Move projects to archives when:
151
+ - The project goal is completed
152
+ - No log entries in 90+ days
153
+ - Explicitly requested by user
154
+ `;
155
+
156
+ const DEFAULT_INDEX = `# Wiki Index
157
+
158
+ ## Projects
159
+
160
+ _No active projects yet._
161
+
162
+ ## Areas
163
+
164
+ _No areas defined yet._
165
+
166
+ ## Resources
167
+
168
+ _No resources yet._
169
+
170
+ ## Archives
171
+
172
+ _No archived items._
173
+ `;
174
+
175
+ const DEFAULT_LOG = "# Activity Log\n";
176
+
177
+ const DEFAULT_SESSIONS = "# Session Digests\n";
178
+
179
+ // -- Helpers -----------------------------------------------------------------
180
+
181
+ async function fileExists(path: string): Promise<boolean> {
182
+ try {
183
+ await access(path);
184
+ return true;
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ async function seedFile(path: string, content: string): Promise<void> {
191
+ if (!(await fileExists(path))) {
192
+ await writeFile(path, content, "utf-8");
193
+ }
194
+ }
195
+
196
+ // -- Functions ---------------------------------------------------------------
197
+
198
+ /**
199
+ * Initialize a wiki directory. Creates the PARA category directories,
200
+ * raw source directories, and seeds schema.md, index.md, log.md, and
201
+ * sessions.md if they do not already exist. Idempotent.
202
+ */
203
+ export async function initWiki(wikiDir: string): Promise<void> {
204
+ // Create PARA category directories
205
+ for (const cat of PARA_CATEGORIES) {
206
+ await mkdir(join(wikiDir, cat), { recursive: true });
207
+ }
208
+
209
+ // Create raw source directories
210
+ for (const sub of ["articles", "docs", "notes"]) {
211
+ await mkdir(join(wikiDir, "raw", sub), { recursive: true });
212
+ }
213
+
214
+ // Seed default files (only if missing)
215
+ await seedFile(join(wikiDir, "schema.md"), DEFAULT_SCHEMA);
216
+ await seedFile(join(wikiDir, "index.md"), DEFAULT_INDEX);
217
+ await seedFile(join(wikiDir, "log.md"), DEFAULT_LOG);
218
+ await seedFile(join(wikiDir, "sessions.md"), DEFAULT_SESSIONS);
219
+ }
220
+
221
+ /**
222
+ * Read a wiki page by category and slug. Returns null if the page does
223
+ * not exist. Parses YAML frontmatter via frontmatter.ts.
224
+ */
225
+ export async function readPage(
226
+ wikiDir: string,
227
+ category: ParaCategory,
228
+ slug: string,
229
+ ): Promise<WikiPage | null> {
230
+ const filePath = join(wikiDir, category, `${slug}.md`);
231
+ let content: string;
232
+ try {
233
+ content = await readFile(filePath, "utf-8");
234
+ } catch (err: unknown) {
235
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
236
+ return null;
237
+ }
238
+ throw err;
239
+ }
240
+
241
+ const { frontmatter, body } = parseFrontmatter(content);
242
+ return { category, slug, frontmatter, body };
243
+ }
244
+
245
+ /**
246
+ * Write a wiki page. Creates the category directory if needed. Serializes
247
+ * frontmatter and body via frontmatter.ts.
248
+ *
249
+ * If stateDb is provided, also updates the page summary cache.
250
+ */
251
+ export async function writePage(
252
+ wikiDir: string,
253
+ page: WikiPage,
254
+ stateDb?: StateDB,
255
+ ): Promise<void> {
256
+ const dirPath = join(wikiDir, page.category);
257
+ await mkdir(dirPath, { recursive: true });
258
+ const filePath = join(dirPath, `${page.slug}.md`);
259
+ const content = serializeFrontmatter(page.frontmatter, page.body);
260
+ await writeFile(filePath, content, "utf-8");
261
+
262
+ // Update page summary cache if state DB is available
263
+ if (stateDb) {
264
+ const firstPara = extractFirstParagraphFromBody(page.body);
265
+ stateDb.upsertPageSummary(
266
+ page.slug,
267
+ page.category,
268
+ page.frontmatter.scope,
269
+ page.frontmatter.tags,
270
+ firstPara,
271
+ page.frontmatter.updated,
272
+ );
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Delete a wiki page. No-op if the page does not exist.
278
+ */
279
+ export async function deletePage(
280
+ wikiDir: string,
281
+ category: ParaCategory,
282
+ slug: string,
283
+ ): Promise<void> {
284
+ const filePath = join(wikiDir, category, `${slug}.md`);
285
+ try {
286
+ await unlink(filePath);
287
+ } catch (err: unknown) {
288
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
289
+ return;
290
+ }
291
+ throw err;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Move a page between PARA categories. Updates the page's `para` and
297
+ * `updated` frontmatter fields. The slug is preserved.
298
+ */
299
+ export async function movePage(
300
+ wikiDir: string,
301
+ from: PageRef,
302
+ toCategory: ParaCategory,
303
+ ): Promise<void> {
304
+ const srcPath = join(wikiDir, from.category, `${from.slug}.md`);
305
+ const destDir = join(wikiDir, toCategory);
306
+ const destPath = join(destDir, `${from.slug}.md`);
307
+
308
+ // Read source
309
+ const content = await readFile(srcPath, "utf-8");
310
+ const { frontmatter, body } = parseFrontmatter(content);
311
+
312
+ // Update frontmatter
313
+ frontmatter.para = toCategory;
314
+ frontmatter.updated = new Date().toISOString();
315
+
316
+ // Write to new location, then remove old file
317
+ await mkdir(destDir, { recursive: true });
318
+ await writeFile(destPath, serializeFrontmatter(frontmatter, body), "utf-8");
319
+ await unlink(srcPath);
320
+ }
321
+
322
+ /**
323
+ * List pages across one or all PARA categories. Returns PageRef objects
324
+ * sorted by category then slug.
325
+ */
326
+ export async function listPages(
327
+ wikiDir: string,
328
+ category?: ParaCategory,
329
+ ): Promise<PageRef[]> {
330
+ const categories = category ? [category] : PARA_CATEGORIES;
331
+ const refs: PageRef[] = [];
332
+
333
+ for (const cat of categories) {
334
+ const dirPath = join(wikiDir, cat);
335
+ let entries: string[];
336
+ try {
337
+ entries = await readdir(dirPath);
338
+ } catch {
339
+ continue; // directory doesn't exist yet
340
+ }
341
+
342
+ for (const entry of entries) {
343
+ if (!entry.endsWith(".md")) continue;
344
+ const slug = entry.slice(0, -3);
345
+ const filePath = join(dirPath, entry);
346
+ const content = await readFile(filePath, "utf-8");
347
+ const { frontmatter } = parseFrontmatter(content);
348
+ refs.push({
349
+ category: cat,
350
+ slug,
351
+ title: frontmatter.title,
352
+ path: `${cat}/${entry}`,
353
+ });
354
+ }
355
+ }
356
+
357
+ // Sort by category order, then slug alphabetically
358
+ refs.sort((a, b) => {
359
+ const catCmp =
360
+ PARA_CATEGORIES.indexOf(a.category) -
361
+ PARA_CATEGORIES.indexOf(b.category);
362
+ if (catCmp !== 0) return catCmp;
363
+ return a.slug.localeCompare(b.slug);
364
+ });
365
+
366
+ return refs;
367
+ }
368
+
369
+ /**
370
+ * Read the wiki index (index.md).
371
+ */
372
+ export async function readIndex(wikiDir: string): Promise<string> {
373
+ return readFile(join(wikiDir, "index.md"), "utf-8");
374
+ }
375
+
376
+ /**
377
+ * Write the wiki index (index.md).
378
+ */
379
+ export async function writeIndex(
380
+ wikiDir: string,
381
+ content: string,
382
+ ): Promise<void> {
383
+ await writeFile(join(wikiDir, "index.md"), content, "utf-8");
384
+ }
385
+
386
+ /**
387
+ * Append an entry to the activity log (log.md).
388
+ */
389
+ export async function appendLog(
390
+ wikiDir: string,
391
+ entry: LogEntry,
392
+ ): Promise<void> {
393
+ const logPath = join(wikiDir, "log.md");
394
+ const existing = await readFile(logPath, "utf-8");
395
+ const pages = entry.pages.length > 0 ? entry.pages.join(", ") : "none";
396
+ const line = `\n## [${entry.date}] ${entry.operation} | ${entry.summary}\nPages: ${pages}\n`;
397
+ await writeFile(logPath, existing + line, "utf-8");
398
+ }
399
+
400
+ /**
401
+ * Read the wiki schema (schema.md).
402
+ */
403
+ export async function readSchema(wikiDir: string): Promise<string> {
404
+ return readFile(join(wikiDir, "schema.md"), "utf-8");
405
+ }
406
+
407
+ // -- Git auto-commit ---------------------------------------------------------
408
+
409
+ /**
410
+ * Run a git command in the wiki directory. Returns stdout or null on failure.
411
+ */
412
+ function git(wikiDir: string, args: string[]): Promise<string | null> {
413
+ return new Promise((resolve) => {
414
+ execFile("git", args, { cwd: wikiDir, timeout: 10_000 }, (err, stdout) => {
415
+ if (err) resolve(null);
416
+ else resolve(stdout.trim());
417
+ });
418
+ });
419
+ }
420
+
421
+ // -- Index rebuild -----------------------------------------------------------
422
+
423
+ /**
424
+ * Rebuild index.md deterministically from all pages on disk.
425
+ *
426
+ * Reads all pages, groups by PARA category, sorts alphabetically within
427
+ * each category, and writes a standardized index.md with one-line summaries.
428
+ */
429
+ export async function rebuildIndex(wikiDir: string): Promise<void> {
430
+ const allRefs = await listPages(wikiDir);
431
+
432
+ const sections: Record<ParaCategory, string[]> = {
433
+ projects: [],
434
+ areas: [],
435
+ resources: [],
436
+ archives: [],
437
+ };
438
+
439
+ for (const ref of allRefs) {
440
+ const page = await readPage(wikiDir, ref.category, ref.slug);
441
+ const title = page?.frontmatter.title ?? ref.title;
442
+ const summary = page ? extractFirstParagraphFromBody(page.body) : "";
443
+ const desc = summary && summary !== "(no summary)"
444
+ ? ": " + (summary.length > 120 ? summary.slice(0, 117) + "..." : summary)
445
+ : "";
446
+ sections[ref.category].push(`- [[${ref.slug}]] \u2014 ${title}${desc}`);
447
+ }
448
+
449
+ const categoryLabels: Record<ParaCategory, string> = {
450
+ projects: "Projects",
451
+ areas: "Areas",
452
+ resources: "Resources",
453
+ archives: "Archives",
454
+ };
455
+
456
+ const lines: string[] = ["# Wiki Index", ""];
457
+ for (const cat of PARA_CATEGORIES) {
458
+ lines.push(`## ${categoryLabels[cat]}`, "");
459
+ if (sections[cat].length > 0) {
460
+ lines.push(...sections[cat]);
461
+ } else {
462
+ const emptyMsg: Record<ParaCategory, string> = {
463
+ projects: "_No active projects yet._",
464
+ areas: "_No areas defined yet._",
465
+ resources: "_No resources yet._",
466
+ archives: "_No archived items._",
467
+ };
468
+ lines.push(emptyMsg[cat]);
469
+ }
470
+ lines.push("");
471
+ }
472
+
473
+ // Remove trailing empty line to avoid double newlines
474
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
475
+ lines.pop();
476
+ }
477
+ lines.push(""); // single trailing newline
478
+
479
+ await writeIndex(wikiDir, lines.join("\n"));
480
+ }
481
+
482
+ /**
483
+ * Extract the first non-empty paragraph from a markdown body.
484
+ * Skips headings and blank lines. Returns a single line.
485
+ */
486
+ function extractFirstParagraphFromBody(body: string): string {
487
+ const lines = body.split("\n");
488
+ const paragraphLines: string[] = [];
489
+ let inParagraph = false;
490
+
491
+ for (const line of lines) {
492
+ const trimmed = line.trim();
493
+
494
+ // Skip headings and empty lines before paragraph starts
495
+ if (!inParagraph) {
496
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
497
+ inParagraph = true;
498
+ }
499
+
500
+ // End paragraph on blank line or heading
501
+ if (inParagraph && (trimmed === "" || trimmed.startsWith("#"))) {
502
+ break;
503
+ }
504
+
505
+ paragraphLines.push(trimmed);
506
+ }
507
+
508
+ const result = paragraphLines.join(" ");
509
+ // Truncate long paragraphs
510
+ if (result.length > 200) {
511
+ return result.slice(0, 197) + "...";
512
+ }
513
+ return result || "(no summary)";
514
+ }
515
+
516
+ /**
517
+ * Auto-commit all wiki changes with a descriptive message.
518
+ *
519
+ * Silently no-ops if:
520
+ * - git is not installed
521
+ * - wiki dir is not a git repo
522
+ * - there are no changes to commit
523
+ */
524
+ export async function gitCommit(
525
+ wikiDir: string,
526
+ message: string,
527
+ ): Promise<boolean> {
528
+ try {
529
+ // Check if it's a git repo
530
+ const isRepo = await git(wikiDir, ["rev-parse", "--git-dir"]);
531
+ if (!isRepo) {
532
+ // Auto-init if not a repo yet
533
+ const initResult = await git(wikiDir, ["init"]);
534
+ if (!initResult) return false;
535
+ }
536
+
537
+ // Stage all changes
538
+ await git(wikiDir, ["add", "-A"]);
539
+
540
+ // Check if there's anything to commit
541
+ const status = await git(wikiDir, ["status", "--porcelain"]);
542
+ if (!status) return false; // nothing to commit
543
+
544
+ // Commit
545
+ const result = await git(wikiDir, ["commit", "-m", message, "--no-gpg-sign"]);
546
+ return result !== null;
547
+ } catch {
548
+ return false;
549
+ }
550
+ }