pi-para 0.5.2 → 0.5.5

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 (175) hide show
  1. package/ARCHITECTURE.md +160 -0
  2. package/CHANGELOG.md +29 -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/dist/atomic-write.d.ts +2 -0
  8. package/dist/atomic-write.d.ts.map +1 -0
  9. package/dist/atomic-write.js +18 -0
  10. package/dist/atomic-write.js.map +1 -0
  11. package/dist/capture.d.ts +4 -4
  12. package/dist/capture.d.ts.map +1 -1
  13. package/dist/capture.js +1 -1
  14. package/dist/capture.js.map +1 -1
  15. package/dist/cli-legacy.d.ts +4 -0
  16. package/dist/cli-legacy.d.ts.map +1 -0
  17. package/dist/cli-legacy.js +41 -0
  18. package/dist/cli-legacy.js.map +1 -0
  19. package/dist/cli.d.ts +2 -9
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +135 -29
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands.d.ts +3 -3
  24. package/dist/commands.d.ts.map +1 -1
  25. package/dist/commands.js +189 -256
  26. package/dist/commands.js.map +1 -1
  27. package/dist/config.d.ts +104 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +242 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +4 -2
  32. package/dist/context.d.ts.map +1 -1
  33. package/dist/context.js +29 -7
  34. package/dist/context.js.map +1 -1
  35. package/dist/credentials.d.ts +27 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +85 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/daemon.d.ts +1 -1
  40. package/dist/daemon.d.ts.map +1 -1
  41. package/dist/doctor.d.ts +20 -0
  42. package/dist/doctor.d.ts.map +1 -0
  43. package/dist/doctor.js +171 -0
  44. package/dist/doctor.js.map +1 -0
  45. package/dist/gepa/index.js +1 -1
  46. package/dist/gepa/index.js.map +1 -1
  47. package/dist/index.d.ts +3 -3
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +102 -37
  50. package/dist/index.js.map +1 -1
  51. package/dist/maintainer.d.ts +2 -2
  52. package/dist/maintainer.d.ts.map +1 -1
  53. package/dist/maintainer.js +2 -2
  54. package/dist/maintainer.js.map +1 -1
  55. package/dist/model-resolver.d.ts +30 -0
  56. package/dist/model-resolver.d.ts.map +1 -0
  57. package/dist/model-resolver.js +71 -0
  58. package/dist/model-resolver.js.map +1 -0
  59. package/dist/paths.d.ts +20 -0
  60. package/dist/paths.d.ts.map +1 -0
  61. package/dist/paths.js +33 -0
  62. package/dist/paths.js.map +1 -0
  63. package/dist/processor.d.ts +2 -2
  64. package/dist/processor.d.ts.map +1 -1
  65. package/dist/processor.js +2 -2
  66. package/dist/processor.js.map +1 -1
  67. package/dist/qmd-providers.d.ts +12 -0
  68. package/dist/qmd-providers.d.ts.map +1 -0
  69. package/dist/qmd-providers.js +45 -0
  70. package/dist/qmd-providers.js.map +1 -0
  71. package/dist/query.d.ts +1 -1
  72. package/dist/query.d.ts.map +1 -1
  73. package/dist/query.js +14 -1
  74. package/dist/query.js.map +1 -1
  75. package/dist/repair.d.ts +11 -0
  76. package/dist/repair.d.ts.map +1 -0
  77. package/dist/repair.js +41 -0
  78. package/dist/repair.js.map +1 -0
  79. package/dist/scheduler/controls.d.ts +35 -0
  80. package/dist/scheduler/controls.d.ts.map +1 -0
  81. package/dist/scheduler/controls.js +98 -0
  82. package/dist/scheduler/controls.js.map +1 -0
  83. package/dist/scheduler/index.d.ts +48 -0
  84. package/dist/scheduler/index.d.ts.map +1 -0
  85. package/dist/scheduler/index.js +193 -0
  86. package/dist/scheduler/index.js.map +1 -0
  87. package/dist/scheduler/leases.d.ts +22 -0
  88. package/dist/scheduler/leases.d.ts.map +1 -0
  89. package/dist/scheduler/leases.js +74 -0
  90. package/dist/scheduler/leases.js.map +1 -0
  91. package/dist/scheduler/session-capture.d.ts +33 -0
  92. package/dist/scheduler/session-capture.d.ts.map +1 -0
  93. package/dist/scheduler/session-capture.js +108 -0
  94. package/dist/scheduler/session-capture.js.map +1 -0
  95. package/dist/scheduler/state.d.ts +56 -0
  96. package/dist/scheduler/state.d.ts.map +1 -0
  97. package/dist/scheduler/state.js +210 -0
  98. package/dist/scheduler/state.js.map +1 -0
  99. package/dist/session-tools.d.ts +1 -1
  100. package/dist/session-tools.d.ts.map +1 -1
  101. package/dist/settings.d.ts +19 -0
  102. package/dist/settings.d.ts.map +1 -0
  103. package/dist/settings.js +106 -0
  104. package/dist/settings.js.map +1 -0
  105. package/dist/setup.d.ts +29 -0
  106. package/dist/setup.d.ts.map +1 -0
  107. package/dist/setup.js +90 -0
  108. package/dist/setup.js.map +1 -0
  109. package/dist/status.d.ts +22 -0
  110. package/dist/status.d.ts.map +1 -0
  111. package/dist/status.js +93 -0
  112. package/dist/status.js.map +1 -0
  113. package/dist/store.d.ts +23 -8
  114. package/dist/store.d.ts.map +1 -1
  115. package/dist/store.js +89 -34
  116. package/dist/store.js.map +1 -1
  117. package/dist/summarize.d.ts +6 -2
  118. package/dist/summarize.d.ts.map +1 -1
  119. package/dist/summarize.js +60 -25
  120. package/dist/summarize.js.map +1 -1
  121. package/dist/templates/prompts.d.ts +1 -1
  122. package/dist/templates/prompts.d.ts.map +1 -1
  123. package/dist/templates/prompts.js +1 -1
  124. package/dist/tools.d.ts +5 -5
  125. package/dist/tools.d.ts.map +1 -1
  126. package/dist/tools.js +243 -78
  127. package/dist/tools.js.map +1 -1
  128. package/dist/wiki-tool-guidance.d.ts +6 -0
  129. package/dist/wiki-tool-guidance.d.ts.map +1 -0
  130. package/dist/wiki-tool-guidance.js +70 -0
  131. package/dist/wiki-tool-guidance.js.map +1 -0
  132. package/dist/wiki.d.ts +6 -1
  133. package/dist/wiki.d.ts.map +1 -1
  134. package/dist/wiki.js +88 -19
  135. package/dist/wiki.js.map +1 -1
  136. package/package.json +80 -15
  137. package/scripts/install.ps1 +44 -0
  138. package/scripts/install.sh +62 -0
  139. package/scripts/smoke-install.mjs +61 -0
  140. package/skills/para/SKILL.md +1 -1
  141. package/skills/setup/SKILL.md +1 -1
  142. package/src/atomic-write.ts +17 -0
  143. package/src/capture.ts +4 -4
  144. package/src/cli-legacy.ts +43 -0
  145. package/src/cli.ts +133 -28
  146. package/src/commands.ts +203 -243
  147. package/src/config.ts +355 -0
  148. package/src/context.ts +29 -14
  149. package/src/credentials.ts +100 -0
  150. package/src/daemon.ts +1 -1
  151. package/src/doctor.ts +204 -0
  152. package/src/gepa/index.ts +1 -1
  153. package/src/index.ts +119 -40
  154. package/src/maintainer.ts +4 -4
  155. package/src/model-resolver.ts +96 -0
  156. package/src/paths.ts +48 -0
  157. package/src/processor.ts +4 -4
  158. package/src/qmd-providers.ts +63 -0
  159. package/src/query.ts +15 -1
  160. package/src/repair.ts +49 -0
  161. package/src/scheduler/controls.ts +125 -0
  162. package/src/scheduler/index.ts +210 -0
  163. package/src/scheduler/leases.ts +98 -0
  164. package/src/scheduler/session-capture.ts +146 -0
  165. package/src/scheduler/state.ts +304 -0
  166. package/src/session-tools.ts +1 -1
  167. package/src/settings.ts +126 -0
  168. package/src/setup.ts +116 -0
  169. package/src/status.ts +115 -0
  170. package/src/store.ts +116 -31
  171. package/src/summarize.ts +72 -16
  172. package/src/templates/prompts.ts +1 -1
  173. package/src/tools.ts +280 -90
  174. package/src/wiki-tool-guidance.ts +84 -0
  175. package/src/wiki.ts +98 -26
package/dist/commands.js CHANGED
@@ -3,14 +3,15 @@
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
- import { readFile, appendFile } from "node:fs/promises";
8
+ import { readFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
  import { lintWiki } from "./lint.js";
11
11
  import { listPages, readPage, writePage, movePage, writeIndex, PARA_CATEGORIES } from "./wiki.js";
12
12
  import { searchWiki } from "./store.js";
13
13
  import { CURRENT_SCHEMA_VERSION, migrateToLatest, parseFrontmatter, validateFrontmatter, } from "./frontmatter.js";
14
+ import { appendCompletedSession } from "./scheduler/session-capture.js";
14
15
  // -- Helpers -----------------------------------------------------------------
15
16
  /** Convert raw text to a kebab-case slug. */
16
17
  function toKebabSlug(raw) {
@@ -208,21 +209,19 @@ export function registerCommands(pi, wikiDir, store, getScope, setScope) {
208
209
  });
209
210
  // ---- /wiki-capture — capture via session LLM ----------------------------
210
211
  pi.registerCommand("wiki-capture", {
211
- description: "Capture knowledge from this session into the wiki (runs in background via daemon)",
212
+ description: "Queue this session for scheduler-backed wiki capture",
212
213
  handler: async (args, ctx) => {
213
214
  const sessionFile = ctx.sessionManager.getSessionFile();
214
215
  if (!sessionFile) {
215
216
  ctx.ui.notify("No session file found", "warning");
216
217
  return;
217
218
  }
218
- // Register the session for daemon capture — runs in background, doesn't block user
219
+ // Register the session for scheduler capture — runs in background, doesn't block user
219
220
  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");
221
+ await appendCompletedSession(wikiDir, sessionFile);
222
+ ctx.ui.notify("Session queued for wiki capture (scheduler will process when Pi is open).\n" +
223
+ "Check progress: /wiki-scheduler status\n" +
224
+ "Tip: Set the capture model in /wiki-settings for better capture quality.", "info");
226
225
  ctx.ui.setStatus("pi-para", "wiki: capture queued");
227
226
  setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
228
227
  }
@@ -312,85 +311,78 @@ export function registerCommands(pi, wikiDir, store, getScope, setScope) {
312
311
  pi.sendUserMessage(`Summarize the wiki: target="${target}". Use wiki_summarize tool with target="${target}".`);
313
312
  },
314
313
  });
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 {
314
+ // ---- /wiki-scheduler — scheduler/capture queue status ----------------------
315
+ const registerSchedulerStatusCommand = (name, legacy) => {
316
+ pi.registerCommand(name, {
317
+ description: legacy
318
+ ? "Compatibility alias for /wiki-scheduler"
319
+ : "Check scheduler queue, task history, and capture status",
320
+ getArgumentCompletions: (prefix) => {
321
+ const cmds = ["status", "queue", "history", "capture-history"];
322
+ const filtered = cmds.filter((c) => c.startsWith(prefix));
323
+ return filtered.length > 0 ? filtered.map((c) => ({ value: c, label: c })) : null;
324
+ },
325
+ handler: async (args, ctx) => {
326
+ const subcmd = args.trim().split(/\s+/)[0] || "status";
327
+ const { listSchedulerTasks, listSchedulerHistory, formatQueueItems, formatSchedulerHistory } = await import("./scheduler/controls.js");
328
+ const { StateDB } = await import("./state.js");
329
329
  switch (subcmd) {
330
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
- }
331
+ const queued = listSchedulerTasks(wikiDir, { status: "queued" }).length;
332
+ const running = listSchedulerTasks(wikiDir, { status: "running" }).length;
333
+ const failed = listSchedulerTasks(wikiDir, { status: "failed" }).length;
334
+ const recent = listSchedulerHistory(wikiDir, { limit: 5 });
343
335
  const lines = [
344
- `Daemon: ${running ? `running (PID ${pid})` : "not running"}`,
345
- startedAt ? `Started: ${startedAt}` : "",
346
- `Failed: ${failed.length}`,
336
+ legacy ? "Scheduler: active (via /wiki-daemon compatibility alias)" : "Scheduler: active",
337
+ `Queue: ${queued} queued, ${running} running, ${failed} failed`,
347
338
  "",
348
- "Recent captures:",
339
+ "Recent scheduler history:",
340
+ formatSchedulerHistory(recent).split("\n").map((line) => ` ${line}`).join("\n"),
349
341
  ];
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
342
  ctx.ui.notify(lines.filter(Boolean).join("\n"), "info");
364
343
  break;
365
344
  }
345
+ case "queue": {
346
+ ctx.ui.notify(formatQueueItems(listSchedulerTasks(wikiDir)), "info");
347
+ break;
348
+ }
366
349
  case "history": {
350
+ ctx.ui.notify(formatSchedulerHistory(listSchedulerHistory(wikiDir, { limit: 15 })), "info");
351
+ break;
352
+ }
353
+ case "capture-history": {
367
354
  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;
355
+ const state = new StateDB(wikiDir);
356
+ try {
357
+ const history = state.getHistory(scope.name, 15);
358
+ if (history.length === 0) {
359
+ ctx.ui.notify(`No capture history for scope: ${scope.name}`, "info");
360
+ break;
361
+ }
362
+ const lines = [`Capture history (${scope.name}):\n`];
363
+ for (const h of history) {
364
+ const status = h.error
365
+ ? `ERROR: ${h.error.slice(0, 60)}`
366
+ : h.pagesCreated.length > 0
367
+ ? `${h.pagesCreated.length} page(s): ${h.pagesCreated.join(", ")}`
368
+ : "skipped";
369
+ lines.push(`${h.processedAt.slice(0, 19)} | ${status}`);
370
+ }
371
+ ctx.ui.notify(lines.join("\n"), "info");
372
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}`);
373
+ finally {
374
+ state.close();
381
375
  }
382
- ctx.ui.notify(lines.join("\n"), "info");
383
376
  break;
384
377
  }
385
378
  default:
386
- ctx.ui.notify(`Usage: /wiki-daemon [status|history]`, "error");
379
+ ctx.ui.notify(`Usage: /${name} [status|queue|history|capture-history]`, "error");
387
380
  }
388
- }
389
- finally {
390
- state.close();
391
- }
392
- },
393
- });
381
+ },
382
+ });
383
+ };
384
+ registerSchedulerStatusCommand("wiki-scheduler", false);
385
+ registerSchedulerStatusCommand("wiki-daemon", true);
394
386
  // ---- /wiki-migrate — batch schema migration ------------------------------
395
387
  pi.registerCommand("wiki-migrate", {
396
388
  description: "Migrate all wiki pages to the current schema version",
@@ -517,266 +509,207 @@ ${goal} — verified and complete.
517
509
  ctx.ui.notify("/wiki-settings requires interactive mode", "error");
518
510
  return;
519
511
  }
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
512
+ const { loadParaConfig, saveParaConfig } = await import("./config.js");
513
+ const { modelSelectionLabel, providerProfileLabel, setContextMaxTokens, setSearchLimit, setLintStaleDays, toggleSearchGraphBoost, toggleLintAutoFix, setCaptureModelAuto, setCaptureModel, setWebWikiEnabled, setWebWikiHost, setWebWikiPort, ensureEmbeddingProfile, ensureRerankProfile, disableRerank, setProviderProfileField, setProviderDims, } = await import("./settings.js");
514
+ const { listSchedulerTasks } = await import("./scheduler/controls.js");
515
+ const { setSecret } = await import("./credentials.js");
516
+ const loaded = await loadParaConfig({ migrate: true });
517
+ const config = loaded.config;
518
+ const persist = async () => saveParaConfig(config, { homeDir: loaded.paths.homeDir });
551
519
  while (true) {
552
520
  const scope = getScope();
553
521
  const pageCount = (await listPages(wikiDir).catch(() => [])).length;
522
+ const queuedTasks = listSchedulerTasks(wikiDir, { status: "queued" }).length;
523
+ const failedTasks = listSchedulerTasks(wikiDir, { status: "failed" }).length;
554
524
  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 : ""}`,
525
+ `[Context] Max tokens: ${config.context.maxTokens}`,
526
+ `[Search] Limit: ${config.context.searchLimit}, Graph boost: ${config.context.searchGraphBoost}`,
527
+ `[Lint] Auto-fix: ${config.lint.autoFix}, Stale days: ${config.lint.staleDays}`,
528
+ `[Capture] Model: ${modelSelectionLabel(config.models.capture)}`,
529
+ `[WebWiki] ${config.webWiki.enabled ? `Enabled at http://${config.webWiki.host}:${config.webWiki.port}` : "Disabled"}`,
530
+ `[Embedding] ${providerProfileLabel(config.qmd.embedding)}`,
531
+ `[Rerank] ${providerProfileLabel(config.qmd.rerank)}`,
563
532
  `---`,
564
- `[Status] Scope: ${scope.name} | Pages: ${pageCount} | qmd: ${qmdVersion || "not installed"} | Daemon: ${daemonStatus}`,
533
+ `[Status] Scope: ${scope.name} | Pages: ${pageCount} | qmd: SDK | Scheduler: ${queuedTasks} queued, ${failedTasks} failed`,
565
534
  ]);
566
535
  if (!choice)
567
536
  break;
568
537
  if (choice.startsWith("[Context]")) {
569
- const val = await ctx.ui.input("Context max tokens:", String(config.contextMaxTokens ?? 4000));
538
+ const val = await ctx.ui.input("Context max tokens:", String(config.context.maxTokens));
570
539
  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");
540
+ setContextMaxTokens(config, val);
541
+ await persist();
542
+ ctx.ui.notify(`Set context.maxTokens = ${config.context.maxTokens}`, "info");
574
543
  }
575
544
  }
576
545
  else if (choice.startsWith("[Search]")) {
577
546
  const sub = await ctx.ui.select("Search Settings", [
578
- `Limit: ${config.searchLimit ?? 10}`,
579
- `Graph boost: ${config.searchGraphBoost ?? true}`,
547
+ `Limit: ${config.context.searchLimit}`,
548
+ `Graph boost: ${config.context.searchGraphBoost}`,
580
549
  ]);
581
550
  if (sub?.startsWith("Limit")) {
582
- const val = await ctx.ui.input("Search result limit:", String(config.searchLimit ?? 10));
551
+ const val = await ctx.ui.input("Search result limit:", String(config.context.searchLimit));
583
552
  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");
553
+ setSearchLimit(config, val);
554
+ await persist();
555
+ ctx.ui.notify(`Set context.searchLimit = ${config.context.searchLimit}`, "info");
587
556
  }
588
557
  }
589
558
  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");
559
+ toggleSearchGraphBoost(config);
560
+ await persist();
561
+ ctx.ui.notify(`Set context.searchGraphBoost = ${config.context.searchGraphBoost}`, "info");
593
562
  }
594
563
  }
595
564
  else if (choice.startsWith("[Lint]")) {
596
565
  const sub = await ctx.ui.select("Lint Settings", [
597
- `Auto-fix: ${config.lintAutoFix ?? true}`,
598
- `Stale days: ${config.lintStaleDays ?? 90}`,
566
+ `Auto-fix: ${config.lint.autoFix}`,
567
+ `Stale days: ${config.lint.staleDays}`,
599
568
  ]);
600
569
  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");
570
+ toggleLintAutoFix(config);
571
+ await persist();
572
+ ctx.ui.notify(`Set lint.autoFix = ${config.lint.autoFix}`, "info");
604
573
  }
605
574
  else if (sub?.startsWith("Stale")) {
606
- const val = await ctx.ui.input("Stale days threshold:", String(config.lintStaleDays ?? 90));
575
+ const val = await ctx.ui.input("Stale days threshold:", String(config.lint.staleDays));
607
576
  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");
577
+ setLintStaleDays(config, val);
578
+ await persist();
579
+ ctx.ui.notify(`Set lint.staleDays = ${config.lint.staleDays}`, "info");
611
580
  }
612
581
  }
613
582
  }
614
- else if (choice.startsWith("[Daemon]")) {
583
+ else if (choice.startsWith("[Capture]")) {
615
584
  try {
616
- const { getProviders, getModels } = await import("@mariozechner/pi-ai");
617
- const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
585
+ const { getProviders, getModels } = await import("@earendil-works/pi-ai/compat");
586
+ const { AuthStorage } = await import("@earendil-works/pi-coding-agent");
618
587
  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
- }
588
+ const availableProviders = getProviders().filter((p) => authStorage.hasAuth(p));
626
589
  if (availableProviders.length === 0) {
627
- ctx.ui.notify("No providers configured. Log in with pi first.", "warning");
590
+ ctx.ui.notify("No Pi auth providers configured. Use /login first or configure a pi-para secret for custom providers.", "warning");
628
591
  }
629
592
  else {
630
- // Let user pick provider
631
- const providerChoice = await ctx.ui.select("Select provider", ["auto (best available)", ...availableProviders]);
593
+ const providerChoice = await ctx.ui.select("Select capture provider", ["auto (best available)", ...availableProviders]);
632
594
  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");
595
+ setCaptureModelAuto(config);
596
+ await persist();
597
+ ctx.ui.notify("Set capture model = auto. Restart open Pi sessions to apply.", "info");
640
598
  }
641
599
  else if (providerChoice) {
642
- // Show models for selected provider
643
600
  const models = getModels(providerChoice)
644
601
  .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);
602
+ const modelChoice = await ctx.ui.select(`Select ${providerChoice} model`, models.map((m) => `${m.id} (${Math.round((m.contextWindow ?? 0) / 1000)}k ctx${m.reasoning ? ", reasoning" : ""})`));
647
603
  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");
604
+ setCaptureModel(config, providerChoice, modelChoice.split(" (")[0]);
605
+ await persist();
606
+ ctx.ui.notify(`Set capture model = ${modelSelectionLabel(config.models.capture)}. Restart open Pi sessions to apply.`, "info");
656
607
  }
657
608
  }
658
609
  }
659
610
  }
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");
611
+ catch {
612
+ const val = await ctx.ui.input("Capture model (provider/model-id, empty for auto):", modelSelectionLabel(config.models.capture));
613
+ if (!val?.trim())
614
+ setCaptureModelAuto(config);
615
+ else {
616
+ const slash = val.indexOf("/");
617
+ if (slash <= 0) {
618
+ ctx.ui.notify("Use provider/model-id format, or leave empty for auto.", "error");
619
+ continue;
620
+ }
621
+ setCaptureModel(config, val.slice(0, slash), val.slice(slash + 1));
668
622
  }
669
- catch { }
670
- ctx.ui.notify(`Set daemonModel = ${config.daemonModel ?? "auto"} (daemon restarted)`, "info");
623
+ await persist();
624
+ ctx.ui.notify(`Set capture model = ${modelSelectionLabel(config.models.capture)}. Restart open Pi sessions to apply.`, "info");
671
625
  }
672
626
  }
673
627
  else if (choice.startsWith("[WebWiki]")) {
674
- const webWiki = config.webWiki ?? { enabled: false, host: "0.0.0.0", port: 10973 };
675
628
  const sub = await ctx.ui.select("Web Wiki Settings", [
676
- `Enabled: ${webWiki.enabled}`,
677
- `Host: ${webWiki.host}`,
678
- `Port: ${webWiki.port}`,
629
+ `Enabled: ${config.webWiki.enabled}`,
630
+ `Host: ${config.webWiki.host}`,
631
+ `Port: ${config.webWiki.port}`,
679
632
  ]);
680
633
  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
- }
634
+ setWebWikiEnabled(config, !config.webWiki.enabled);
635
+ await persist();
636
+ ctx.ui.notify(`Web Wiki ${config.webWiki.enabled ? "enabled" : "disabled"}. Restart open Pi sessions to apply.`, "info");
702
637
  }
703
638
  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);
639
+ const val = await ctx.ui.input("Host:", config.webWiki.host);
705
640
  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");
641
+ setWebWikiHost(config, val);
642
+ await persist();
710
643
  }
711
644
  }
712
645
  else if (sub?.startsWith("Port")) {
713
- const val = await ctx.ui.input("Port:", String(webWiki.port));
646
+ const val = await ctx.ui.input("Port:", String(config.webWiki.port));
714
647
  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");
648
+ setWebWikiPort(config, val);
649
+ await persist();
719
650
  }
720
651
  }
721
652
  }
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`,
653
+ else if (choice.startsWith("[Embedding]") || choice.startsWith("[Rerank]")) {
654
+ const isEmbedding = choice.startsWith("[Embedding]");
655
+ const profile = isEmbedding ? ensureEmbeddingProfile(config) : ensureRerankProfile(config);
656
+ const sub = await ctx.ui.select(isEmbedding ? "Embedding provider" : "Rerank provider", [
657
+ `Provider: ${profile.provider}`,
658
+ `Base URL: ${profile.baseUrl ?? "not set"}`,
659
+ `Model: ${profile.model ?? "not set"}`,
660
+ ...(isEmbedding ? [`Dims: ${profile.dims ?? "auto"}`] : []),
661
+ `API format: ${profile.apiFormat ?? "openai"}`,
662
+ `Credential ref: ${profile.credentialRef}`,
663
+ `Store/update local secret`,
664
+ ...(!isEmbedding ? [`Disable rerank`] : []),
732
665
  ]);
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
- }
666
+ if (sub?.startsWith("Provider")) {
667
+ const val = await ctx.ui.input("Provider id:", profile.provider);
668
+ if (val)
669
+ setProviderProfileField(profile, "provider", val);
739
670
  }
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
- }
671
+ else if (sub?.startsWith("Base URL")) {
672
+ const val = await ctx.ui.input("Base URL:", profile.baseUrl ?? "");
673
+ if (val)
674
+ setProviderProfileField(profile, "baseUrl", val);
746
675
  }
747
676
  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
- }
677
+ const val = await ctx.ui.input("Model id:", profile.model ?? "");
678
+ if (val)
679
+ setProviderProfileField(profile, "model", val);
753
680
  }
754
681
  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
- }
682
+ const val = await ctx.ui.input("Embedding dimensions:", profile.dims ? String(profile.dims) : "");
683
+ if (val)
684
+ setProviderDims(profile, val);
760
685
  }
761
686
  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;
687
+ const fmt = await ctx.ui.select("API format", ["openai", "anthropic", "custom"]);
688
+ if (fmt)
689
+ setProviderProfileField(profile, "apiFormat", fmt);
690
+ }
691
+ else if (sub?.startsWith("Credential ref")) {
692
+ const val = await ctx.ui.input("Credential ref (pi-auth:<provider>, secret:<name>, or none):", profile.credentialRef);
693
+ if (val)
694
+ setProviderProfileField(profile, "credentialRef", val);
695
+ }
696
+ else if (sub?.startsWith("Store/update local secret")) {
697
+ const name = await ctx.ui.input("Secret name:", profile.credentialRef.startsWith("secret:") ? profile.credentialRef.slice("secret:".length) : "");
698
+ if (name) {
699
+ const value = await ctx.ui.input("API key (stored in ~/.pi/para/secrets.json, not wiki git):", "");
700
+ if (value) {
701
+ await setSecret(name, value, loaded.paths.secretsPath);
702
+ setProviderProfileField(profile, "credentialRef", `secret:${name}`);
703
+ }
766
704
  }
767
705
  }
768
- else if (sub?.startsWith("Remove")) {
769
- delete providers[providerType];
706
+ else if (sub?.startsWith("Disable rerank")) {
707
+ disableRerank(config);
770
708
  }
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");
709
+ await persist();
710
+ ctx.ui.notify(`Updated ${isEmbedding ? "embedding" : "rerank"} provider config.`, "info");
777
711
  }
778
712
  else if (choice.startsWith("---") || choice.startsWith("[Status]")) {
779
- // Status row — just show, don't edit
780
713
  continue;
781
714
  }
782
715
  }