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.
- package/ARCHITECTURE.md +160 -0
- package/CHANGELOG.md +51 -0
- package/CONFIGURATION.md +243 -0
- package/README.md +140 -237
- package/SETUP.md +196 -112
- package/STRUCTURE.md +140 -0
- package/package.json +86 -18
- package/scripts/build-assets.mjs +29 -0
- package/scripts/install.ps1 +55 -0
- package/scripts/install.sh +63 -0
- package/scripts/smoke-install.mjs +63 -0
- package/skills/para/SKILL.md +1 -1
- package/skills/setup/SKILL.md +1 -1
- package/src/atomic-write.ts +17 -0
- package/src/capture.ts +4 -4
- package/src/cli-legacy.ts +43 -0
- package/src/cli.ts +133 -28
- package/src/commands.ts +203 -243
- package/src/config.ts +355 -0
- package/src/context.ts +29 -14
- package/src/credentials.ts +100 -0
- package/src/daemon.ts +1 -1
- package/src/doctor.ts +209 -0
- package/src/gepa/index.ts +1 -1
- package/src/index.ts +119 -40
- package/src/maintainer.ts +4 -4
- package/src/model-resolver.ts +96 -0
- package/src/paths.ts +48 -0
- package/src/processor.ts +4 -4
- package/src/qmd-providers.ts +63 -0
- package/src/query.ts +15 -1
- package/src/repair.ts +49 -0
- package/src/scheduler/controls.ts +125 -0
- package/src/scheduler/index.ts +210 -0
- package/src/scheduler/leases.ts +98 -0
- package/src/scheduler/session-capture.ts +146 -0
- package/src/scheduler/state.ts +304 -0
- package/src/session-tools.ts +1 -1
- package/src/settings.ts +126 -0
- package/src/setup.ts +116 -0
- package/src/status.ts +115 -0
- package/src/store.ts +116 -31
- package/src/summarize.ts +72 -16
- package/src/templates/prompts.ts +1 -1
- package/src/tools.ts +280 -90
- package/src/wiki-tool-guidance.ts +84 -0
- package/src/wiki.ts +98 -26
- package/dist/capture.d.ts +0 -34
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js +0 -461
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -405
- package/dist/cli.js.map +0 -1
- package/dist/commands.d.ts +0 -15
- package/dist/commands.d.ts.map +0 -1
- package/dist/commands.js +0 -786
- package/dist/commands.js.map +0 -1
- package/dist/context.d.ts +0 -61
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -289
- package/dist/context.js.map +0 -1
- package/dist/daemon.d.ts +0 -49
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -226
- package/dist/daemon.js.map +0 -1
- package/dist/frontmatter.d.ts +0 -59
- package/dist/frontmatter.d.ts.map +0 -1
- package/dist/frontmatter.js +0 -170
- package/dist/frontmatter.js.map +0 -1
- package/dist/gepa/dataset.d.ts +0 -36
- package/dist/gepa/dataset.d.ts.map +0 -1
- package/dist/gepa/dataset.js +0 -148
- package/dist/gepa/dataset.js.map +0 -1
- package/dist/gepa/evaluator.d.ts +0 -51
- package/dist/gepa/evaluator.d.ts.map +0 -1
- package/dist/gepa/evaluator.js +0 -317
- package/dist/gepa/evaluator.js.map +0 -1
- package/dist/gepa/index.d.ts +0 -66
- package/dist/gepa/index.d.ts.map +0 -1
- package/dist/gepa/index.js +0 -240
- package/dist/gepa/index.js.map +0 -1
- package/dist/gepa/llm.d.ts +0 -35
- package/dist/gepa/llm.d.ts.map +0 -1
- package/dist/gepa/llm.js +0 -268
- package/dist/gepa/llm.js.map +0 -1
- package/dist/gepa/optimized/capture-prompt.txt +0 -65
- package/dist/gepa/optimizer.d.ts +0 -29
- package/dist/gepa/optimizer.d.ts.map +0 -1
- package/dist/gepa/optimizer.js +0 -249
- package/dist/gepa/optimizer.js.map +0 -1
- package/dist/gepa/targets.d.ts +0 -39
- package/dist/gepa/targets.d.ts.map +0 -1
- package/dist/gepa/targets.js +0 -326
- package/dist/gepa/targets.js.map +0 -1
- package/dist/gepa/types.d.ts +0 -168
- package/dist/gepa/types.d.ts.map +0 -1
- package/dist/gepa/types.js +0 -124
- package/dist/gepa/types.js.map +0 -1
- package/dist/index.d.ts +0 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -294
- package/dist/index.js.map +0 -1
- package/dist/ingest.d.ts +0 -88
- package/dist/ingest.d.ts.map +0 -1
- package/dist/ingest.js +0 -105
- package/dist/ingest.js.map +0 -1
- package/dist/link-discovery.d.ts +0 -40
- package/dist/link-discovery.d.ts.map +0 -1
- package/dist/link-discovery.js +0 -122
- package/dist/link-discovery.js.map +0 -1
- package/dist/link-utils.d.ts +0 -30
- package/dist/link-utils.d.ts.map +0 -1
- package/dist/link-utils.js +0 -145
- package/dist/link-utils.js.map +0 -1
- package/dist/lint.d.ts +0 -39
- package/dist/lint.d.ts.map +0 -1
- package/dist/lint.js +0 -827
- package/dist/lint.js.map +0 -1
- package/dist/maintainer.d.ts +0 -24
- package/dist/maintainer.d.ts.map +0 -1
- package/dist/maintainer.js +0 -358
- package/dist/maintainer.js.map +0 -1
- package/dist/processor.d.ts +0 -14
- package/dist/processor.d.ts.map +0 -1
- package/dist/processor.js +0 -361
- package/dist/processor.js.map +0 -1
- package/dist/query.d.ts +0 -69
- package/dist/query.d.ts.map +0 -1
- package/dist/query.js +0 -107
- package/dist/query.js.map +0 -1
- package/dist/raw.d.ts +0 -57
- package/dist/raw.d.ts.map +0 -1
- package/dist/raw.js +0 -219
- package/dist/raw.js.map +0 -1
- package/dist/redact.d.ts +0 -21
- package/dist/redact.d.ts.map +0 -1
- package/dist/redact.js +0 -143
- package/dist/redact.js.map +0 -1
- package/dist/scope.d.ts +0 -54
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -161
- package/dist/scope.js.map +0 -1
- package/dist/scripts/bulk-link-fix.d.ts +0 -9
- package/dist/scripts/bulk-link-fix.d.ts.map +0 -1
- package/dist/scripts/bulk-link-fix.js +0 -106
- package/dist/scripts/bulk-link-fix.js.map +0 -1
- package/dist/session-tools.d.ts +0 -33
- package/dist/session-tools.d.ts.map +0 -1
- package/dist/session-tools.js +0 -211
- package/dist/session-tools.js.map +0 -1
- package/dist/state.d.ts +0 -54
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -176
- package/dist/state.js.map +0 -1
- package/dist/store.d.ts +0 -56
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -227
- package/dist/store.js.map +0 -1
- package/dist/summarize.d.ts +0 -56
- package/dist/summarize.d.ts.map +0 -1
- package/dist/summarize.js +0 -231
- package/dist/summarize.js.map +0 -1
- package/dist/tag-registry.d.ts +0 -56
- package/dist/tag-registry.d.ts.map +0 -1
- package/dist/tag-registry.js +0 -146
- package/dist/tag-registry.js.map +0 -1
- package/dist/templates/prompts.d.ts +0 -63
- package/dist/templates/prompts.d.ts.map +0 -1
- package/dist/templates/prompts.js +0 -260
- package/dist/templates/prompts.js.map +0 -1
- package/dist/tools.d.ts +0 -26
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js +0 -1033
- package/dist/tools.js.map +0 -1
- package/dist/watcher.d.ts +0 -25
- package/dist/watcher.d.ts.map +0 -1
- package/dist/watcher.js +0 -84
- package/dist/watcher.js.map +0 -1
- package/dist/webui/client/dist/index.html +0 -1885
- package/dist/webui/server.d.ts +0 -31
- package/dist/webui/server.d.ts.map +0 -1
- package/dist/webui/server.js +0 -566
- package/dist/webui/server.js.map +0 -1
- package/dist/wiki.d.ts +0 -103
- package/dist/wiki.d.ts.map +0 -1
- package/dist/wiki.js +0 -428
- package/dist/wiki.js.map +0 -1
- package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
- 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 "@
|
|
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
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
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 "@
|
|
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
|
|