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/doctor.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { initWiki } from "./wiki.js";
3
+ import { loadParaConfig, type ParaUserConfig } from "./config.js";
4
+ import { readSecretStore, resolveCredentialRef } from "./credentials.js";
5
+ import { getParaPaths } from "./paths.js";
6
+ import { openStore, closeStore } from "./store.js";
7
+ import type { ProviderCredentialRef } from "./config.js";
8
+ import { createPiModelRegistry, getCaptureSelection, resolveSelectedModel } from "./model-resolver.js";
9
+ import { SchedulerStateDB } from "./scheduler/state.js";
10
+ import { ensureGeneratedStateGitignore, fixSecretPermissions, missingGeneratedStateGitignorePatterns } from "./repair.js";
11
+
12
+ export type DoctorStatus = "ok" | "warn" | "error";
13
+
14
+ export interface DoctorCheck {
15
+ name: string;
16
+ status: DoctorStatus;
17
+ message: string;
18
+ fixable?: boolean;
19
+ }
20
+
21
+ export interface DoctorOptions {
22
+ homeDir?: string;
23
+ fix?: boolean;
24
+ validateQmd?: boolean;
25
+ testCaptureModel?: boolean;
26
+ }
27
+
28
+ export interface DoctorResult {
29
+ ok: boolean;
30
+ checks: DoctorCheck[];
31
+ }
32
+
33
+ export async function runDoctor(options: DoctorOptions = {}): Promise<DoctorResult> {
34
+ const checks: DoctorCheck[] = [];
35
+ const loaded = await loadParaConfig({ homeDir: options.homeDir, migrate: options.fix ?? false });
36
+ const paths = getParaPaths({ homeDir: options.homeDir, wikiDir: loaded.config.wiki.dir });
37
+
38
+ checks.push(checkExists("config", paths.userConfigPath, "warn"));
39
+ checks.push(checkExists("pi-settings", paths.piSettingsPath, "warn"));
40
+
41
+ if (options.fix) await initWiki(paths.wikiDir);
42
+ checks.push(checkExists("wiki-dir", paths.wikiDir, "warn", true));
43
+
44
+ const schedulerCheck = checkScheduler(paths.schedulerDbPath);
45
+ checks.push(schedulerCheck);
46
+
47
+ if (options.fix) {
48
+ fixSecretPermissions(paths.secretsPath);
49
+ await ensureGeneratedStateGitignore(paths.wikiDir);
50
+ }
51
+
52
+ checks.push(await checkSecrets(paths.secretsPath));
53
+ checks.push(await checkGeneratedStateGitignore(paths.wikiDir));
54
+ checks.push(...await checkProviderDiagnostics(loaded.config, paths.secretsPath, options));
55
+
56
+ if (options.validateQmd !== false) {
57
+ checks.push(await checkQmd(paths.wikiDir, loaded.config, paths.secretsPath));
58
+ }
59
+
60
+ const backlogPath = `${paths.wikiDir}/.completed-sessions`;
61
+ if (existsSync(backlogPath)) {
62
+ checks.push({
63
+ name: "capture-backlog",
64
+ status: "warn",
65
+ message: ".completed-sessions exists; startup catch-up will process queued entries once capture task is enabled",
66
+ });
67
+ } else {
68
+ checks.push({ name: "capture-backlog", status: "ok", message: "no completed-session registry found" });
69
+ }
70
+
71
+ return { ok: checks.every((check) => check.status !== "error"), checks };
72
+ }
73
+
74
+ export function formatDoctorResult(result: DoctorResult): string {
75
+ const lines = ["pi-para doctor"];
76
+ for (const check of result.checks) {
77
+ const icon = check.status === "ok" ? "✓" : check.status === "warn" ? "⚠" : "✗";
78
+ lines.push(` ${icon} ${check.name}: ${check.message}`);
79
+ }
80
+ return lines.join("\n");
81
+ }
82
+
83
+ function checkExists(name: string, path: string, missingStatus: DoctorStatus, fixable = false): DoctorCheck {
84
+ return existsSync(path)
85
+ ? { name, status: "ok", message: path }
86
+ : { name, status: missingStatus, message: `${path} not found`, fixable };
87
+ }
88
+
89
+ function checkScheduler(dbPath: string): DoctorCheck {
90
+ try {
91
+ const db = new SchedulerStateDB(dbPath);
92
+ const queued = db.list("queued").length;
93
+ db.close();
94
+ return { name: "scheduler", status: "ok", message: `${dbPath} (${queued} queued)` };
95
+ } catch (err) {
96
+ return { name: "scheduler", status: "error", message: err instanceof Error ? err.message : String(err) };
97
+ }
98
+ }
99
+
100
+ async function checkSecrets(secretsPath: string): Promise<DoctorCheck> {
101
+ try {
102
+ const store = await readSecretStore(secretsPath);
103
+ if (!existsSync(secretsPath)) return { name: "secrets", status: "ok", message: "no local pi-para secrets configured" };
104
+ // POSIX mode bits are meaningless on Windows/NTFS — stat reports 666 and
105
+ // chmod is a no-op, so a permissions warning there is a false positive.
106
+ if (process.platform === "win32") {
107
+ return { name: "secrets", status: "ok", message: `${Object.keys(store.secrets).length} local secret(s) (POSIX permission check skipped on Windows)` };
108
+ }
109
+ const mode = statSync(secretsPath).mode & 0o777;
110
+ if (mode & 0o077) {
111
+ return { name: "secrets", status: "warn", message: `${secretsPath} permissions are too open (${mode.toString(8)})`, fixable: true };
112
+ }
113
+ return { name: "secrets", status: "ok", message: `${Object.keys(store.secrets).length} local secret(s), permissions ${mode.toString(8)}` };
114
+ } catch (err) {
115
+ return { name: "secrets", status: "error", message: err instanceof Error ? err.message : String(err) };
116
+ }
117
+ }
118
+
119
+ async function checkGeneratedStateGitignore(wikiDir: string): Promise<DoctorCheck> {
120
+ try {
121
+ const missing = missingGeneratedStateGitignorePatterns(wikiDir);
122
+ if (missing.length > 0) {
123
+ return { name: "gitignore", status: "warn", message: `missing generated-state ignores: ${missing.join(", ")}`, fixable: true };
124
+ }
125
+ return { name: "gitignore", status: "ok", message: "generated SQLite/state files are ignored" };
126
+ } catch (err) {
127
+ return { name: "gitignore", status: "error", message: err instanceof Error ? err.message : String(err) };
128
+ }
129
+ }
130
+
131
+ async function checkProviderDiagnostics(
132
+ config: ParaUserConfig,
133
+ secretsPath: string,
134
+ options: DoctorOptions,
135
+ ): Promise<DoctorCheck[]> {
136
+ const checks: DoctorCheck[] = [];
137
+
138
+ if (config.qmd.providerConfig === "legacy-qmd-compatible") {
139
+ checks.push({ name: "qmd-providers", status: "warn", message: "using legacy QMD provider config compatibility mode" });
140
+ } else {
141
+ checks.push({ name: "qmd-providers", status: "ok", message: "using pi-para provider profiles" });
142
+ }
143
+
144
+ if (!config.qmd.embedEnabled) {
145
+ checks.push({ name: "qmd-embedding", status: "ok", message: "embedding disabled" });
146
+ } else if (config.qmd.embedding) {
147
+ checks.push(await checkCredentialProfile("qmd-embedding", config.qmd.embedding, secretsPath));
148
+ } else {
149
+ checks.push({ name: "qmd-embedding", status: "warn", message: "embedding enabled but no pi-para embedding profile is configured" });
150
+ }
151
+
152
+ if (config.qmd.rerank) {
153
+ checks.push(await checkCredentialProfile("qmd-rerank", config.qmd.rerank, secretsPath));
154
+ } else {
155
+ checks.push({ name: "qmd-rerank", status: "ok", message: "rerank disabled" });
156
+ }
157
+
158
+ checks.push(await checkCaptureModel(config, secretsPath, options.testCaptureModel === true));
159
+ return checks;
160
+ }
161
+
162
+ async function checkCredentialProfile(name: string, profile: ProviderCredentialRef, secretsPath: string): Promise<DoctorCheck> {
163
+ const label = `${profile.provider}/${profile.model ?? "model not set"}`;
164
+ if (profile.credentialRef === "none") {
165
+ return { name, status: "ok", message: `${label} uses no credential` };
166
+ }
167
+ const resolved = await resolveCredentialRef(profile.credentialRef, { secretsPath });
168
+ if (!resolved.ok) {
169
+ return { name, status: "warn", message: `${label} credential missing: ${resolved.error}` };
170
+ }
171
+ return { name, status: "ok", message: `${label} credential available via ${resolved.source}` };
172
+ }
173
+
174
+ async function checkCaptureModel(config: ParaUserConfig, secretsPath: string, testModel: boolean): Promise<DoctorCheck> {
175
+ const selection = getCaptureSelection(config);
176
+ if (!testModel) {
177
+ if (selection === "auto") return { name: "capture-model", status: "ok", message: "auto selection; run doctor --test-capture-model to resolve" };
178
+ return checkCredentialProfile("capture-model", selection, secretsPath);
179
+ }
180
+
181
+ try {
182
+ const registry = await createPiModelRegistry();
183
+ if (!registry) return { name: "capture-model", status: "warn", message: "Pi model registry unavailable" };
184
+ const model = resolveSelectedModel(selection, registry.modelRegistry, { preferredModelSpec: "anthropic/claude-sonnet-4-20250514" });
185
+ if (!model) return { name: "capture-model", status: "warn", message: "no capture model resolved" };
186
+ const getKey = selection === "auto"
187
+ ? () => registry.modelRegistry.getApiKeyForProvider(model.provider)
188
+ : () => resolveCredentialRef(selection.credentialRef, { secretsPath, authStorage: registry.authStorage }).then((r) => r.value);
189
+ const key = await getKey();
190
+ return key
191
+ ? { name: "capture-model", status: "ok", message: `${model.provider}/${model.id} credential available` }
192
+ : { name: "capture-model", status: "warn", message: `${model.provider}/${model.id} resolved but credential is missing` };
193
+ } catch (err) {
194
+ return { name: "capture-model", status: "warn", message: err instanceof Error ? err.message : String(err) };
195
+ }
196
+ }
197
+
198
+ async function checkQmd(wikiDir: string, config: ParaUserConfig, secretsPath: string): Promise<DoctorCheck> {
199
+ try {
200
+ const store = await openStore(wikiDir, {
201
+ paraConfig: { ...config, qmd: { ...config.qmd, embedEnabled: false } },
202
+ secretsPath,
203
+ });
204
+ await closeStore(store);
205
+ return { name: "qmd-sdk", status: "ok", message: "store opened and updated with pi-para provider profiles" };
206
+ } catch (err) {
207
+ return { name: "qmd-sdk", status: "error", message: err instanceof Error ? err.message : String(err), fixable: false };
208
+ }
209
+ }
package/src/gepa/index.ts CHANGED
@@ -80,7 +80,7 @@ function findPackageRoot(): string {
80
80
  if (existsSync(pkgPath)) {
81
81
  try {
82
82
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
83
- if (pkg.name === "@picassio/pi-para") return dir;
83
+ if (pkg.name === "pi-para" || pkg.name === "@picassio/pi-para") return dir;
84
84
  } catch {}
85
85
  }
86
86
  dir = dirname(dir);
package/src/index.ts CHANGED
@@ -4,15 +4,14 @@
4
4
  * Main extension entry point. Wires together wiki filesystem, qmd store,
5
5
  * scope detection, context injection, tools, and commands.
6
6
  *
7
- * Session capture is handled by a separate daemon (pi-para-daemon).
8
- * This extension registers completed sessions for the daemon to process.
7
+ * Session capture and maintenance are coordinated by an in-process scheduler.
8
+ * Completed-session registry entries are kept as a durable catch-up queue.
9
9
  */
10
10
 
11
- import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
12
- import { join, dirname } from "node:path";
11
+ import { join } from "node:path";
13
12
  import { homedir, networkInterfaces } from "node:os";
14
13
 
15
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
14
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
15
  import type { QMDStore } from "qmd-engine";
17
16
 
18
17
  import type { ProjectScope } from "./scope.js";
@@ -24,6 +23,15 @@ import { setupContextInjection, markContextDirty } from "./context.js";
24
23
  import type { ContextConfig } from "./context.js";
25
24
  import { registerCommands } from "./commands.js";
26
25
  import { createServer } from "node:http";
26
+ import { startWikiScheduler, stopWikiScheduler } from "./scheduler/index.js";
27
+ import {
28
+ appendCompletedSession,
29
+ createCaptureSessionHandler,
30
+ enqueueCompletedSessionsFromRegistry,
31
+ } from "./scheduler/session-capture.js";
32
+ import { loadParaConfig, toLegacyRuntimeConfig, type ParaUserConfig } from "./config.js";
33
+ import { createModelApiKeyResolver, createPiModelRegistry, getCaptureSelection, resolveSelectedModel } from "./model-resolver.js";
34
+ import { StateDB } from "./state.js";
27
35
 
28
36
  // Re-export public types for consumers
29
37
  export type { ParaCategory, WikiPage, PageFrontmatter, PageRef, LogEntry } from "./wiki.js";
@@ -48,8 +56,8 @@ interface ParaConfig {
48
56
  searchLimit: number;
49
57
  searchIncludeArchives: boolean;
50
58
  searchGraphBoost: boolean;
51
- /** Daemon LLM: "provider/model-id" (e.g. "anthropic/claude-sonnet-4").
52
- * If not set, daemon auto-detects from pi env keys or qmd config. */
59
+ /** Legacy capture LLM: "provider/model-id" (e.g. "anthropic/claude-sonnet-4").
60
+ * New config stores this as models.capture; legacy field remains for migration. */
53
61
  daemonModel: string | null;
54
62
  /** Web wiki UI settings */
55
63
  webWiki: {
@@ -108,30 +116,37 @@ function getDefaultConfig(): ParaConfig {
108
116
  };
109
117
  }
110
118
 
111
- async function loadConfig(): Promise<ParaConfig> {
112
- const defaults = getDefaultConfig();
113
- const configPath = join(defaults.wikiDir, "config.json");
119
+ interface LoadedRuntimeConfig {
120
+ runtime: ParaConfig;
121
+ userConfig: ParaUserConfig | null;
122
+ secretsPath: string | null;
123
+ }
124
+
125
+ async function loadConfig(): Promise<LoadedRuntimeConfig> {
114
126
  try {
115
- const raw = await readFile(configPath, "utf-8");
116
- const parsed = JSON.parse(raw) as Partial<ParaConfig>;
117
- const wikiDir = parsed.wikiDir
118
- ? parsed.wikiDir.replace(/^~\//, `${homedir()}/`)
119
- : defaults.wikiDir;
120
- return { ...defaults, ...parsed, wikiDir };
127
+ const loaded = await loadParaConfig({ migrate: true });
128
+ const runtime = toLegacyRuntimeConfig(loaded.config);
129
+ return {
130
+ runtime: {
131
+ ...runtime,
132
+ gepa: {
133
+ ...runtime.gepa,
134
+ studentModel: runtime.gepa.studentModel ?? null,
135
+ teacherModel: runtime.gepa.teacherModel ?? null,
136
+ judgeModel: runtime.gepa.judgeModel ?? null,
137
+ },
138
+ },
139
+ userConfig: loaded.config,
140
+ secretsPath: loaded.paths.secretsPath,
141
+ };
121
142
  } catch {
122
- try {
123
- await mkdir(dirname(configPath), { recursive: true });
124
- await writeFile(configPath, JSON.stringify(defaults, null, 2), "utf-8");
125
- } catch {
126
- // Non-fatal
127
- }
128
- return { ...defaults };
143
+ return { runtime: getDefaultConfig(), userConfig: null, secretsPath: null };
129
144
  }
130
145
  }
131
146
 
132
147
  // -- LAN IP detection --------------------------------------------------------
133
148
 
134
- /** Check if the daemon's web server is alive on the given port. */
149
+ /** Check if the optional web wiki server is alive on the given port. */
135
150
  function checkWebServerAlive(port: number): Promise<boolean> {
136
151
  return new Promise((resolve) => {
137
152
  const req = createServer().listen(port, "127.0.0.1");
@@ -148,6 +163,10 @@ function checkWebServerAlive(port: number): Promise<boolean> {
148
163
  });
149
164
  }
150
165
 
166
+ function isPrintModeProcess(argv = process.argv): boolean {
167
+ return argv.includes("-p") || argv.includes("--print") || argv.includes("--mode") && argv[argv.indexOf("--mode") + 1] === "json";
168
+ }
169
+
151
170
  function getLanIp(): string | null {
152
171
  const interfaces = networkInterfaces();
153
172
  for (const name of Object.keys(interfaces)) {
@@ -184,7 +203,8 @@ function reconstructState(ctx: ExtensionContext): ParaSessionState | null {
184
203
  // -- Extension entry point ---------------------------------------------------
185
204
 
186
205
  export default async function piPara(pi: ExtensionAPI): Promise<void> {
187
- const config = await loadConfig();
206
+ const loadedConfig = await loadConfig();
207
+ const config = loadedConfig.runtime;
188
208
  const wikiDir = config.wikiDir;
189
209
 
190
210
  let store: QMDStore | null = null;
@@ -221,7 +241,11 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
221
241
  if (storeRetrying) return false;
222
242
  storeRetrying = true;
223
243
  try {
224
- store = await openStore(wikiDir);
244
+ store = await openStore(wikiDir, {
245
+ paraConfig: loadedConfig.userConfig ?? undefined,
246
+ secretsPath: loadedConfig.secretsPath ?? undefined,
247
+ backgroundEmbed: false,
248
+ });
225
249
  storeDisabled = false;
226
250
  markContextDirty(); // rebuild context now that store works
227
251
  return true;
@@ -280,7 +304,11 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
280
304
  ctx.ui.setStatus("pi-para", "wiki: indexing...");
281
305
  storeDisabled = false;
282
306
  try {
283
- store = await openStore(wikiDir);
307
+ store = await openStore(wikiDir, {
308
+ paraConfig: loadedConfig.userConfig ?? undefined,
309
+ secretsPath: loadedConfig.secretsPath ?? undefined,
310
+ backgroundEmbed: false,
311
+ });
284
312
  } catch (err) {
285
313
  store = null;
286
314
  storeDisabled = true;
@@ -302,7 +330,60 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
302
330
  sessionFile: ctx.sessionManager.getSessionFile() ?? null,
303
331
  } satisfies ParaSessionState);
304
332
 
305
- // Check if daemon's web wiki server is running (don't start our own)
333
+ const schedulerHandlers = {} as Record<string, any>;
334
+ const schedulerConfig = loadedConfig.userConfig?.scheduler;
335
+ const wikiConfig = loadedConfig.userConfig?.wiki;
336
+ const autoCaptureEnabled = wikiConfig?.autoCapture !== false;
337
+ const startupCatchupEnabled = autoCaptureEnabled && wikiConfig?.captureOnStartup !== false && schedulerConfig?.startupCatchup !== false;
338
+ try {
339
+ const loadedParaConfig = await loadParaConfig({ migrate: true });
340
+ const registry = autoCaptureEnabled ? await createPiModelRegistry() : null;
341
+ if (registry) {
342
+ const captureSelection = getCaptureSelection(loadedParaConfig.config);
343
+ const captureModel = resolveSelectedModel(captureSelection, registry.modelRegistry, {
344
+ legacyModelSpec: config.daemonModel,
345
+ preferredModelSpec: "anthropic/claude-sonnet-4-20250514",
346
+ });
347
+ if (captureModel) {
348
+ schedulerHandlers["capture-session"] = createCaptureSessionHandler({
349
+ wikiDir,
350
+ storeProvider: () => store,
351
+ model: captureModel,
352
+ getApiKey: createModelApiKeyResolver(captureSelection, registry.modelRegistry, {
353
+ authStorage: registry.authStorage,
354
+ secretsPath: loadedParaConfig.paths.secretsPath,
355
+ }),
356
+ });
357
+ }
358
+ }
359
+ } catch {
360
+ // Capture handler registration is best-effort; manual capture remains available.
361
+ }
362
+
363
+ const schedulerEnabled = !isPrintModeProcess() && schedulerConfig?.enabled !== false;
364
+ const scheduler = schedulerEnabled
365
+ ? startWikiScheduler({
366
+ wikiDir,
367
+ enabled: true,
368
+ intervalMs: (schedulerConfig?.intervalMinutes ?? 15) * 60_000,
369
+ storeProvider: () => store,
370
+ markDirty: () => markContextDirty(),
371
+ handlers: schedulerHandlers,
372
+ })
373
+ : null;
374
+
375
+ if (scheduler && startupCatchupEnabled) {
376
+ try {
377
+ const stateDb = new StateDB(wikiDir);
378
+ await enqueueCompletedSessionsFromRegistry(scheduler, wikiDir, { stateDb });
379
+ stateDb.close();
380
+ void scheduler.tick();
381
+ } catch {
382
+ // Startup capture catch-up is best-effort.
383
+ }
384
+ }
385
+
386
+ // Check if web wiki server is running (don't start our own yet)
306
387
  if (config.webWiki.enabled) {
307
388
  const port = config.webWiki.port;
308
389
  try {
@@ -312,7 +393,7 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
312
393
  const url = lanIp ? `http://${lanIp}:${port}` : `http://localhost:${port}`;
313
394
  ctx.ui.setStatus("pi-para", `wiki: ${url}`);
314
395
  } else {
315
- ctx.ui.setStatus("pi-para", "wiki: ready (start daemon for web UI)");
396
+ ctx.ui.setStatus("pi-para", "wiki: ready (web UI disabled)");
316
397
  setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
317
398
  }
318
399
  } catch {
@@ -337,7 +418,7 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
337
418
  markContextDirty();
338
419
  });
339
420
 
340
- // -- session_compact: register session for daemon capture on compaction ----
421
+ // -- session_compact: register session for scheduler capture on compaction --
341
422
 
342
423
  pi.on("session_compact", async (_event, ctx) => {
343
424
  // Compaction means the session has accumulated enough content that details
@@ -345,9 +426,7 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
345
426
  const sessionFile = ctx.sessionManager.getSessionFile();
346
427
  if (sessionFile) {
347
428
  try {
348
- const registry = join(wikiDir, ".completed-sessions");
349
- const entry = `${new Date().toISOString()}|${sessionFile}\n`;
350
- await appendFile(registry, entry);
429
+ await appendCompletedSession(wikiDir, sessionFile);
351
430
  ctx.ui.setStatus("pi-para", "wiki: capture queued (compaction)");
352
431
  setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
353
432
  } catch {
@@ -356,22 +435,22 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
356
435
  }
357
436
  });
358
437
 
359
- // -- session_shutdown: register session for daemon, close store ------------
438
+ // -- session_shutdown: register session for scheduler, close store ---------
360
439
 
361
440
  pi.on("session_shutdown", async (_event, ctx) => {
362
- // 1. Register session as completed (for daemon to process later)
441
+ // 1. Register session as completed (scheduler catch-up processes later)
363
442
  const sessionFile = ctx.sessionManager.getSessionFile();
364
443
  if (sessionFile) {
365
444
  try {
366
- const registry = join(wikiDir, ".completed-sessions");
367
- const entry = `${new Date().toISOString()}|${sessionFile}\n`;
368
- await appendFile(registry, entry);
445
+ await appendCompletedSession(wikiDir, sessionFile);
369
446
  } catch {
370
- // Non-fatal — daemon can also discover sessions via filesystem scan
447
+ // Non-fatal — startup catch-up can also discover sessions via filesystem scan
371
448
  }
372
449
  }
373
450
 
374
- // 2. Close the store (instant)
451
+ // 2. Stop scheduler and close the store (instant)
452
+ stopWikiScheduler(wikiDir);
453
+
375
454
  if (store) {
376
455
  try {
377
456
  await closeStore(store);
package/src/maintainer.ts CHANGED
@@ -9,12 +9,12 @@
9
9
  * stay in code. This agent handles everything that needs intelligence.
10
10
  */
11
11
 
12
- import { Agent } from "@mariozechner/pi-agent-core";
13
- import type { AgentTool } from "@mariozechner/pi-agent-core";
14
- import type { Model } from "@mariozechner/pi-ai";
12
+ import { Agent } from "@earendil-works/pi-agent-core";
13
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
14
+ import type { Model } from "@earendil-works/pi-ai";
15
15
  import type { QMDStore } from "qmd-engine";
16
16
  import { Type } from "typebox";
17
- import { StringEnum } from "@mariozechner/pi-ai";
17
+ import { StringEnum } from "@earendil-works/pi-ai";
18
18
 
19
19
  import {
20
20
  readPage,
@@ -0,0 +1,96 @@
1
+ import type { Model } from "@earendil-works/pi-ai";
2
+ import type { ProviderCredentialRef, ParaUserConfig } from "./config.js";
3
+ import { resolveCredentialRef } from "./credentials.js";
4
+
5
+ export interface ModelRegistryLike {
6
+ getAvailable(): Model<any>[];
7
+ find(provider: string, modelId: string): Model<any> | undefined;
8
+ getApiKeyForProvider(provider: string): Promise<string | undefined>;
9
+ }
10
+
11
+ export interface AuthStorageLike {
12
+ getApiKey(provider: string): Promise<string | undefined>;
13
+ }
14
+
15
+ export type ModelSelection = ProviderCredentialRef | "auto";
16
+
17
+ export function parseProviderModelSpec(spec: string): { provider: string; modelId: string } | null {
18
+ const idx = spec.indexOf("/");
19
+ if (idx <= 0 || idx === spec.length - 1) return null;
20
+ return { provider: spec.slice(0, idx), modelId: spec.slice(idx + 1) };
21
+ }
22
+
23
+ export function resolveSelectedModel(
24
+ selection: ModelSelection,
25
+ registry: ModelRegistryLike,
26
+ opts: { legacyModelSpec?: string | null; preferredModelSpec?: string } = {},
27
+ ): Model<any> | null {
28
+ if (selection !== "auto") {
29
+ if (selection.model) return registry.find(selection.provider, selection.model) ?? null;
30
+ return registry.getAvailable().find((model) => model.provider === selection.provider) ?? null;
31
+ }
32
+
33
+ const legacy = opts.legacyModelSpec ? parseProviderModelSpec(opts.legacyModelSpec) : null;
34
+ if (legacy) {
35
+ const model = registry.find(legacy.provider, legacy.modelId);
36
+ if (model) return model;
37
+ }
38
+
39
+ const preferred = opts.preferredModelSpec ? parseProviderModelSpec(opts.preferredModelSpec) : null;
40
+ if (preferred) {
41
+ const model = registry.find(preferred.provider, preferred.modelId);
42
+ if (model) return model;
43
+ }
44
+
45
+ return pickBestAvailableModel(registry.getAvailable());
46
+ }
47
+
48
+ export function pickBestAvailableModel(models: Model<any>[]): Model<any> | null {
49
+ if (models.length === 0) return null;
50
+ const sonnet = models.find((model) => model.provider === "anthropic" && /sonnet/i.test(model.id));
51
+ if (sonnet) return sonnet;
52
+ return [...models].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0))[0] ?? null;
53
+ }
54
+
55
+ export function createModelApiKeyResolver(
56
+ selection: ModelSelection,
57
+ registry: ModelRegistryLike,
58
+ opts: { authStorage?: AuthStorageLike; secretsPath?: string } = {},
59
+ ): (provider: string) => Promise<string | undefined> {
60
+ return async (provider: string) => {
61
+ if (selection === "auto") return registry.getApiKeyForProvider(provider);
62
+
63
+ if (selection.credentialRef.startsWith("pi-auth:")) {
64
+ const configuredProvider = selection.credentialRef.slice("pi-auth:".length);
65
+ if (configuredProvider !== provider && selection.provider !== provider) return undefined;
66
+ return (await opts.authStorage?.getApiKey(provider)) ?? await registry.getApiKeyForProvider(provider);
67
+ }
68
+
69
+ if (selection.credentialRef.startsWith("secret:")) {
70
+ if (selection.provider !== provider) return undefined;
71
+ const resolved = await resolveCredentialRef(selection.credentialRef, { secretsPath: opts.secretsPath });
72
+ return resolved.ok ? resolved.value : undefined;
73
+ }
74
+
75
+ return undefined;
76
+ };
77
+ }
78
+
79
+ export function getCaptureSelection(config: ParaUserConfig): ModelSelection {
80
+ return config.models.capture;
81
+ }
82
+
83
+ export async function createPiModelRegistry(): Promise<{ authStorage: AuthStorageLike; modelRegistry: ModelRegistryLike } | null> {
84
+ try {
85
+ const mod = await import("@earendil-works/pi-coding-agent");
86
+ const typed = mod as unknown as {
87
+ AuthStorage?: { create(): AuthStorageLike };
88
+ ModelRegistry?: { create(authStorage: AuthStorageLike): ModelRegistryLike };
89
+ };
90
+ if (!typed.AuthStorage || !typed.ModelRegistry) return null;
91
+ const authStorage = typed.AuthStorage.create();
92
+ return { authStorage, modelRegistry: typed.ModelRegistry.create(authStorage) };
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
package/src/paths.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+
4
+ export interface ParaPaths {
5
+ homeDir: string;
6
+ agentDir: string;
7
+ paraDir: string;
8
+ wikiDir: string;
9
+ userConfigPath: string;
10
+ secretsPath: string;
11
+ schedulerDbPath: string;
12
+ qmdDbPath: string;
13
+ legacyConfigPath: string;
14
+ legacyQmdConfigPath: string;
15
+ piSettingsPath: string;
16
+ }
17
+
18
+ export function expandHome(path: string, homeDir = homedir()): string {
19
+ if (path === "~") return homeDir;
20
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(homeDir, path.slice(2));
21
+ return path;
22
+ }
23
+
24
+ export function resolvePath(path: string, baseDir = process.cwd(), homeDir = homedir()): string {
25
+ const expanded = expandHome(path, homeDir);
26
+ return isAbsolute(expanded) ? expanded : resolve(baseDir, expanded);
27
+ }
28
+
29
+ export function getParaPaths(opts: { homeDir?: string; wikiDir?: string } = {}): ParaPaths {
30
+ const homeDir = opts.homeDir ?? homedir();
31
+ const agentDir = join(homeDir, ".pi", "agent");
32
+ const paraDir = join(homeDir, ".pi", "para");
33
+ const wikiDir = opts.wikiDir ? resolvePath(opts.wikiDir, process.cwd(), homeDir) : join(homeDir, ".pi", "wiki");
34
+
35
+ return {
36
+ homeDir,
37
+ agentDir,
38
+ paraDir,
39
+ wikiDir,
40
+ userConfigPath: join(paraDir, "config.jsonc"),
41
+ secretsPath: join(paraDir, "secrets.json"),
42
+ schedulerDbPath: join(wikiDir, ".pi-para.sqlite"),
43
+ qmdDbPath: join(wikiDir, ".qmd.sqlite"),
44
+ legacyConfigPath: join(wikiDir, "config.json"),
45
+ legacyQmdConfigPath: join(homeDir, ".config", "qmd", "index.yml"),
46
+ piSettingsPath: join(agentDir, "settings.json"),
47
+ };
48
+ }
package/src/processor.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  * to extract knowledge and write wiki pages.
4
4
  */
5
5
 
6
- import { Agent } from "@mariozechner/pi-agent-core";
7
- import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
8
- import type { Model } from "@mariozechner/pi-ai";
6
+ import { Agent } from "@earendil-works/pi-agent-core";
7
+ import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core";
8
+ import type { Model } from "@earendil-works/pi-ai";
9
9
  import type { QMDStore } from "qmd-engine";
10
10
 
11
11
  import {
@@ -25,7 +25,7 @@ import { redactSecrets } from "./redact.js";
25
25
  import { reindex, searchWiki } from "./store.js";
26
26
  import { appendSessionDigest } from "./raw.js";
27
27
  import { Type } from "typebox";
28
- import { StringEnum } from "@mariozechner/pi-ai";
28
+ import { StringEnum } from "@earendil-works/pi-ai";
29
29
 
30
30
  import { loadSession, createSessionTools } from "./session-tools.js";
31
31
  import type { LoadedSession } from "./session-tools.js";