pi-para 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/ARCHITECTURE.md +160 -0
  2. package/CHANGELOG.md +51 -0
  3. package/CONFIGURATION.md +243 -0
  4. package/README.md +140 -237
  5. package/SETUP.md +196 -112
  6. package/STRUCTURE.md +140 -0
  7. package/package.json +86 -18
  8. package/scripts/build-assets.mjs +29 -0
  9. package/scripts/install.ps1 +55 -0
  10. package/scripts/install.sh +63 -0
  11. package/scripts/smoke-install.mjs +63 -0
  12. package/skills/para/SKILL.md +1 -1
  13. package/skills/setup/SKILL.md +1 -1
  14. package/src/atomic-write.ts +17 -0
  15. package/src/capture.ts +4 -4
  16. package/src/cli-legacy.ts +43 -0
  17. package/src/cli.ts +133 -28
  18. package/src/commands.ts +203 -243
  19. package/src/config.ts +355 -0
  20. package/src/context.ts +29 -14
  21. package/src/credentials.ts +100 -0
  22. package/src/daemon.ts +1 -1
  23. package/src/doctor.ts +209 -0
  24. package/src/gepa/index.ts +1 -1
  25. package/src/index.ts +119 -40
  26. package/src/maintainer.ts +4 -4
  27. package/src/model-resolver.ts +96 -0
  28. package/src/paths.ts +48 -0
  29. package/src/processor.ts +4 -4
  30. package/src/qmd-providers.ts +63 -0
  31. package/src/query.ts +15 -1
  32. package/src/repair.ts +49 -0
  33. package/src/scheduler/controls.ts +125 -0
  34. package/src/scheduler/index.ts +210 -0
  35. package/src/scheduler/leases.ts +98 -0
  36. package/src/scheduler/session-capture.ts +146 -0
  37. package/src/scheduler/state.ts +304 -0
  38. package/src/session-tools.ts +1 -1
  39. package/src/settings.ts +126 -0
  40. package/src/setup.ts +116 -0
  41. package/src/status.ts +115 -0
  42. package/src/store.ts +116 -31
  43. package/src/summarize.ts +72 -16
  44. package/src/templates/prompts.ts +1 -1
  45. package/src/tools.ts +280 -90
  46. package/src/wiki-tool-guidance.ts +84 -0
  47. package/src/wiki.ts +98 -26
  48. package/dist/capture.d.ts +0 -34
  49. package/dist/capture.d.ts.map +0 -1
  50. package/dist/capture.js +0 -461
  51. package/dist/capture.js.map +0 -1
  52. package/dist/cli.d.ts +0 -15
  53. package/dist/cli.d.ts.map +0 -1
  54. package/dist/cli.js +0 -405
  55. package/dist/cli.js.map +0 -1
  56. package/dist/commands.d.ts +0 -15
  57. package/dist/commands.d.ts.map +0 -1
  58. package/dist/commands.js +0 -786
  59. package/dist/commands.js.map +0 -1
  60. package/dist/context.d.ts +0 -61
  61. package/dist/context.d.ts.map +0 -1
  62. package/dist/context.js +0 -289
  63. package/dist/context.js.map +0 -1
  64. package/dist/daemon.d.ts +0 -49
  65. package/dist/daemon.d.ts.map +0 -1
  66. package/dist/daemon.js +0 -226
  67. package/dist/daemon.js.map +0 -1
  68. package/dist/frontmatter.d.ts +0 -59
  69. package/dist/frontmatter.d.ts.map +0 -1
  70. package/dist/frontmatter.js +0 -170
  71. package/dist/frontmatter.js.map +0 -1
  72. package/dist/gepa/dataset.d.ts +0 -36
  73. package/dist/gepa/dataset.d.ts.map +0 -1
  74. package/dist/gepa/dataset.js +0 -148
  75. package/dist/gepa/dataset.js.map +0 -1
  76. package/dist/gepa/evaluator.d.ts +0 -51
  77. package/dist/gepa/evaluator.d.ts.map +0 -1
  78. package/dist/gepa/evaluator.js +0 -317
  79. package/dist/gepa/evaluator.js.map +0 -1
  80. package/dist/gepa/index.d.ts +0 -66
  81. package/dist/gepa/index.d.ts.map +0 -1
  82. package/dist/gepa/index.js +0 -240
  83. package/dist/gepa/index.js.map +0 -1
  84. package/dist/gepa/llm.d.ts +0 -35
  85. package/dist/gepa/llm.d.ts.map +0 -1
  86. package/dist/gepa/llm.js +0 -268
  87. package/dist/gepa/llm.js.map +0 -1
  88. package/dist/gepa/optimized/capture-prompt.txt +0 -65
  89. package/dist/gepa/optimizer.d.ts +0 -29
  90. package/dist/gepa/optimizer.d.ts.map +0 -1
  91. package/dist/gepa/optimizer.js +0 -249
  92. package/dist/gepa/optimizer.js.map +0 -1
  93. package/dist/gepa/targets.d.ts +0 -39
  94. package/dist/gepa/targets.d.ts.map +0 -1
  95. package/dist/gepa/targets.js +0 -326
  96. package/dist/gepa/targets.js.map +0 -1
  97. package/dist/gepa/types.d.ts +0 -168
  98. package/dist/gepa/types.d.ts.map +0 -1
  99. package/dist/gepa/types.js +0 -124
  100. package/dist/gepa/types.js.map +0 -1
  101. package/dist/index.d.ts +0 -21
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -294
  104. package/dist/index.js.map +0 -1
  105. package/dist/ingest.d.ts +0 -88
  106. package/dist/ingest.d.ts.map +0 -1
  107. package/dist/ingest.js +0 -105
  108. package/dist/ingest.js.map +0 -1
  109. package/dist/link-discovery.d.ts +0 -40
  110. package/dist/link-discovery.d.ts.map +0 -1
  111. package/dist/link-discovery.js +0 -122
  112. package/dist/link-discovery.js.map +0 -1
  113. package/dist/link-utils.d.ts +0 -30
  114. package/dist/link-utils.d.ts.map +0 -1
  115. package/dist/link-utils.js +0 -145
  116. package/dist/link-utils.js.map +0 -1
  117. package/dist/lint.d.ts +0 -39
  118. package/dist/lint.d.ts.map +0 -1
  119. package/dist/lint.js +0 -827
  120. package/dist/lint.js.map +0 -1
  121. package/dist/maintainer.d.ts +0 -24
  122. package/dist/maintainer.d.ts.map +0 -1
  123. package/dist/maintainer.js +0 -358
  124. package/dist/maintainer.js.map +0 -1
  125. package/dist/processor.d.ts +0 -14
  126. package/dist/processor.d.ts.map +0 -1
  127. package/dist/processor.js +0 -361
  128. package/dist/processor.js.map +0 -1
  129. package/dist/query.d.ts +0 -69
  130. package/dist/query.d.ts.map +0 -1
  131. package/dist/query.js +0 -107
  132. package/dist/query.js.map +0 -1
  133. package/dist/raw.d.ts +0 -57
  134. package/dist/raw.d.ts.map +0 -1
  135. package/dist/raw.js +0 -219
  136. package/dist/raw.js.map +0 -1
  137. package/dist/redact.d.ts +0 -21
  138. package/dist/redact.d.ts.map +0 -1
  139. package/dist/redact.js +0 -143
  140. package/dist/redact.js.map +0 -1
  141. package/dist/scope.d.ts +0 -54
  142. package/dist/scope.d.ts.map +0 -1
  143. package/dist/scope.js +0 -161
  144. package/dist/scope.js.map +0 -1
  145. package/dist/scripts/bulk-link-fix.d.ts +0 -9
  146. package/dist/scripts/bulk-link-fix.d.ts.map +0 -1
  147. package/dist/scripts/bulk-link-fix.js +0 -106
  148. package/dist/scripts/bulk-link-fix.js.map +0 -1
  149. package/dist/session-tools.d.ts +0 -33
  150. package/dist/session-tools.d.ts.map +0 -1
  151. package/dist/session-tools.js +0 -211
  152. package/dist/session-tools.js.map +0 -1
  153. package/dist/state.d.ts +0 -54
  154. package/dist/state.d.ts.map +0 -1
  155. package/dist/state.js +0 -176
  156. package/dist/state.js.map +0 -1
  157. package/dist/store.d.ts +0 -56
  158. package/dist/store.d.ts.map +0 -1
  159. package/dist/store.js +0 -227
  160. package/dist/store.js.map +0 -1
  161. package/dist/summarize.d.ts +0 -56
  162. package/dist/summarize.d.ts.map +0 -1
  163. package/dist/summarize.js +0 -231
  164. package/dist/summarize.js.map +0 -1
  165. package/dist/tag-registry.d.ts +0 -56
  166. package/dist/tag-registry.d.ts.map +0 -1
  167. package/dist/tag-registry.js +0 -146
  168. package/dist/tag-registry.js.map +0 -1
  169. package/dist/templates/prompts.d.ts +0 -63
  170. package/dist/templates/prompts.d.ts.map +0 -1
  171. package/dist/templates/prompts.js +0 -260
  172. package/dist/templates/prompts.js.map +0 -1
  173. package/dist/tools.d.ts +0 -26
  174. package/dist/tools.d.ts.map +0 -1
  175. package/dist/tools.js +0 -1033
  176. package/dist/tools.js.map +0 -1
  177. package/dist/watcher.d.ts +0 -25
  178. package/dist/watcher.d.ts.map +0 -1
  179. package/dist/watcher.js +0 -84
  180. package/dist/watcher.js.map +0 -1
  181. package/dist/webui/client/dist/index.html +0 -1885
  182. package/dist/webui/server.d.ts +0 -31
  183. package/dist/webui/server.d.ts.map +0 -1
  184. package/dist/webui/server.js +0 -566
  185. package/dist/webui/server.js.map +0 -1
  186. package/dist/wiki.d.ts +0 -103
  187. package/dist/wiki.d.ts.map +0 -1
  188. package/dist/wiki.js +0 -428
  189. package/dist/wiki.js.map +0 -1
  190. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  191. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  192. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  193. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  194. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  195. package/scripts/gepa/uv.lock +0 -2241
package/src/commands.ts CHANGED
@@ -3,14 +3,14 @@
3
3
  *
4
4
  * Commands: /wiki, /wiki-ingest, /wiki-lint, /wiki-capture,
5
5
  * /wiki-scope, /wiki-search, /wiki-summarize,
6
- * /wiki-daemon, /wiki-settings
6
+ * /wiki-scheduler, /wiki-daemon compatibility alias, /wiki-settings
7
7
  */
8
8
 
9
- import { readFile, appendFile } from "node:fs/promises";
9
+ import { readFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
 
12
12
  import type { QMDStore } from "qmd-engine";
13
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
14
 
15
15
  import type { ProjectScope } from "./scope.js";
16
16
  import type { LintReport } from "./lint.js";
@@ -24,6 +24,7 @@ import {
24
24
  parseFrontmatter,
25
25
  validateFrontmatter,
26
26
  } from "./frontmatter.js";
27
+ import { appendCompletedSession } from "./scheduler/session-capture.js";
27
28
 
28
29
  // -- Helpers -----------------------------------------------------------------
29
30
 
@@ -261,7 +262,7 @@ export function registerCommands(
261
262
  // ---- /wiki-capture — capture via session LLM ----------------------------
262
263
 
263
264
  pi.registerCommand("wiki-capture", {
264
- description: "Capture knowledge from this session into the wiki (runs in background via daemon)",
265
+ description: "Queue this session for scheduler-backed wiki capture",
265
266
  handler: async (args, ctx) => {
266
267
  const sessionFile = ctx.sessionManager.getSessionFile();
267
268
  if (!sessionFile) {
@@ -269,16 +270,14 @@ export function registerCommands(
269
270
  return;
270
271
  }
271
272
 
272
- // Register the session for daemon capture — runs in background, doesn't block user
273
+ // Register the session for scheduler capture — runs in background, doesn't block user
273
274
  try {
274
- const registry = join(wikiDir, ".completed-sessions");
275
- const entry = `${new Date().toISOString()}|${sessionFile}\n`;
276
- await appendFile(registry, entry);
275
+ await appendCompletedSession(wikiDir, sessionFile);
277
276
 
278
277
  ctx.ui.notify(
279
- "Session queued for wiki capture (daemon will process in background).\n" +
280
- "Check progress: /wiki-daemon status\n" +
281
- "Tip: Set \"daemonModel\" in ~/.pi/wiki/config.json for better capture quality.",
278
+ "Session queued for wiki capture (scheduler will process when Pi is open).\n" +
279
+ "Check progress: /wiki-scheduler status\n" +
280
+ "Tip: Set the capture model in /wiki-settings for better capture quality.",
282
281
  "info",
283
282
  );
284
283
  ctx.ui.setStatus("pi-para", "wiki: capture queued");
@@ -386,86 +385,80 @@ export function registerCommands(
386
385
  },
387
386
  });
388
387
 
389
- // ---- /wiki-daemon — daemon management ------------------------------------
388
+ // ---- /wiki-scheduler — scheduler/capture queue status ----------------------
389
+
390
+ const registerSchedulerStatusCommand = (name: "wiki-scheduler" | "wiki-daemon", legacy: boolean) => {
391
+ pi.registerCommand(name, {
392
+ description: legacy
393
+ ? "Compatibility alias for /wiki-scheduler"
394
+ : "Check scheduler queue, task history, and capture status",
395
+ getArgumentCompletions: (prefix) => {
396
+ const cmds = ["status", "queue", "history", "capture-history"];
397
+ const filtered = cmds.filter((c) => c.startsWith(prefix));
398
+ return filtered.length > 0 ? filtered.map((c) => ({ value: c, label: c })) : null;
399
+ },
400
+ handler: async (args, ctx) => {
401
+ const subcmd = args.trim().split(/\s+/)[0] || "status";
402
+ const { listSchedulerTasks, listSchedulerHistory, formatQueueItems, formatSchedulerHistory } = await import("./scheduler/controls.js");
403
+ const { StateDB } = await import("./state.js");
390
404
 
391
- pi.registerCommand("wiki-daemon", {
392
- description: "Check daemon status and capture history",
393
- getArgumentCompletions: (prefix) => {
394
- const cmds = ["status", "history"];
395
- const filtered = cmds.filter((c) => c.startsWith(prefix));
396
- return filtered.length > 0 ? filtered.map((c) => ({ value: c, label: c })) : null;
397
- },
398
- handler: async (args, ctx) => {
399
- const subcmd = args.trim().split(/\s+/)[0] || "status";
400
-
401
- // Read state directly from the state DB — fast, no subprocess needed
402
- const { StateDB } = await import("./state.js");
403
- const state = new StateDB(wikiDir);
404
-
405
- try {
406
405
  switch (subcmd) {
407
406
  case "status": {
408
- const pid = state.getState("daemon_pid");
409
- const startedAt = state.getState("daemon_started_at");
410
- const history = state.getHistory(undefined, 5);
411
- const failed = state.getFailed();
412
-
413
- let running = false;
414
- if (pid) {
415
- try { process.kill(parseInt(pid), 0); running = true; } catch {}
416
- }
417
-
407
+ const queued = listSchedulerTasks(wikiDir, { status: "queued" }).length;
408
+ const running = listSchedulerTasks(wikiDir, { status: "running" }).length;
409
+ const failed = listSchedulerTasks(wikiDir, { status: "failed" }).length;
410
+ const recent = listSchedulerHistory(wikiDir, { limit: 5 });
418
411
  const lines = [
419
- `Daemon: ${running ? `running (PID ${pid})` : "not running"}`,
420
- startedAt ? `Started: ${startedAt}` : "",
421
- `Failed: ${failed.length}`,
412
+ legacy ? "Scheduler: active (via /wiki-daemon compatibility alias)" : "Scheduler: active",
413
+ `Queue: ${queued} queued, ${running} running, ${failed} failed`,
422
414
  "",
423
- "Recent captures:",
415
+ "Recent scheduler history:",
416
+ formatSchedulerHistory(recent).split("\n").map((line) => ` ${line}`).join("\n"),
424
417
  ];
425
- if (history.length === 0) {
426
- lines.push(" (none)");
427
- } else {
428
- for (const h of history) {
429
- const status = h.error
430
- ? `ERROR: ${h.error.slice(0, 50)}`
431
- : h.pagesCreated.length > 0
432
- ? `${h.pagesCreated.length} page(s)`
433
- : "skipped";
434
- lines.push(` ${h.processedAt.slice(0, 19)} | ${h.scope} | ${status}`);
435
- }
436
- }
437
418
  ctx.ui.notify(lines.filter(Boolean).join("\n"), "info");
438
419
  break;
439
420
  }
421
+ case "queue": {
422
+ ctx.ui.notify(formatQueueItems(listSchedulerTasks(wikiDir)), "info");
423
+ break;
424
+ }
440
425
  case "history": {
426
+ ctx.ui.notify(formatSchedulerHistory(listSchedulerHistory(wikiDir, { limit: 15 })), "info");
427
+ break;
428
+ }
429
+ case "capture-history": {
441
430
  const scope = getScope();
442
- const history = state.getHistory(scope.name, 15);
443
-
444
- if (history.length === 0) {
445
- ctx.ui.notify(`No capture history for scope: ${scope.name}`, "info");
446
- break;
447
- }
448
-
449
- const lines = [`Capture history (${scope.name}):\n`];
450
- for (const h of history) {
451
- const status = h.error
452
- ? `ERROR: ${h.error.slice(0, 60)}`
453
- : h.pagesCreated.length > 0
454
- ? `${h.pagesCreated.length} page(s): ${h.pagesCreated.join(", ")}`
455
- : "skipped";
456
- lines.push(`${h.processedAt.slice(0, 19)} | ${status}`);
431
+ const state = new StateDB(wikiDir);
432
+ try {
433
+ const history = state.getHistory(scope.name, 15);
434
+ if (history.length === 0) {
435
+ ctx.ui.notify(`No capture history for scope: ${scope.name}`, "info");
436
+ break;
437
+ }
438
+ const lines = [`Capture history (${scope.name}):\n`];
439
+ for (const h of history) {
440
+ const status = h.error
441
+ ? `ERROR: ${h.error.slice(0, 60)}`
442
+ : h.pagesCreated.length > 0
443
+ ? `${h.pagesCreated.length} page(s): ${h.pagesCreated.join(", ")}`
444
+ : "skipped";
445
+ lines.push(`${h.processedAt.slice(0, 19)} | ${status}`);
446
+ }
447
+ ctx.ui.notify(lines.join("\n"), "info");
448
+ } finally {
449
+ state.close();
457
450
  }
458
- ctx.ui.notify(lines.join("\n"), "info");
459
451
  break;
460
452
  }
461
453
  default:
462
- ctx.ui.notify(`Usage: /wiki-daemon [status|history]`, "error");
454
+ ctx.ui.notify(`Usage: /${name} [status|queue|history|capture-history]`, "error");
463
455
  }
464
- } finally {
465
- state.close();
466
- }
467
- },
468
- });
456
+ },
457
+ });
458
+ };
459
+
460
+ registerSchedulerStatusCommand("wiki-scheduler", false);
461
+ registerSchedulerStatusCommand("wiki-daemon", true);
469
462
 
470
463
  // ---- /wiki-migrate — batch schema migration ------------------------------
471
464
 
@@ -640,239 +633,206 @@ ${goal} — verified and complete.
640
633
  return;
641
634
  }
642
635
 
643
- const { writeFile: writeFileAsync } = await import("node:fs/promises");
644
- const { parse, stringify } = await import("yaml");
645
- const { homedir } = await import("node:os");
646
- const { execSync } = await import("node:child_process");
647
-
648
- // Load current config
649
- const configPath = join(wikiDir, "config.json");
650
- let config: Record<string, unknown> = {};
651
- try {
652
- config = JSON.parse(await readFile(configPath, "utf-8"));
653
- } catch { /* defaults */ }
654
-
655
- // Load qmd config
656
- const qmdPath = join(homedir(), ".config", "qmd", "index.yml");
657
- let qmdConfig: Record<string, unknown> = {};
658
- try {
659
- qmdConfig = parse(await readFile(qmdPath, "utf-8")) ?? {};
660
- } catch { /* no qmd config */ }
661
- const providers = (qmdConfig.providers ?? {}) as Record<string, Record<string, string>>;
662
-
663
- // Check statuses
664
- let qmdVersion = "";
665
- try { qmdVersion = execSync("qmd --version 2>/dev/null", { encoding: "utf-8" }).trim(); } catch {}
666
- let daemonStatus = "not running";
667
- try { daemonStatus = execSync("systemctl --user is-active pi-para-daemon 2>/dev/null", { encoding: "utf-8" }).trim(); } catch {}
636
+ const { loadParaConfig, saveParaConfig } = await import("./config.js");
637
+ const {
638
+ modelSelectionLabel,
639
+ providerProfileLabel,
640
+ setContextMaxTokens,
641
+ setSearchLimit,
642
+ setLintStaleDays,
643
+ toggleSearchGraphBoost,
644
+ toggleLintAutoFix,
645
+ setCaptureModelAuto,
646
+ setCaptureModel,
647
+ setWebWikiEnabled,
648
+ setWebWikiHost,
649
+ setWebWikiPort,
650
+ ensureEmbeddingProfile,
651
+ ensureRerankProfile,
652
+ disableRerank,
653
+ setProviderProfileField,
654
+ setProviderDims,
655
+ } = await import("./settings.js");
656
+ const { listSchedulerTasks } = await import("./scheduler/controls.js");
657
+ const { setSecret } = await import("./credentials.js");
658
+
659
+ const loaded = await loadParaConfig({ migrate: true });
660
+ const config = loaded.config;
661
+ const persist = async () => saveParaConfig(config, { homeDir: loaded.paths.homeDir });
668
662
 
669
- // Main menu loop
670
663
  while (true) {
671
664
  const scope = getScope();
672
665
  const pageCount = (await listPages(wikiDir).catch(() => [])).length;
666
+ const queuedTasks = listSchedulerTasks(wikiDir, { status: "queued" }).length;
667
+ const failedTasks = listSchedulerTasks(wikiDir, { status: "failed" }).length;
673
668
 
674
669
  const choice = await ctx.ui.select("Wiki Settings", [
675
- `[Context] Max tokens: ${config.contextMaxTokens ?? 4000}`,
676
- `[Search] Limit: ${config.searchLimit ?? 10}, Graph boost: ${config.searchGraphBoost ?? true}`,
677
- `[Lint] Auto-fix: ${config.lintAutoFix ?? true}, Stale days: ${config.lintStaleDays ?? 90}`,
678
- `[Daemon] Model: ${config.daemonModel ?? "auto"}`,
679
- `[WebWiki] ${(config as any).webWiki?.enabled ? `Enabled at http://${(config as any).webWiki?.host ?? "0.0.0.0"}:${(config as any).webWiki?.port ?? 10973}` : "Disabled"}`,
680
- `[Embed] ${providers.embed?.model ?? "not configured"} ${providers.embed?.url ? "at " + providers.embed.url : ""}`,
681
- `[Chat] ${providers.chat?.model ?? "not configured"} ${providers.chat?.url ? "at " + providers.chat.url : ""}`,
682
- `[Rerank] ${providers.rerank?.model ?? "not configured"} ${providers.rerank?.url ? "at " + providers.rerank.url : ""}`,
670
+ `[Context] Max tokens: ${config.context.maxTokens}`,
671
+ `[Search] Limit: ${config.context.searchLimit}, Graph boost: ${config.context.searchGraphBoost}`,
672
+ `[Lint] Auto-fix: ${config.lint.autoFix}, Stale days: ${config.lint.staleDays}`,
673
+ `[Capture] Model: ${modelSelectionLabel(config.models.capture)}`,
674
+ `[WebWiki] ${config.webWiki.enabled ? `Enabled at http://${config.webWiki.host}:${config.webWiki.port}` : "Disabled"}`,
675
+ `[Embedding] ${providerProfileLabel(config.qmd.embedding)}`,
676
+ `[Rerank] ${providerProfileLabel(config.qmd.rerank)}`,
683
677
  `---`,
684
- `[Status] Scope: ${scope.name} | Pages: ${pageCount} | qmd: ${qmdVersion || "not installed"} | Daemon: ${daemonStatus}`,
678
+ `[Status] Scope: ${scope.name} | Pages: ${pageCount} | qmd: SDK | Scheduler: ${queuedTasks} queued, ${failedTasks} failed`,
685
679
  ]);
686
680
 
687
681
  if (!choice) break;
688
682
 
689
683
  if (choice.startsWith("[Context]")) {
690
- const val = await ctx.ui.input("Context max tokens:", String(config.contextMaxTokens ?? 4000));
684
+ const val = await ctx.ui.input("Context max tokens:", String(config.context.maxTokens));
691
685
  if (val) {
692
- config.contextMaxTokens = parseInt(val) || 4000;
693
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
694
- ctx.ui.notify(`Set contextMaxTokens = ${config.contextMaxTokens}`, "info");
686
+ setContextMaxTokens(config, val);
687
+ await persist();
688
+ ctx.ui.notify(`Set context.maxTokens = ${config.context.maxTokens}`, "info");
695
689
  }
696
690
  } else if (choice.startsWith("[Search]")) {
697
691
  const sub = await ctx.ui.select("Search Settings", [
698
- `Limit: ${config.searchLimit ?? 10}`,
699
- `Graph boost: ${config.searchGraphBoost ?? true}`,
692
+ `Limit: ${config.context.searchLimit}`,
693
+ `Graph boost: ${config.context.searchGraphBoost}`,
700
694
  ]);
701
695
  if (sub?.startsWith("Limit")) {
702
- const val = await ctx.ui.input("Search result limit:", String(config.searchLimit ?? 10));
696
+ const val = await ctx.ui.input("Search result limit:", String(config.context.searchLimit));
703
697
  if (val) {
704
- config.searchLimit = parseInt(val) || 10;
705
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
706
- ctx.ui.notify(`Set searchLimit = ${config.searchLimit}`, "info");
698
+ setSearchLimit(config, val);
699
+ await persist();
700
+ ctx.ui.notify(`Set context.searchLimit = ${config.context.searchLimit}`, "info");
707
701
  }
708
702
  } else if (sub?.startsWith("Graph boost")) {
709
- config.searchGraphBoost = !(config.searchGraphBoost ?? true);
710
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
711
- ctx.ui.notify(`Set searchGraphBoost = ${config.searchGraphBoost}`, "info");
703
+ toggleSearchGraphBoost(config);
704
+ await persist();
705
+ ctx.ui.notify(`Set context.searchGraphBoost = ${config.context.searchGraphBoost}`, "info");
712
706
  }
713
707
  } else if (choice.startsWith("[Lint]")) {
714
708
  const sub = await ctx.ui.select("Lint Settings", [
715
- `Auto-fix: ${config.lintAutoFix ?? true}`,
716
- `Stale days: ${config.lintStaleDays ?? 90}`,
709
+ `Auto-fix: ${config.lint.autoFix}`,
710
+ `Stale days: ${config.lint.staleDays}`,
717
711
  ]);
718
712
  if (sub?.startsWith("Auto-fix")) {
719
- config.lintAutoFix = !(config.lintAutoFix ?? true);
720
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
721
- ctx.ui.notify(`Set lintAutoFix = ${config.lintAutoFix}`, "info");
713
+ toggleLintAutoFix(config);
714
+ await persist();
715
+ ctx.ui.notify(`Set lint.autoFix = ${config.lint.autoFix}`, "info");
722
716
  } else if (sub?.startsWith("Stale")) {
723
- const val = await ctx.ui.input("Stale days threshold:", String(config.lintStaleDays ?? 90));
717
+ const val = await ctx.ui.input("Stale days threshold:", String(config.lint.staleDays));
724
718
  if (val) {
725
- config.lintStaleDays = parseInt(val) || 90;
726
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
727
- ctx.ui.notify(`Set lintStaleDays = ${config.lintStaleDays}`, "info");
719
+ setLintStaleDays(config, val);
720
+ await persist();
721
+ ctx.ui.notify(`Set lint.staleDays = ${config.lint.staleDays}`, "info");
728
722
  }
729
723
  }
730
- } else if (choice.startsWith("[Daemon]")) {
724
+ } else if (choice.startsWith("[Capture]")) {
731
725
  try {
732
- const { getProviders, getModels } = await import("@mariozechner/pi-ai");
733
- const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
726
+ const { getProviders, getModels } = await import("@earendil-works/pi-ai/compat");
727
+ const { AuthStorage } = await import("@earendil-works/pi-coding-agent");
734
728
  const authStorage = AuthStorage.create();
735
-
736
- // Find providers that have auth configured
737
- const allProviders = getProviders();
738
- const availableProviders: string[] = [];
739
- for (const p of allProviders) {
740
- if (authStorage.hasAuth(p)) availableProviders.push(p);
741
- }
729
+ const availableProviders = getProviders().filter((p: string) => authStorage.hasAuth(p));
742
730
 
743
731
  if (availableProviders.length === 0) {
744
- ctx.ui.notify("No providers configured. Log in with pi first.", "warning");
732
+ ctx.ui.notify("No Pi auth providers configured. Use /login first or configure a pi-para secret for custom providers.", "warning");
745
733
  } else {
746
- // Let user pick provider
747
- const providerChoice = await ctx.ui.select(
748
- "Select provider",
749
- ["auto (best available)", ...availableProviders],
750
- );
751
-
734
+ const providerChoice = await ctx.ui.select("Select capture provider", ["auto (best available)", ...availableProviders]);
752
735
  if (providerChoice === "auto (best available)") {
753
- config.daemonModel = null;
754
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
755
- try { execSync("systemctl --user restart pi-para-daemon 2>/dev/null"); } catch {}
756
- ctx.ui.notify("Set daemonModel = auto (daemon restarted)", "info");
736
+ setCaptureModelAuto(config);
737
+ await persist();
738
+ ctx.ui.notify("Set capture model = auto. Restart open Pi sessions to apply.", "info");
757
739
  } else if (providerChoice) {
758
- // Show models for selected provider
759
740
  const models = getModels(providerChoice as any)
760
741
  .sort((a: any, b: any) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
761
- const modelOptions = models.map((m: any) =>
762
- `${m.id} (${Math.round((m.contextWindow ?? 0) / 1000)}k ctx${m.reasoning ? ", reasoning" : ""})`
763
- );
764
742
  const modelChoice = await ctx.ui.select(
765
743
  `Select ${providerChoice} model`,
766
- modelOptions,
744
+ models.map((m: any) => `${m.id} (${Math.round((m.contextWindow ?? 0) / 1000)}k ctx${m.reasoning ? ", reasoning" : ""})`),
767
745
  );
768
746
  if (modelChoice) {
769
- const modelId = modelChoice.split(" (")[0];
770
- config.daemonModel = `${providerChoice}/${modelId}`;
771
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
772
- try { execSync("systemctl --user restart pi-para-daemon 2>/dev/null"); } catch {}
773
- ctx.ui.notify(`Set daemonModel = ${config.daemonModel} (daemon restarted)`, "info");
747
+ setCaptureModel(config, providerChoice, modelChoice.split(" (")[0]);
748
+ await persist();
749
+ ctx.ui.notify(`Set capture model = ${modelSelectionLabel(config.models.capture)}. Restart open Pi sessions to apply.`, "info");
774
750
  }
775
751
  }
776
752
  }
777
- } catch (err) {
778
- // Fallback to manual input if AuthStorage not available
779
- const val = await ctx.ui.input(
780
- "Daemon model (provider/model-id, or empty for auto):",
781
- config.daemonModel ? String(config.daemonModel) : "",
782
- );
783
- config.daemonModel = val?.trim() || null;
784
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
785
- try { const { execSync: exec } = await import("node:child_process"); exec("systemctl --user restart pi-para-daemon 2>/dev/null"); } catch {}
786
- ctx.ui.notify(`Set daemonModel = ${config.daemonModel ?? "auto"} (daemon restarted)`, "info");
753
+ } catch {
754
+ const val = await ctx.ui.input("Capture model (provider/model-id, empty for auto):", modelSelectionLabel(config.models.capture));
755
+ if (!val?.trim()) setCaptureModelAuto(config);
756
+ else {
757
+ const slash = val.indexOf("/");
758
+ if (slash <= 0) {
759
+ ctx.ui.notify("Use provider/model-id format, or leave empty for auto.", "error");
760
+ continue;
761
+ }
762
+ setCaptureModel(config, val.slice(0, slash), val.slice(slash + 1));
763
+ }
764
+ await persist();
765
+ ctx.ui.notify(`Set capture model = ${modelSelectionLabel(config.models.capture)}. Restart open Pi sessions to apply.`, "info");
787
766
  }
788
767
  } else if (choice.startsWith("[WebWiki]")) {
789
- const webWiki = (config as any).webWiki ?? { enabled: false, host: "0.0.0.0", port: 10973 };
790
768
  const sub = await ctx.ui.select("Web Wiki Settings", [
791
- `Enabled: ${webWiki.enabled}`,
792
- `Host: ${webWiki.host}`,
793
- `Port: ${webWiki.port}`,
769
+ `Enabled: ${config.webWiki.enabled}`,
770
+ `Host: ${config.webWiki.host}`,
771
+ `Port: ${config.webWiki.port}`,
794
772
  ]);
795
773
  if (sub?.startsWith("Enabled")) {
796
- webWiki.enabled = !webWiki.enabled;
797
- (config as any).webWiki = webWiki;
798
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
799
- if (webWiki.enabled) {
800
- // Detect LAN IP
801
- const { networkInterfaces } = await import("node:os");
802
- const nets = networkInterfaces();
803
- let lanIp = webWiki.host;
804
- for (const ifaces of Object.values(nets)) {
805
- for (const iface of ifaces ?? []) {
806
- if (iface.family === "IPv4" && !iface.internal) {
807
- lanIp = iface.address;
808
- break;
809
- }
810
- }
811
- }
812
- ctx.ui.notify(`Web Wiki enabled at http://${lanIp}:${webWiki.port}\nRestart pi to apply.`, "info");
813
- } else {
814
- ctx.ui.notify("Web Wiki disabled. Restart pi to apply.", "info");
815
- }
774
+ setWebWikiEnabled(config, !config.webWiki.enabled);
775
+ await persist();
776
+ ctx.ui.notify(`Web Wiki ${config.webWiki.enabled ? "enabled" : "disabled"}. Restart open Pi sessions to apply.`, "info");
816
777
  } else if (sub?.startsWith("Host")) {
817
- const val = await ctx.ui.input("Host (0.0.0.0 for LAN, 127.0.0.1 for local only):", webWiki.host);
818
- if (val) {
819
- webWiki.host = val;
820
- (config as any).webWiki = webWiki;
821
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
822
- ctx.ui.notify(`Set webWiki.host = ${webWiki.host}`, "info");
823
- }
778
+ const val = await ctx.ui.input("Host:", config.webWiki.host);
779
+ if (val) { setWebWikiHost(config, val); await persist(); }
824
780
  } else if (sub?.startsWith("Port")) {
825
- const val = await ctx.ui.input("Port:", String(webWiki.port));
826
- if (val) {
827
- webWiki.port = parseInt(val) || 10973;
828
- (config as any).webWiki = webWiki;
829
- await writeFileAsync(configPath, JSON.stringify(config, null, 2));
830
- ctx.ui.notify(`Set webWiki.port = ${webWiki.port}`, "info");
831
- }
781
+ const val = await ctx.ui.input("Port:", String(config.webWiki.port));
782
+ if (val) { setWebWikiPort(config, val); await persist(); }
832
783
  }
833
- } else if (choice.startsWith("[Embed]") || choice.startsWith("[Chat]") || choice.startsWith("[Rerank]")) {
834
- const providerType = choice.startsWith("[Embed]") ? "embed" : choice.startsWith("[Chat]") ? "chat" : "rerank";
835
- const current = providers[providerType] ?? {};
836
-
837
- const sub = await ctx.ui.select(`${providerType} provider`, [
838
- `URL: ${current.url ?? "not set"}`,
839
- `Key: ${current.key ? current.key.slice(0, 8) + "..." : "not set"}`,
840
- `Model: ${current.model ?? "not set"}`,
841
- ...(providerType === "embed" ? [`Dims: ${current.dims ?? "auto"}`] : []),
842
- ...(providerType === "chat" ? [`API format: ${current.api ?? "openai"}`] : []),
843
- `Remove this provider`,
784
+ } else if (choice.startsWith("[Embedding]") || choice.startsWith("[Rerank]")) {
785
+ const isEmbedding = choice.startsWith("[Embedding]");
786
+ const profile = isEmbedding ? ensureEmbeddingProfile(config) : ensureRerankProfile(config);
787
+ const sub = await ctx.ui.select(isEmbedding ? "Embedding provider" : "Rerank provider", [
788
+ `Provider: ${profile.provider}`,
789
+ `Base URL: ${profile.baseUrl ?? "not set"}`,
790
+ `Model: ${profile.model ?? "not set"}`,
791
+ ...(isEmbedding ? [`Dims: ${profile.dims ?? "auto"}`] : []),
792
+ `API format: ${profile.apiFormat ?? "openai"}`,
793
+ `Credential ref: ${profile.credentialRef}`,
794
+ `Store/update local secret`,
795
+ ...(!isEmbedding ? [`Disable rerank`] : []),
844
796
  ]);
845
797
 
846
- if (sub?.startsWith("URL")) {
847
- const val = await ctx.ui.input(`${providerType} URL:`, current.url ?? "");
848
- if (val) { current.url = val; providers[providerType] = current; }
849
- } else if (sub?.startsWith("Key")) {
850
- const val = await ctx.ui.input(`${providerType} API key:`, "");
851
- if (val) { current.key = val; providers[providerType] = current; }
798
+ if (sub?.startsWith("Provider")) {
799
+ const val = await ctx.ui.input("Provider id:", profile.provider);
800
+ if (val) setProviderProfileField(profile, "provider", val);
801
+ } else if (sub?.startsWith("Base URL")) {
802
+ const val = await ctx.ui.input("Base URL:", profile.baseUrl ?? "");
803
+ if (val) setProviderProfileField(profile, "baseUrl", val);
852
804
  } else if (sub?.startsWith("Model")) {
853
- const val = await ctx.ui.input(`${providerType} model:`, current.model ?? "");
854
- if (val) { current.model = val; providers[providerType] = current; }
805
+ const val = await ctx.ui.input("Model id:", profile.model ?? "");
806
+ if (val) setProviderProfileField(profile, "model", val);
855
807
  } else if (sub?.startsWith("Dims")) {
856
- const val = await ctx.ui.input("Embedding dimensions:", current.dims ?? "");
857
- if (val) { current.dims = val; providers[providerType] = current; }
808
+ const val = await ctx.ui.input("Embedding dimensions:", profile.dims ? String(profile.dims) : "");
809
+ if (val) setProviderDims(profile, val);
858
810
  } else if (sub?.startsWith("API format")) {
859
- const fmt = await ctx.ui.select("API format", ["openai", "anthropic"]);
860
- if (fmt) { current.api = fmt; providers[providerType] = current; }
861
- } else if (sub?.startsWith("Remove")) {
862
- delete providers[providerType];
811
+ const fmt = await ctx.ui.select("API format", ["openai", "anthropic", "custom"]);
812
+ if (fmt) setProviderProfileField(profile, "apiFormat", fmt);
813
+ } else if (sub?.startsWith("Credential ref")) {
814
+ const val = await ctx.ui.input("Credential ref (pi-auth:<provider>, secret:<name>, or none):", profile.credentialRef);
815
+ if (val) setProviderProfileField(profile, "credentialRef", val);
816
+ } else if (sub?.startsWith("Store/update local secret")) {
817
+ const name = await ctx.ui.input("Secret name:", profile.credentialRef.startsWith("secret:") ? profile.credentialRef.slice("secret:".length) : "");
818
+ if (name) {
819
+ const value = await ctx.ui.input("API key (stored in ~/.pi/para/secrets.json, not wiki git):", "");
820
+ if (value) {
821
+ await setSecret(name, value, loaded.paths.secretsPath);
822
+ setProviderProfileField(profile, "credentialRef", `secret:${name}`);
823
+ }
824
+ }
825
+ } else if (sub?.startsWith("Disable rerank")) {
826
+ disableRerank(config);
863
827
  }
864
828
 
865
- // Save qmd config
866
- qmdConfig.providers = providers;
867
- const { mkdirSync } = await import("node:fs");
868
- mkdirSync(join(homedir(), ".config", "qmd"), { recursive: true });
869
- await writeFileAsync(qmdPath, stringify(qmdConfig));
870
- ctx.ui.notify(`Updated ${qmdPath}`, "info");
829
+ await persist();
830
+ ctx.ui.notify(`Updated ${isEmbedding ? "embedding" : "rerank"} provider config.`, "info");
871
831
  } else if (choice.startsWith("---") || choice.startsWith("[Status]")) {
872
- // Status row — just show, don't edit
873
832
  continue;
874
833
  }
875
834
  }
876
835
  },
877
836
  });
837
+
878
838
  }