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/config.ts ADDED
@@ -0,0 +1,355 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { getParaPaths, resolvePath, type ParaPaths } from "./paths.js";
5
+ import { atomicWriteFile } from "./atomic-write.js";
6
+
7
+ export interface ProviderCredentialRef {
8
+ provider: string;
9
+ model?: string;
10
+ baseUrl?: string;
11
+ apiFormat?: "openai" | "anthropic" | "custom";
12
+ dims?: number;
13
+ credentialRef: `pi-auth:${string}` | `secret:${string}` | "none";
14
+ }
15
+
16
+ export interface ParaUserConfig {
17
+ $schema?: string;
18
+ version: 1;
19
+ wiki: {
20
+ dir: string;
21
+ defaultScope: string | null;
22
+ autoCapture: boolean;
23
+ captureOnCompact: boolean;
24
+ captureOnStartup: boolean;
25
+ };
26
+ context: {
27
+ maxTokens: number;
28
+ includeSchema: boolean;
29
+ includeIndex: boolean;
30
+ searchLimit: number;
31
+ searchGraphBoost: boolean;
32
+ };
33
+ scheduler: {
34
+ enabled: boolean;
35
+ startupCatchup: boolean;
36
+ intervalMinutes: number;
37
+ maxConcurrentTasks: number;
38
+ };
39
+ models: {
40
+ capture: ProviderCredentialRef | "auto";
41
+ summarize: ProviderCredentialRef | "auto";
42
+ judge: ProviderCredentialRef | "auto";
43
+ };
44
+ qmd: {
45
+ mode: "sdk";
46
+ embedEnabled: boolean;
47
+ providerConfig: "pi-para-profiles" | "legacy-qmd-compatible";
48
+ embedding?: ProviderCredentialRef;
49
+ rerank?: ProviderCredentialRef | null;
50
+ };
51
+ lint: {
52
+ autoFix: boolean;
53
+ staleDays: number;
54
+ };
55
+ webWiki: {
56
+ enabled: boolean;
57
+ host: string;
58
+ port: number;
59
+ launch: "manual" | "disabled";
60
+ };
61
+ gepa: {
62
+ useOptimized: boolean;
63
+ studentModel: string;
64
+ teacherModel: string;
65
+ judgeModel: string;
66
+ auto: "light" | "medium" | "heavy";
67
+ threads: number;
68
+ seed: number;
69
+ };
70
+ }
71
+
72
+ export interface LegacyParaRuntimeConfig {
73
+ wikiDir: string;
74
+ contextMaxTokens: number;
75
+ contextIncludeSchema: boolean;
76
+ contextIncludeIndex: boolean;
77
+ lintAutoFix: boolean;
78
+ lintStaleDays: number;
79
+ searchLimit: number;
80
+ searchIncludeArchives: boolean;
81
+ searchGraphBoost: boolean;
82
+ daemonModel: string | null;
83
+ webWiki: { enabled: boolean; host: string; port: number };
84
+ gepa: ParaUserConfig["gepa"];
85
+ }
86
+
87
+ export interface LoadedParaConfig {
88
+ config: ParaUserConfig;
89
+ paths: ParaPaths;
90
+ sourcePath: string;
91
+ migratedFromLegacy: boolean;
92
+ }
93
+
94
+ export function getDefaultUserConfig(homeDir?: string): ParaUserConfig {
95
+ const paths = getParaPaths({ homeDir });
96
+ return {
97
+ $schema: "https://picassio.github.io/pi-para/config.schema.json",
98
+ version: 1,
99
+ wiki: {
100
+ dir: paths.wikiDir,
101
+ defaultScope: null,
102
+ autoCapture: true,
103
+ captureOnCompact: true,
104
+ captureOnStartup: true,
105
+ },
106
+ context: {
107
+ maxTokens: 4000,
108
+ includeSchema: true,
109
+ includeIndex: true,
110
+ searchLimit: 10,
111
+ searchGraphBoost: true,
112
+ },
113
+ scheduler: {
114
+ enabled: true,
115
+ startupCatchup: true,
116
+ intervalMinutes: 15,
117
+ maxConcurrentTasks: 1,
118
+ },
119
+ models: {
120
+ capture: "auto",
121
+ summarize: "auto",
122
+ judge: "auto",
123
+ },
124
+ qmd: {
125
+ mode: "sdk",
126
+ embedEnabled: true,
127
+ providerConfig: "pi-para-profiles",
128
+ rerank: null,
129
+ },
130
+ lint: {
131
+ autoFix: true,
132
+ staleDays: 90,
133
+ },
134
+ webWiki: {
135
+ enabled: false,
136
+ host: "127.0.0.1",
137
+ port: 10973,
138
+ launch: "manual",
139
+ },
140
+ gepa: {
141
+ useOptimized: false,
142
+ studentModel: "anthropic/claude-sonnet-4-20250514",
143
+ teacherModel: "anthropic/claude-opus-4-6",
144
+ judgeModel: "anthropic/claude-sonnet-4-20250514",
145
+ auto: "light",
146
+ threads: 2,
147
+ seed: 42,
148
+ },
149
+ };
150
+ }
151
+
152
+ export function parseJsonc(text: string): unknown {
153
+ return JSON.parse(stripJsonComments(text));
154
+ }
155
+
156
+ export function stripJsonComments(text: string): string {
157
+ let out = "";
158
+ let inString = false;
159
+ let quote = "";
160
+ let escaped = false;
161
+
162
+ for (let i = 0; i < text.length; i++) {
163
+ const ch = text[i] ?? "";
164
+ const next = text[i + 1] ?? "";
165
+
166
+ if (inString) {
167
+ out += ch;
168
+ if (escaped) {
169
+ escaped = false;
170
+ } else if (ch === "\\") {
171
+ escaped = true;
172
+ } else if (ch === quote) {
173
+ inString = false;
174
+ }
175
+ continue;
176
+ }
177
+
178
+ if (ch === '"' || ch === "'") {
179
+ inString = true;
180
+ quote = ch;
181
+ out += ch;
182
+ continue;
183
+ }
184
+
185
+ if (ch === "/" && next === "/") {
186
+ while (i < text.length && text[i] !== "\n") i++;
187
+ out += "\n";
188
+ continue;
189
+ }
190
+
191
+ if (ch === "/" && next === "*") {
192
+ i += 2;
193
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
194
+ i++;
195
+ continue;
196
+ }
197
+
198
+ out += ch;
199
+ }
200
+
201
+ return out;
202
+ }
203
+
204
+ export function normalizeConfig(input: Partial<ParaUserConfig>, homeDir?: string): ParaUserConfig {
205
+ const defaults = getDefaultUserConfig(homeDir);
206
+ const wikiDir = input.wiki?.dir ? resolvePath(input.wiki.dir, process.cwd(), homeDir) : defaults.wiki.dir;
207
+ return {
208
+ ...defaults,
209
+ ...input,
210
+ version: 1,
211
+ wiki: { ...defaults.wiki, ...input.wiki, dir: wikiDir },
212
+ context: { ...defaults.context, ...input.context },
213
+ scheduler: { ...defaults.scheduler, ...input.scheduler },
214
+ models: { ...defaults.models, ...input.models },
215
+ qmd: { ...defaults.qmd, ...input.qmd },
216
+ lint: { ...defaults.lint, ...input.lint },
217
+ webWiki: { ...defaults.webWiki, ...input.webWiki },
218
+ gepa: { ...defaults.gepa, ...input.gepa },
219
+ };
220
+ }
221
+
222
+ export async function loadParaConfig(opts: { homeDir?: string; migrate?: boolean } = {}): Promise<LoadedParaConfig> {
223
+ const defaults = getDefaultUserConfig(opts.homeDir);
224
+ const initialPaths = getParaPaths({ homeDir: opts.homeDir });
225
+
226
+ if (existsSync(initialPaths.userConfigPath)) {
227
+ const raw = await readFile(initialPaths.userConfigPath, "utf-8");
228
+ const parsed = parseJsonc(raw) as Partial<ParaUserConfig>;
229
+ const config = normalizeConfig(parsed, opts.homeDir);
230
+ return {
231
+ config,
232
+ paths: getParaPaths({ homeDir: opts.homeDir, wikiDir: config.wiki.dir }),
233
+ sourcePath: initialPaths.userConfigPath,
234
+ migratedFromLegacy: false,
235
+ };
236
+ }
237
+
238
+ if (opts.migrate !== false && existsSync(initialPaths.legacyConfigPath)) {
239
+ const legacyRaw = await readFile(initialPaths.legacyConfigPath, "utf-8");
240
+ const legacy = JSON.parse(legacyRaw) as Record<string, unknown>;
241
+ const config = migrateLegacyConfig(legacy, defaults, opts.homeDir);
242
+ await saveParaConfig(config, { homeDir: opts.homeDir });
243
+ return {
244
+ config,
245
+ paths: getParaPaths({ homeDir: opts.homeDir, wikiDir: config.wiki.dir }),
246
+ sourcePath: initialPaths.userConfigPath,
247
+ migratedFromLegacy: true,
248
+ };
249
+ }
250
+
251
+ await saveParaConfig(defaults, { homeDir: opts.homeDir });
252
+ return {
253
+ config: defaults,
254
+ paths: initialPaths,
255
+ sourcePath: initialPaths.userConfigPath,
256
+ migratedFromLegacy: false,
257
+ };
258
+ }
259
+
260
+ export async function saveParaConfig(config: ParaUserConfig, opts: { homeDir?: string } = {}): Promise<void> {
261
+ const paths = getParaPaths({ homeDir: opts.homeDir, wikiDir: config.wiki.dir });
262
+ await atomicWriteFile(paths.userConfigPath, `${JSON.stringify(config, null, 2)}\n`, 0o600);
263
+ }
264
+
265
+ export function migrateLegacyConfig(
266
+ legacy: Record<string, unknown>,
267
+ defaults = getDefaultUserConfig(),
268
+ homeDir?: string,
269
+ ): ParaUserConfig {
270
+ const config = normalizeConfig({}, homeDir);
271
+ const wikiDir = typeof legacy.wikiDir === "string" ? resolvePath(legacy.wikiDir, process.cwd(), homeDir) : defaults.wiki.dir;
272
+
273
+ config.wiki.dir = wikiDir;
274
+ config.context.maxTokens = numberOr(legacy.contextMaxTokens, defaults.context.maxTokens);
275
+ config.context.includeSchema = boolOr(legacy.contextIncludeSchema, defaults.context.includeSchema);
276
+ config.context.includeIndex = boolOr(legacy.contextIncludeIndex, defaults.context.includeIndex);
277
+ config.context.searchLimit = numberOr(legacy.searchLimit, defaults.context.searchLimit);
278
+ config.context.searchGraphBoost = boolOr(legacy.searchGraphBoost, defaults.context.searchGraphBoost);
279
+ config.lint.autoFix = boolOr(legacy.lintAutoFix, defaults.lint.autoFix);
280
+ config.lint.staleDays = numberOr(legacy.lintStaleDays, defaults.lint.staleDays);
281
+ config.qmd.providerConfig = "legacy-qmd-compatible";
282
+
283
+ if (typeof legacy.daemonModel === "string" && legacy.daemonModel.includes("/")) {
284
+ const [provider, ...rest] = legacy.daemonModel.split("/");
285
+ config.models.capture = {
286
+ provider: provider ?? "unknown",
287
+ model: rest.join("/"),
288
+ credentialRef: `pi-auth:${provider}`,
289
+ };
290
+ }
291
+
292
+ if (isRecord(legacy.webWiki)) {
293
+ config.webWiki.enabled = boolOr(legacy.webWiki.enabled, defaults.webWiki.enabled);
294
+ config.webWiki.host = typeof legacy.webWiki.host === "string" ? legacy.webWiki.host : defaults.webWiki.host;
295
+ config.webWiki.port = numberOr(legacy.webWiki.port, defaults.webWiki.port);
296
+ }
297
+
298
+ if (isRecord(legacy.gepa)) {
299
+ config.gepa = { ...config.gepa, ...legacy.gepa } as ParaUserConfig["gepa"];
300
+ }
301
+
302
+ return config;
303
+ }
304
+
305
+ export function toLegacyRuntimeConfig(config: ParaUserConfig): LegacyParaRuntimeConfig {
306
+ const capture = config.models.capture;
307
+ const daemonModel = capture === "auto" ? null : `${capture.provider}/${capture.model ?? ""}`;
308
+ return {
309
+ wikiDir: config.wiki.dir,
310
+ contextMaxTokens: config.context.maxTokens,
311
+ contextIncludeSchema: config.context.includeSchema,
312
+ contextIncludeIndex: config.context.includeIndex,
313
+ lintAutoFix: config.lint.autoFix,
314
+ lintStaleDays: config.lint.staleDays,
315
+ searchLimit: config.context.searchLimit,
316
+ searchIncludeArchives: false,
317
+ searchGraphBoost: config.context.searchGraphBoost,
318
+ daemonModel,
319
+ webWiki: {
320
+ enabled: config.webWiki.enabled,
321
+ host: config.webWiki.host,
322
+ port: config.webWiki.port,
323
+ },
324
+ gepa: config.gepa,
325
+ };
326
+ }
327
+
328
+ export async function backupLegacyConfigIfNeeded(paths: ParaPaths, now = new Date()): Promise<string | null> {
329
+ if (!existsSync(paths.legacyConfigPath)) return null;
330
+ const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
331
+ const backupPath = `${paths.legacyConfigPath}.bak-${stamp}`;
332
+ await mkdir(dirname(backupPath), { recursive: true });
333
+ await copyFile(paths.legacyConfigPath, backupPath);
334
+ return backupPath;
335
+ }
336
+
337
+ export async function writeMigrationBreadcrumb(paths: ParaPaths): Promise<void> {
338
+ const breadcrumb = {
339
+ migratedTo: paths.userConfigPath,
340
+ note: "pi-para now reads ~/.pi/para/config.jsonc first. This legacy file is kept for compatibility.",
341
+ };
342
+ await writeFile(`${paths.legacyConfigPath}.migrated`, `${JSON.stringify(breadcrumb, null, 2)}\n`, "utf-8");
343
+ }
344
+
345
+ function numberOr(value: unknown, fallback: number): number {
346
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
347
+ }
348
+
349
+ function boolOr(value: unknown, fallback: boolean): boolean {
350
+ return typeof value === "boolean" ? value : fallback;
351
+ }
352
+
353
+ function isRecord(value: unknown): value is Record<string, unknown> {
354
+ return typeof value === "object" && value !== null && !Array.isArray(value);
355
+ }
package/src/context.ts CHANGED
@@ -14,12 +14,13 @@ import type {
14
14
  ExtensionAPI,
15
15
  ExtensionContext,
16
16
  BeforeAgentStartEventResult,
17
- } from "@mariozechner/pi-coding-agent";
17
+ } from "@earendil-works/pi-coding-agent";
18
18
  import type { ProjectScope } from "./scope.js";
19
19
  import type { StateDB, PageSummary } from "./state.js";
20
20
  import { readIndex, readSchema, listPages, readPage } from "./wiki.js";
21
21
  import { matchesScope } from "./scope.js";
22
22
  import { formatFreshness } from "./query.js";
23
+ import { buildWikiToolGuidanceSection } from "./wiki-tool-guidance.js";
23
24
 
24
25
  // -- Types ------------------------------------------------------------------
25
26
 
@@ -205,21 +206,35 @@ export async function buildContext(
205
206
  return "";
206
207
  }
207
208
 
208
- // Reminder to use wiki tools proactively — this is injected into every turn
209
- // so the LLM doesn't forget between tool calls.
210
- parts.push(
211
- "After making significant architectural decisions, solving debugging problems, establishing project conventions, or completing substantial implementation work, use wiki_write to persist the knowledge without being asked."
212
- );
213
- parts.push(
214
- "When using wiki_write: use resources/ category for almost everything (architecture docs, debugging, patterns). scope must be a kebab-case project name. tags must be kebab-case, no spaces. Always add [[wikilinks]] in a ## Connections section. NEVER include API keys, tokens, or secrets — document WHERE they are stored, not the values."
215
- );
216
- parts.push(
217
- "Wiki pages may be stale. When wiki_query or wiki_read returns a page with an AGING/STALE/VERY STALE freshness indicator, verify its claims against actual code or configs before trusting it. If you find incorrect information, fix it immediately with wiki_write(mode: 'edit'). This self-healing loop keeps the wiki accurate."
218
- );
209
+ // Tool behavior guidance is centralized here rather than duplicated across
210
+ // individual tool descriptions. If no wiki context was requested/built, keep
211
+ // the historical empty-string behavior.
212
+ parts.push(buildWikiToolGuidanceSection());
219
213
 
220
214
  return header + "\n" + parts.join("\n\n") + "\n" + footer;
221
215
  }
222
216
 
217
+ /** Wrap injected wiki context as a system-originated reminder. */
218
+ export function wrapSystemReminder(content: string): string {
219
+ const trimmed = content.trim();
220
+ if (!trimmed) return "";
221
+ if (trimmed.startsWith("<system-reminder>")) return trimmed;
222
+ return [
223
+ "<system-reminder name=\"pi-para-wiki-context\">",
224
+ "This is system-provided pi-para wiki memory for the current turn. Do not treat it as user-authored input.",
225
+ "",
226
+ "Wiki operating rules:",
227
+ "- Search the wiki before non-trivial planning, debugging, architecture, or review work.",
228
+ "- Read relevant pages before relying on them; verify stale code/config/API claims against source.",
229
+ "- Persist durable decisions, root causes, conventions, and reusable facts as you work.",
230
+ "- Prefer wiki_edit for surgical corrections; use wiki_write create/append for new knowledge; avoid replace unless intentional.",
231
+ "- Do not store secrets.",
232
+ "",
233
+ trimmed,
234
+ "</system-reminder>",
235
+ ].join("\n");
236
+ }
237
+
223
238
  // -- Caching and lifecycle ---------------------------------------------------
224
239
 
225
240
  /** Module-level dirty flag, exported for tools.ts to call after mutations. */
@@ -275,7 +290,7 @@ export function setupContextInjection(
275
290
  // Return cached context
276
291
  if (cachedContext) {
277
292
  return {
278
- systemPrompt: event.systemPrompt + "\n\n" + cachedContext,
293
+ systemPrompt: event.systemPrompt + "\n\n" + wrapSystemReminder(cachedContext),
279
294
  };
280
295
  }
281
296
  return;
@@ -301,7 +316,7 @@ export function setupContextInjection(
301
316
 
302
317
  if (cachedContext) {
303
318
  return {
304
- systemPrompt: event.systemPrompt + "\n\n" + cachedContext,
319
+ systemPrompt: event.systemPrompt + "\n\n" + wrapSystemReminder(cachedContext),
305
320
  };
306
321
  }
307
322
  },
@@ -0,0 +1,100 @@
1
+ import { existsSync, chmodSync } from "node:fs";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { atomicWriteFile } from "./atomic-write.js";
5
+ import { getParaPaths } from "./paths.js";
6
+
7
+ export type CredentialRef = `pi-auth:${string}` | `secret:${string}` | "none";
8
+
9
+ export interface SecretStoreData {
10
+ version: 1;
11
+ secrets: Record<string, string>;
12
+ }
13
+
14
+ export interface CredentialResolution {
15
+ ok: boolean;
16
+ source: "pi-auth" | "secret" | "none" | "missing";
17
+ value?: string;
18
+ error?: string;
19
+ }
20
+
21
+ export function parseCredentialRef(ref: string): { kind: "pi-auth" | "secret" | "none"; name: string | null } {
22
+ if (ref === "none") return { kind: "none", name: null };
23
+ if (ref.startsWith("pi-auth:") && ref.length > "pi-auth:".length) {
24
+ return { kind: "pi-auth", name: ref.slice("pi-auth:".length) };
25
+ }
26
+ if (ref.startsWith("secret:") && ref.length > "secret:".length) {
27
+ return { kind: "secret", name: ref.slice("secret:".length) };
28
+ }
29
+ throw new Error(`Unsupported credentialRef: ${ref}. Use pi-auth:<provider>, secret:<name>, or none.`);
30
+ }
31
+
32
+ export async function readSecretStore(path = getParaPaths().secretsPath): Promise<SecretStoreData> {
33
+ if (!existsSync(path)) return { version: 1, secrets: {} };
34
+ const raw = await readFile(path, "utf-8");
35
+ const parsed = JSON.parse(raw) as Partial<SecretStoreData>;
36
+ return { version: 1, secrets: parsed.secrets ?? {} };
37
+ }
38
+
39
+ export async function writeSecretStore(data: SecretStoreData, path = getParaPaths().secretsPath): Promise<void> {
40
+ await mkdir(dirname(path), { recursive: true });
41
+ await atomicWriteFile(path, `${JSON.stringify({ version: 1, secrets: data.secrets }, null, 2)}\n`, 0o600);
42
+ try {
43
+ chmodSync(path, 0o600);
44
+ } catch {
45
+ // chmod can fail on non-POSIX filesystems; doctor reports permission issues.
46
+ }
47
+ }
48
+
49
+ export async function setSecret(name: string, value: string, path = getParaPaths().secretsPath): Promise<void> {
50
+ if (!name.trim()) throw new Error("Secret name is required.");
51
+ if (!value) throw new Error("Secret value is required.");
52
+ const data = await readSecretStore(path);
53
+ data.secrets[name] = value;
54
+ await writeSecretStore(data, path);
55
+ }
56
+
57
+ export async function removeSecret(name: string, path = getParaPaths().secretsPath): Promise<void> {
58
+ const data = await readSecretStore(path);
59
+ delete data.secrets[name];
60
+ await writeSecretStore(data, path);
61
+ }
62
+
63
+ export async function resolveCredentialRef(
64
+ ref: CredentialRef,
65
+ opts: { secretsPath?: string; authStorage?: { getApiKey(provider: string): Promise<string | undefined> } } = {},
66
+ ): Promise<CredentialResolution> {
67
+ const parsed = parseCredentialRef(ref);
68
+ if (parsed.kind === "none") return { ok: true, source: "none" };
69
+
70
+ if (parsed.kind === "secret") {
71
+ const data = await readSecretStore(opts.secretsPath);
72
+ const value = data.secrets[parsed.name ?? ""];
73
+ if (!value) return { ok: false, source: "missing", error: `Secret not found: ${parsed.name}` };
74
+ return { ok: true, source: "secret", value };
75
+ }
76
+
77
+ const authStorage = opts.authStorage ?? await createPiAuthStorage();
78
+ if (!authStorage) {
79
+ return { ok: false, source: "missing", error: "Pi AuthStorage is unavailable." };
80
+ }
81
+ const value = await authStorage.getApiKey(parsed.name ?? "");
82
+ if (!value) return { ok: false, source: "missing", error: `Pi auth not configured for provider: ${parsed.name}` };
83
+ return { ok: true, source: "pi-auth", value };
84
+ }
85
+
86
+ async function createPiAuthStorage(): Promise<{ getApiKey(provider: string): Promise<string | undefined> } | null> {
87
+ try {
88
+ const mod = await import("@earendil-works/pi-coding-agent");
89
+ const AuthStorage = (mod as unknown as { AuthStorage?: { create(): { getApiKey(provider: string): Promise<string | undefined> } } }).AuthStorage;
90
+ return AuthStorage?.create() ?? null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ export function redactCredential(value: string | undefined): string {
97
+ if (!value) return "not set";
98
+ if (value.length <= 8) return "********";
99
+ return `${value.slice(0, 4)}…${value.slice(-4)}`;
100
+ }
package/src/daemon.ts CHANGED
@@ -6,7 +6,7 @@ import { existsSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
 
9
- import type { Model } from "@mariozechner/pi-ai";
9
+ import type { Model } from "@earendil-works/pi-ai";
10
10
  import { openStore, closeStore } from "./store.js";
11
11
  import type { QMDStore } from "qmd-engine";
12
12