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/dist/commands.js DELETED
@@ -1,786 +0,0 @@
1
- /**
2
- * Slash command registrations for user interaction.
3
- *
4
- * Commands: /wiki, /wiki-ingest, /wiki-lint, /wiki-capture,
5
- * /wiki-scope, /wiki-search, /wiki-summarize,
6
- * /wiki-daemon, /wiki-settings
7
- */
8
- import { readFile, appendFile } from "node:fs/promises";
9
- import { join } from "node:path";
10
- import { lintWiki } from "./lint.js";
11
- import { listPages, readPage, writePage, movePage, writeIndex, PARA_CATEGORIES } from "./wiki.js";
12
- import { searchWiki } from "./store.js";
13
- import { CURRENT_SCHEMA_VERSION, migrateToLatest, parseFrontmatter, validateFrontmatter, } from "./frontmatter.js";
14
- // -- Helpers -----------------------------------------------------------------
15
- /** Convert raw text to a kebab-case slug. */
16
- function toKebabSlug(raw) {
17
- return raw
18
- .toLowerCase()
19
- .replace(/[^a-z0-9-]/g, "-")
20
- .replace(/-+/g, "-")
21
- .replace(/^-|-$/g, "");
22
- }
23
- /** Convert a slug to title case. */
24
- function slugToTitle(slug) {
25
- return slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
26
- }
27
- /** Rebuild index.md from all pages on disk. */
28
- async function rebuildIndexFromDisk(wikiDir) {
29
- const allPages = await listPages(wikiDir);
30
- const sections = {
31
- projects: [],
32
- areas: [],
33
- resources: [],
34
- archives: [],
35
- };
36
- for (const ref of allPages) {
37
- const page = await readPage(wikiDir, ref.category, ref.slug);
38
- const title = page?.frontmatter.title ?? ref.title;
39
- const summary = page?.body
40
- .split("\n")
41
- .find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("---"))
42
- ?.trim() ?? "";
43
- const desc = summary.length > 120 ? summary.slice(0, 117) + "..." : summary;
44
- sections[ref.category].push(`- [[${ref.slug}]] \u2014 ${title}${desc ? ": " + desc : ""}`);
45
- }
46
- const indexLines = [
47
- "# Wiki Index",
48
- "",
49
- "## Projects",
50
- "",
51
- sections.projects.length > 0
52
- ? sections.projects.join("\n")
53
- : "_No active projects yet._",
54
- "",
55
- "## Areas",
56
- "",
57
- sections.areas.length > 0
58
- ? sections.areas.join("\n")
59
- : "_No areas defined yet._",
60
- "",
61
- "## Resources",
62
- "",
63
- sections.resources.length > 0
64
- ? sections.resources.join("\n")
65
- : "_No resources yet._",
66
- "",
67
- "## Archives",
68
- "",
69
- sections.archives.length > 0
70
- ? sections.archives.join("\n")
71
- : "_No archived items._",
72
- ];
73
- await writeIndex(wikiDir, indexLines.join("\n"));
74
- }
75
- /** Parse log.md and return the last N entries as text lines. */
76
- async function readLastLogEntries(wikiDir, count) {
77
- let content;
78
- try {
79
- content = await readFile(join(wikiDir, "log.md"), "utf-8");
80
- }
81
- catch {
82
- return [];
83
- }
84
- // Find all entry start positions (each begins with "## [")
85
- const starts = [];
86
- const pattern = /^## \[/gm;
87
- let match;
88
- while ((match = pattern.exec(content)) !== null) {
89
- starts.push(match.index);
90
- }
91
- // Extract the last `count` entries
92
- const selected = starts.slice(-count);
93
- const entries = [];
94
- for (let i = 0; i < selected.length; i++) {
95
- const start = selected[i];
96
- const nextIdx = starts.indexOf(start) + 1;
97
- const end = nextIdx < starts.length ? starts[nextIdx] : content.length;
98
- entries.push(content.slice(start, end).trim());
99
- }
100
- return entries;
101
- }
102
- /** Format lint report for display via notify. */
103
- function formatLintNotify(report) {
104
- const { issues, fixed, stats } = report;
105
- const lines = [];
106
- lines.push(`Pages: ${stats.totalPages} (${PARA_CATEGORIES.map((c) => `${c}: ${stats.byCategory[c]}`).join(", ")})`);
107
- lines.push(`Links: ${stats.totalLinks} | Broken: ${stats.brokenLinks} | Orphans: ${stats.orphanPages}`);
108
- if (fixed.length > 0) {
109
- lines.push(`Auto-fixed: ${fixed.length} issue(s)`);
110
- for (const f of fixed.slice(0, 5)) {
111
- lines.push(` [fix] ${f.page ?? ""}: ${f.message}`);
112
- }
113
- if (fixed.length > 5) {
114
- lines.push(` ... and ${fixed.length - 5} more`);
115
- }
116
- }
117
- if (issues.length > 0) {
118
- lines.push(`Remaining: ${issues.length} issue(s)`);
119
- for (const issue of issues.slice(0, 10)) {
120
- lines.push(` [${issue.severity}] ${issue.page ?? issue.category}: ${issue.message}`);
121
- }
122
- if (issues.length > 10) {
123
- lines.push(` ... and ${issues.length - 10} more`);
124
- }
125
- }
126
- else {
127
- lines.push("Wiki is healthy.");
128
- }
129
- return lines.join("\n");
130
- }
131
- // -- Public API --------------------------------------------------------------
132
- /**
133
- * Register all slash commands with the pi extension API.
134
- */
135
- export function registerCommands(pi, wikiDir, store, getScope, setScope) {
136
- // ---- /wiki — status overview ---------------------------------------------
137
- pi.registerCommand("wiki", {
138
- description: "Show wiki status overview (scope, page counts, recent log entries)",
139
- handler: async (_args, ctx) => {
140
- const scope = getScope();
141
- // Page counts by category
142
- const allPages = await listPages(wikiDir);
143
- const byCat = {};
144
- for (const cat of PARA_CATEGORIES) {
145
- byCat[cat] = 0;
146
- }
147
- for (const page of allPages) {
148
- byCat[page.category] = (byCat[page.category] ?? 0) + 1;
149
- }
150
- // Last 5 log entries
151
- const logEntries = await readLastLogEntries(wikiDir, 5);
152
- const lines = [];
153
- lines.push(`Scope: ${scope.name} (${scope.source})`);
154
- lines.push(` include: ${scope.include.join(", ")}`);
155
- if (scope.exclude.length > 0) {
156
- lines.push(` exclude: ${scope.exclude.join(", ")}`);
157
- }
158
- lines.push("");
159
- lines.push(`Pages: ${allPages.length} total`);
160
- for (const cat of PARA_CATEGORIES) {
161
- lines.push(` ${cat}: ${byCat[cat]}`);
162
- }
163
- lines.push("");
164
- if (logEntries.length > 0) {
165
- lines.push("Recent activity:");
166
- for (const entry of logEntries) {
167
- // Compact each entry to one line
168
- const firstLine = entry.split("\n")[0];
169
- lines.push(` ${firstLine.replace(/^## /, "")}`);
170
- }
171
- }
172
- else {
173
- lines.push("No activity logged yet.");
174
- }
175
- ctx.ui.notify(lines.join("\n"), "info");
176
- },
177
- });
178
- // ---- /wiki-ingest — quick ingest shortcut --------------------------------
179
- pi.registerCommand("wiki-ingest", {
180
- description: "Ingest a URL or file into the wiki",
181
- handler: async (args, ctx) => {
182
- const source = args.trim();
183
- if (!source) {
184
- ctx.ui.notify("Usage: /wiki-ingest <url-or-file>", "error");
185
- return;
186
- }
187
- await ctx.waitForIdle();
188
- pi.sendUserMessage(`Ingest this into the wiki: ${source}`);
189
- },
190
- });
191
- // ---- /wiki-lint — run lint checks ----------------------------------------
192
- pi.registerCommand("wiki-lint", {
193
- description: "Run wiki health checks (--report-only to skip auto-fix)",
194
- getArgumentCompletions: (prefix) => {
195
- if ("--report-only".startsWith(prefix)) {
196
- return [{ value: "--report-only", label: "--report-only" }];
197
- }
198
- return null;
199
- },
200
- handler: async (args, ctx) => {
201
- const reportOnly = args.trim() === "--report-only";
202
- ctx.ui.notify("Running wiki lint...", "info");
203
- const report = await lintWiki(wikiDir, {
204
- autoFix: !reportOnly,
205
- });
206
- ctx.ui.notify(formatLintNotify(report), "info");
207
- },
208
- });
209
- // ---- /wiki-capture — capture via session LLM ----------------------------
210
- pi.registerCommand("wiki-capture", {
211
- description: "Capture knowledge from this session into the wiki (runs in background via daemon)",
212
- handler: async (args, ctx) => {
213
- const sessionFile = ctx.sessionManager.getSessionFile();
214
- if (!sessionFile) {
215
- ctx.ui.notify("No session file found", "warning");
216
- return;
217
- }
218
- // Register the session for daemon capture — runs in background, doesn't block user
219
- try {
220
- const registry = join(wikiDir, ".completed-sessions");
221
- const entry = `${new Date().toISOString()}|${sessionFile}\n`;
222
- await appendFile(registry, entry);
223
- ctx.ui.notify("Session queued for wiki capture (daemon will process in background).\n" +
224
- "Check progress: /wiki-daemon status\n" +
225
- "Tip: Set \"daemonModel\" in ~/.pi/wiki/config.json for better capture quality.", "info");
226
- ctx.ui.setStatus("pi-para", "wiki: capture queued");
227
- setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
228
- }
229
- catch (err) {
230
- ctx.ui.notify(`Failed to queue capture: ${err instanceof Error ? err.message : String(err)}`, "error");
231
- }
232
- },
233
- });
234
- // ---- /wiki-scope — show or override scope --------------------------------
235
- pi.registerCommand("wiki-scope", {
236
- description: "Show or override the current project scope",
237
- handler: async (args, ctx) => {
238
- const override = args.trim();
239
- if (!override) {
240
- // Show current scope
241
- const scope = getScope();
242
- const lines = [
243
- `Scope: ${scope.name}`,
244
- `Source: ${scope.source}`,
245
- `Include: ${scope.include.join(", ")}`,
246
- ];
247
- if (scope.exclude.length > 0) {
248
- lines.push(`Exclude: ${scope.exclude.join(", ")}`);
249
- }
250
- ctx.ui.notify(lines.join("\n"), "info");
251
- return;
252
- }
253
- // Override scope for this session
254
- const newScope = {
255
- name: override,
256
- include: [override],
257
- exclude: [],
258
- source: "config", // treated as explicit override
259
- };
260
- setScope(newScope);
261
- ctx.ui.notify(`Scope overridden to: ${override}`, "info");
262
- },
263
- });
264
- // ---- /wiki-search — quick search -----------------------------------------
265
- pi.registerCommand("wiki-search", {
266
- description: "Search the wiki knowledge base",
267
- handler: async (args, ctx) => {
268
- const query = args.trim();
269
- if (!query) {
270
- ctx.ui.notify("Usage: /wiki-search <query>", "error");
271
- return;
272
- }
273
- const scope = getScope();
274
- const results = await searchWiki(store, query, {
275
- scope,
276
- limit: 10,
277
- });
278
- if (results.length === 0) {
279
- ctx.ui.notify(`No results for "${query}"`, "info");
280
- return;
281
- }
282
- const lines = [`Search: "${query}" (${results.length} result(s))\n`];
283
- for (const r of results) {
284
- const fm = r.frontmatter;
285
- const scorePct = (r.score * 100).toFixed(0);
286
- lines.push(`[${scorePct}%] ${fm.title} (${r.page.path})`);
287
- lines.push(` ${fm.para} | ${fm.scope.join(", ")} | ${fm.tags.join(", ")}`);
288
- // Show first 120 chars of snippet
289
- const snippet = r.snippet.replace(/\n/g, " ").trim();
290
- if (snippet) {
291
- lines.push(` ${snippet.length > 120 ? snippet.slice(0, 117) + "..." : snippet}`);
292
- }
293
- lines.push("");
294
- }
295
- ctx.ui.notify(lines.join("\n"), "info");
296
- },
297
- });
298
- // ---- /wiki-summarize — summarize pages/categories/wiki -------------------
299
- pi.registerCommand("wiki-summarize", {
300
- description: "Summarize a page, category, or the entire wiki",
301
- getArgumentCompletions: (prefix) => {
302
- // Offer category names and "all" as completions
303
- const options = [...PARA_CATEGORIES, "all"];
304
- const filtered = options.filter((o) => o.startsWith(prefix));
305
- return filtered.length > 0
306
- ? filtered.map((o) => ({ value: o, label: o }))
307
- : null;
308
- },
309
- handler: async (args, ctx) => {
310
- const target = args.trim() || "all";
311
- await ctx.waitForIdle();
312
- pi.sendUserMessage(`Summarize the wiki: target="${target}". Use wiki_summarize tool with target="${target}".`);
313
- },
314
- });
315
- // ---- /wiki-daemon — daemon management ------------------------------------
316
- pi.registerCommand("wiki-daemon", {
317
- description: "Check daemon status and capture history",
318
- getArgumentCompletions: (prefix) => {
319
- const cmds = ["status", "history"];
320
- const filtered = cmds.filter((c) => c.startsWith(prefix));
321
- return filtered.length > 0 ? filtered.map((c) => ({ value: c, label: c })) : null;
322
- },
323
- handler: async (args, ctx) => {
324
- const subcmd = args.trim().split(/\s+/)[0] || "status";
325
- // Read state directly from the state DB — fast, no subprocess needed
326
- const { StateDB } = await import("./state.js");
327
- const state = new StateDB(wikiDir);
328
- try {
329
- switch (subcmd) {
330
- case "status": {
331
- const pid = state.getState("daemon_pid");
332
- const startedAt = state.getState("daemon_started_at");
333
- const history = state.getHistory(undefined, 5);
334
- const failed = state.getFailed();
335
- let running = false;
336
- if (pid) {
337
- try {
338
- process.kill(parseInt(pid), 0);
339
- running = true;
340
- }
341
- catch { }
342
- }
343
- const lines = [
344
- `Daemon: ${running ? `running (PID ${pid})` : "not running"}`,
345
- startedAt ? `Started: ${startedAt}` : "",
346
- `Failed: ${failed.length}`,
347
- "",
348
- "Recent captures:",
349
- ];
350
- if (history.length === 0) {
351
- lines.push(" (none)");
352
- }
353
- else {
354
- for (const h of history) {
355
- const status = h.error
356
- ? `ERROR: ${h.error.slice(0, 50)}`
357
- : h.pagesCreated.length > 0
358
- ? `${h.pagesCreated.length} page(s)`
359
- : "skipped";
360
- lines.push(` ${h.processedAt.slice(0, 19)} | ${h.scope} | ${status}`);
361
- }
362
- }
363
- ctx.ui.notify(lines.filter(Boolean).join("\n"), "info");
364
- break;
365
- }
366
- case "history": {
367
- const scope = getScope();
368
- const history = state.getHistory(scope.name, 15);
369
- if (history.length === 0) {
370
- ctx.ui.notify(`No capture history for scope: ${scope.name}`, "info");
371
- break;
372
- }
373
- const lines = [`Capture history (${scope.name}):\n`];
374
- for (const h of history) {
375
- const status = h.error
376
- ? `ERROR: ${h.error.slice(0, 60)}`
377
- : h.pagesCreated.length > 0
378
- ? `${h.pagesCreated.length} page(s): ${h.pagesCreated.join(", ")}`
379
- : "skipped";
380
- lines.push(`${h.processedAt.slice(0, 19)} | ${status}`);
381
- }
382
- ctx.ui.notify(lines.join("\n"), "info");
383
- break;
384
- }
385
- default:
386
- ctx.ui.notify(`Usage: /wiki-daemon [status|history]`, "error");
387
- }
388
- }
389
- finally {
390
- state.close();
391
- }
392
- },
393
- });
394
- // ---- /wiki-migrate — batch schema migration ------------------------------
395
- pi.registerCommand("wiki-migrate", {
396
- description: "Migrate all wiki pages to the current schema version",
397
- handler: async (_args, ctx) => {
398
- ctx.ui.notify(`Migrating wiki pages to schema version ${CURRENT_SCHEMA_VERSION}...`, "info");
399
- const allPages = await listPages(wikiDir);
400
- let migratedCount = 0;
401
- for (const ref of allPages) {
402
- const page = await readPage(wikiDir, ref.category, ref.slug);
403
- if (!page)
404
- continue;
405
- const version = page.frontmatter.schemaVersion ?? 1;
406
- if (version >= CURRENT_SCHEMA_VERSION)
407
- continue;
408
- const rawObj = page.frontmatter;
409
- const result = migrateToLatest(rawObj, page.body);
410
- const migratedFm = result.fm;
411
- migratedFm.updated = new Date().toISOString();
412
- await writePage(wikiDir, {
413
- category: ref.category,
414
- slug: ref.slug,
415
- frontmatter: migratedFm,
416
- body: result.body,
417
- });
418
- migratedCount++;
419
- }
420
- ctx.ui.notify(migratedCount > 0
421
- ? `Migrated ${migratedCount} page(s) to schema version ${CURRENT_SCHEMA_VERSION}.`
422
- : `All ${allPages.length} page(s) already at schema version ${CURRENT_SCHEMA_VERSION}.`, "info");
423
- },
424
- });
425
- // ---- /wiki-project — project lifecycle management ------------------------
426
- pi.registerCommand("wiki-project", {
427
- description: "Create a project page (/wiki-project <name> <goal>) or archive it (/wiki-project done <name>)",
428
- handler: async (args, ctx) => {
429
- const parts = args.trim().split(/\s+/);
430
- if (parts.length === 0 || !parts[0]) {
431
- ctx.ui.notify("Usage: /wiki-project <name> <goal>\n /wiki-project done <name>", "error");
432
- return;
433
- }
434
- if (parts[0] === "done") {
435
- // ---------- Archive a project ----------
436
- const rawName = parts.slice(1).join("-");
437
- if (!rawName) {
438
- ctx.ui.notify("Usage: /wiki-project done <name>", "error");
439
- return;
440
- }
441
- const slug = toKebabSlug(rawName);
442
- if (!slug) {
443
- ctx.ui.notify("Invalid project name — must contain at least one alphanumeric character.", "error");
444
- return;
445
- }
446
- const page = await readPage(wikiDir, "projects", slug);
447
- if (!page) {
448
- ctx.ui.notify(`Project not found: projects/${slug}`, "error");
449
- return;
450
- }
451
- const ref = {
452
- category: "projects",
453
- slug,
454
- title: page.frontmatter.title,
455
- path: `projects/${slug}.md`,
456
- };
457
- await movePage(wikiDir, ref, "archives");
458
- await rebuildIndexFromDisk(wikiDir);
459
- ctx.ui.notify(`Archived project: ${page.frontmatter.title} → archives/${slug}`, "info");
460
- return;
461
- }
462
- // ---------- Create a new project ----------
463
- const rawName = parts[0];
464
- const goal = parts.slice(1).join(" ");
465
- if (!goal) {
466
- ctx.ui.notify("Usage: /wiki-project <name> <goal>\n /wiki-project done <name>", "error");
467
- return;
468
- }
469
- const slug = toKebabSlug(rawName);
470
- if (!slug) {
471
- ctx.ui.notify("Invalid project name — must contain at least one alphanumeric character.", "error");
472
- return;
473
- }
474
- const title = slugToTitle(slug);
475
- const scope = getScope();
476
- const now = new Date().toISOString();
477
- const body = `# ${title}
478
-
479
- ## Goal
480
- ${goal}
481
-
482
- ## Status
483
- - [ ] Define scope and milestones
484
- - [ ] Implementation
485
- - [ ] Verification
486
-
487
- ## End Condition
488
- ${goal} — verified and complete.
489
-
490
- ## Connections
491
- (add related wiki pages here)`;
492
- const frontmatter = validateFrontmatter({
493
- title,
494
- para: "projects",
495
- scope: [scope.name],
496
- tags: [],
497
- sources: [],
498
- created: now,
499
- updated: now,
500
- links: [],
501
- });
502
- await writePage(wikiDir, {
503
- category: "projects",
504
- slug,
505
- frontmatter,
506
- body,
507
- });
508
- await rebuildIndexFromDisk(wikiDir);
509
- ctx.ui.notify(`Created project: projects/${slug} — "${title}"`, "info");
510
- },
511
- });
512
- // ---- /wiki-settings — interactive configuration --------------------------
513
- pi.registerCommand("wiki-settings", {
514
- description: "View and edit wiki configuration",
515
- handler: async (_args, ctx) => {
516
- if (!ctx.hasUI) {
517
- ctx.ui.notify("/wiki-settings requires interactive mode", "error");
518
- return;
519
- }
520
- const { writeFile: writeFileAsync } = await import("node:fs/promises");
521
- const { parse, stringify } = await import("yaml");
522
- const { homedir } = await import("node:os");
523
- const { execSync } = await import("node:child_process");
524
- // Load current config
525
- const configPath = join(wikiDir, "config.json");
526
- let config = {};
527
- try {
528
- config = JSON.parse(await readFile(configPath, "utf-8"));
529
- }
530
- catch { /* defaults */ }
531
- // Load qmd config
532
- const qmdPath = join(homedir(), ".config", "qmd", "index.yml");
533
- let qmdConfig = {};
534
- try {
535
- qmdConfig = parse(await readFile(qmdPath, "utf-8")) ?? {};
536
- }
537
- catch { /* no qmd config */ }
538
- const providers = (qmdConfig.providers ?? {});
539
- // Check statuses
540
- let qmdVersion = "";
541
- try {
542
- qmdVersion = execSync("qmd --version 2>/dev/null", { encoding: "utf-8" }).trim();
543
- }
544
- catch { }
545
- let daemonStatus = "not running";
546
- try {
547
- daemonStatus = execSync("systemctl --user is-active pi-para-daemon 2>/dev/null", { encoding: "utf-8" }).trim();
548
- }
549
- catch { }
550
- // Main menu loop
551
- while (true) {
552
- const scope = getScope();
553
- const pageCount = (await listPages(wikiDir).catch(() => [])).length;
554
- const choice = await ctx.ui.select("Wiki Settings", [
555
- `[Context] Max tokens: ${config.contextMaxTokens ?? 4000}`,
556
- `[Search] Limit: ${config.searchLimit ?? 10}, Graph boost: ${config.searchGraphBoost ?? true}`,
557
- `[Lint] Auto-fix: ${config.lintAutoFix ?? true}, Stale days: ${config.lintStaleDays ?? 90}`,
558
- `[Daemon] Model: ${config.daemonModel ?? "auto"}`,
559
- `[WebWiki] ${config.webWiki?.enabled ? `Enabled at http://${config.webWiki?.host ?? "0.0.0.0"}:${config.webWiki?.port ?? 10973}` : "Disabled"}`,
560
- `[Embed] ${providers.embed?.model ?? "not configured"} ${providers.embed?.url ? "at " + providers.embed.url : ""}`,
561
- `[Chat] ${providers.chat?.model ?? "not configured"} ${providers.chat?.url ? "at " + providers.chat.url : ""}`,
562
- `[Rerank] ${providers.rerank?.model ?? "not configured"} ${providers.rerank?.url ? "at " + providers.rerank.url : ""}`,
563
- `---`,
564
- `[Status] Scope: ${scope.name} | Pages: ${pageCount} | qmd: ${qmdVersion || "not installed"} | Daemon: ${daemonStatus}`,
565
- ]);
566
- if (!choice)
567
- break;
568
- if (choice.startsWith("[Context]")) {
569
- const val = await ctx.ui.input("Context max tokens:", String(config.contextMaxTokens ?? 4000));
570
- if (val) {
571
- config.contextMaxTokens = parseInt(val) || 4000;
572
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
573
- ctx.ui.notify(`Set contextMaxTokens = ${config.contextMaxTokens}`, "info");
574
- }
575
- }
576
- else if (choice.startsWith("[Search]")) {
577
- const sub = await ctx.ui.select("Search Settings", [
578
- `Limit: ${config.searchLimit ?? 10}`,
579
- `Graph boost: ${config.searchGraphBoost ?? true}`,
580
- ]);
581
- if (sub?.startsWith("Limit")) {
582
- const val = await ctx.ui.input("Search result limit:", String(config.searchLimit ?? 10));
583
- if (val) {
584
- config.searchLimit = parseInt(val) || 10;
585
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
586
- ctx.ui.notify(`Set searchLimit = ${config.searchLimit}`, "info");
587
- }
588
- }
589
- else if (sub?.startsWith("Graph boost")) {
590
- config.searchGraphBoost = !(config.searchGraphBoost ?? true);
591
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
592
- ctx.ui.notify(`Set searchGraphBoost = ${config.searchGraphBoost}`, "info");
593
- }
594
- }
595
- else if (choice.startsWith("[Lint]")) {
596
- const sub = await ctx.ui.select("Lint Settings", [
597
- `Auto-fix: ${config.lintAutoFix ?? true}`,
598
- `Stale days: ${config.lintStaleDays ?? 90}`,
599
- ]);
600
- if (sub?.startsWith("Auto-fix")) {
601
- config.lintAutoFix = !(config.lintAutoFix ?? true);
602
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
603
- ctx.ui.notify(`Set lintAutoFix = ${config.lintAutoFix}`, "info");
604
- }
605
- else if (sub?.startsWith("Stale")) {
606
- const val = await ctx.ui.input("Stale days threshold:", String(config.lintStaleDays ?? 90));
607
- if (val) {
608
- config.lintStaleDays = parseInt(val) || 90;
609
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
610
- ctx.ui.notify(`Set lintStaleDays = ${config.lintStaleDays}`, "info");
611
- }
612
- }
613
- }
614
- else if (choice.startsWith("[Daemon]")) {
615
- try {
616
- const { getProviders, getModels } = await import("@mariozechner/pi-ai");
617
- const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
618
- const authStorage = AuthStorage.create();
619
- // Find providers that have auth configured
620
- const allProviders = getProviders();
621
- const availableProviders = [];
622
- for (const p of allProviders) {
623
- if (authStorage.hasAuth(p))
624
- availableProviders.push(p);
625
- }
626
- if (availableProviders.length === 0) {
627
- ctx.ui.notify("No providers configured. Log in with pi first.", "warning");
628
- }
629
- else {
630
- // Let user pick provider
631
- const providerChoice = await ctx.ui.select("Select provider", ["auto (best available)", ...availableProviders]);
632
- if (providerChoice === "auto (best available)") {
633
- config.daemonModel = null;
634
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
635
- try {
636
- execSync("systemctl --user restart pi-para-daemon 2>/dev/null");
637
- }
638
- catch { }
639
- ctx.ui.notify("Set daemonModel = auto (daemon restarted)", "info");
640
- }
641
- else if (providerChoice) {
642
- // Show models for selected provider
643
- const models = getModels(providerChoice)
644
- .sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
645
- const modelOptions = models.map((m) => `${m.id} (${Math.round((m.contextWindow ?? 0) / 1000)}k ctx${m.reasoning ? ", reasoning" : ""})`);
646
- const modelChoice = await ctx.ui.select(`Select ${providerChoice} model`, modelOptions);
647
- if (modelChoice) {
648
- const modelId = modelChoice.split(" (")[0];
649
- config.daemonModel = `${providerChoice}/${modelId}`;
650
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
651
- try {
652
- execSync("systemctl --user restart pi-para-daemon 2>/dev/null");
653
- }
654
- catch { }
655
- ctx.ui.notify(`Set daemonModel = ${config.daemonModel} (daemon restarted)`, "info");
656
- }
657
- }
658
- }
659
- }
660
- catch (err) {
661
- // Fallback to manual input if AuthStorage not available
662
- const val = await ctx.ui.input("Daemon model (provider/model-id, or empty for auto):", config.daemonModel ? String(config.daemonModel) : "");
663
- config.daemonModel = val?.trim() || null;
664
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
665
- try {
666
- const { execSync: exec } = await import("node:child_process");
667
- exec("systemctl --user restart pi-para-daemon 2>/dev/null");
668
- }
669
- catch { }
670
- ctx.ui.notify(`Set daemonModel = ${config.daemonModel ?? "auto"} (daemon restarted)`, "info");
671
- }
672
- }
673
- else if (choice.startsWith("[WebWiki]")) {
674
- const webWiki = config.webWiki ?? { enabled: false, host: "0.0.0.0", port: 10973 };
675
- const sub = await ctx.ui.select("Web Wiki Settings", [
676
- `Enabled: ${webWiki.enabled}`,
677
- `Host: ${webWiki.host}`,
678
- `Port: ${webWiki.port}`,
679
- ]);
680
- if (sub?.startsWith("Enabled")) {
681
- webWiki.enabled = !webWiki.enabled;
682
- config.webWiki = webWiki;
683
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
684
- if (webWiki.enabled) {
685
- // Detect LAN IP
686
- const { networkInterfaces } = await import("node:os");
687
- const nets = networkInterfaces();
688
- let lanIp = webWiki.host;
689
- for (const ifaces of Object.values(nets)) {
690
- for (const iface of ifaces ?? []) {
691
- if (iface.family === "IPv4" && !iface.internal) {
692
- lanIp = iface.address;
693
- break;
694
- }
695
- }
696
- }
697
- ctx.ui.notify(`Web Wiki enabled at http://${lanIp}:${webWiki.port}\nRestart pi to apply.`, "info");
698
- }
699
- else {
700
- ctx.ui.notify("Web Wiki disabled. Restart pi to apply.", "info");
701
- }
702
- }
703
- else if (sub?.startsWith("Host")) {
704
- const val = await ctx.ui.input("Host (0.0.0.0 for LAN, 127.0.0.1 for local only):", webWiki.host);
705
- if (val) {
706
- webWiki.host = val;
707
- config.webWiki = webWiki;
708
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
709
- ctx.ui.notify(`Set webWiki.host = ${webWiki.host}`, "info");
710
- }
711
- }
712
- else if (sub?.startsWith("Port")) {
713
- const val = await ctx.ui.input("Port:", String(webWiki.port));
714
- if (val) {
715
- webWiki.port = parseInt(val) || 10973;
716
- config.webWiki = webWiki;
717
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
718
- ctx.ui.notify(`Set webWiki.port = ${webWiki.port}`, "info");
719
- }
720
- }
721
- }
722
- else if (choice.startsWith("[Embed]") || choice.startsWith("[Chat]") || choice.startsWith("[Rerank]")) {
723
- const providerType = choice.startsWith("[Embed]") ? "embed" : choice.startsWith("[Chat]") ? "chat" : "rerank";
724
- const current = providers[providerType] ?? {};
725
- const sub = await ctx.ui.select(`${providerType} provider`, [
726
- `URL: ${current.url ?? "not set"}`,
727
- `Key: ${current.key ? current.key.slice(0, 8) + "..." : "not set"}`,
728
- `Model: ${current.model ?? "not set"}`,
729
- ...(providerType === "embed" ? [`Dims: ${current.dims ?? "auto"}`] : []),
730
- ...(providerType === "chat" ? [`API format: ${current.api ?? "openai"}`] : []),
731
- `Remove this provider`,
732
- ]);
733
- if (sub?.startsWith("URL")) {
734
- const val = await ctx.ui.input(`${providerType} URL:`, current.url ?? "");
735
- if (val) {
736
- current.url = val;
737
- providers[providerType] = current;
738
- }
739
- }
740
- else if (sub?.startsWith("Key")) {
741
- const val = await ctx.ui.input(`${providerType} API key:`, "");
742
- if (val) {
743
- current.key = val;
744
- providers[providerType] = current;
745
- }
746
- }
747
- else if (sub?.startsWith("Model")) {
748
- const val = await ctx.ui.input(`${providerType} model:`, current.model ?? "");
749
- if (val) {
750
- current.model = val;
751
- providers[providerType] = current;
752
- }
753
- }
754
- else if (sub?.startsWith("Dims")) {
755
- const val = await ctx.ui.input("Embedding dimensions:", current.dims ?? "");
756
- if (val) {
757
- current.dims = val;
758
- providers[providerType] = current;
759
- }
760
- }
761
- else if (sub?.startsWith("API format")) {
762
- const fmt = await ctx.ui.select("API format", ["openai", "anthropic"]);
763
- if (fmt) {
764
- current.api = fmt;
765
- providers[providerType] = current;
766
- }
767
- }
768
- else if (sub?.startsWith("Remove")) {
769
- delete providers[providerType];
770
- }
771
- // Save qmd config
772
- qmdConfig.providers = providers;
773
- const { mkdirSync } = await import("node:fs");
774
- mkdirSync(join(homedir(), ".config", "qmd"), { recursive: true });
775
- await writeFileAsync(qmdPath, stringify(qmdConfig));
776
- ctx.ui.notify(`Updated ${qmdPath}`, "info");
777
- }
778
- else if (choice.startsWith("---") || choice.startsWith("[Status]")) {
779
- // Status row — just show, don't edit
780
- continue;
781
- }
782
- }
783
- },
784
- });
785
- }
786
- //# sourceMappingURL=commands.js.map