pi-para 0.6.4 → 0.6.6
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/CHANGELOG.md +26 -0
- package/CONFIGURATION.md +1 -1
- package/README.md +1 -1
- package/SETUP.md +2 -2
- package/STRUCTURE.md +1 -1
- package/dist/cli.d.ts +23 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -83
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +12 -5
- package/dist/commands.js.map +1 -1
- package/dist/credentials.d.ts +2 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +6 -14
- package/dist/credentials.js.map +1 -1
- package/dist/doctor.js +19 -18
- package/dist/doctor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -11
- package/dist/index.js.map +1 -1
- package/dist/link-utils.d.ts +4 -12
- package/dist/link-utils.d.ts.map +1 -1
- package/dist/link-utils.js +35 -16
- package/dist/link-utils.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +51 -35
- package/dist/lint.js.map +1 -1
- package/dist/model-resolver.d.ts +13 -6
- package/dist/model-resolver.d.ts.map +1 -1
- package/dist/model-resolver.js +35 -19
- package/dist/model-resolver.js.map +1 -1
- package/dist/tools.d.ts +4 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +11 -5
- package/dist/tools.js.map +1 -1
- package/package.json +9 -9
- package/src/cli.ts +69 -81
- package/src/commands.ts +15 -6
- package/src/credentials.ts +7 -14
- package/src/doctor.ts +18 -16
- package/src/index.ts +20 -11
- package/src/link-utils.ts +40 -16
- package/src/lint.ts +49 -33
- package/src/model-resolver.ts +45 -20
- package/src/tools.ts +16 -4
package/src/doctor.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { readSecretStore, resolveCredentialRef } from "./credentials.js";
|
|
|
5
5
|
import { getParaPaths } from "./paths.js";
|
|
6
6
|
import { openStore, closeStore } from "./store.js";
|
|
7
7
|
import type { ProviderCredentialRef } from "./config.js";
|
|
8
|
-
import {
|
|
8
|
+
import { createModelApiKeyResolver, createPiModelServices, getCaptureSelection, resolveSelectedModel } from "./model-resolver.js";
|
|
9
9
|
import { SchedulerStateDB } from "./scheduler/state.js";
|
|
10
10
|
import { ensureGeneratedStateGitignore, fixSecretPermissions, missingGeneratedStateGitignorePatterns } from "./repair.js";
|
|
11
11
|
|
|
@@ -51,7 +51,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<DoctorResu
|
|
|
51
51
|
|
|
52
52
|
checks.push(await checkSecrets(paths.secretsPath));
|
|
53
53
|
checks.push(await checkGeneratedStateGitignore(paths.wikiDir));
|
|
54
|
-
checks.push(...await checkProviderDiagnostics(loaded.config, paths.secretsPath, options));
|
|
54
|
+
checks.push(...await checkProviderDiagnostics(loaded.config, paths.secretsPath, `${paths.agentDir}/auth.json`, options));
|
|
55
55
|
|
|
56
56
|
if (options.validateQmd !== false) {
|
|
57
57
|
checks.push(await checkQmd(paths.wikiDir, loaded.config, paths.secretsPath));
|
|
@@ -131,6 +131,7 @@ async function checkGeneratedStateGitignore(wikiDir: string): Promise<DoctorChec
|
|
|
131
131
|
async function checkProviderDiagnostics(
|
|
132
132
|
config: ParaUserConfig,
|
|
133
133
|
secretsPath: string,
|
|
134
|
+
authPath: string,
|
|
134
135
|
options: DoctorOptions,
|
|
135
136
|
): Promise<DoctorCheck[]> {
|
|
136
137
|
const checks: DoctorCheck[] = [];
|
|
@@ -144,49 +145,50 @@ async function checkProviderDiagnostics(
|
|
|
144
145
|
if (!config.qmd.embedEnabled) {
|
|
145
146
|
checks.push({ name: "qmd-embedding", status: "ok", message: "embedding disabled" });
|
|
146
147
|
} else if (config.qmd.embedding) {
|
|
147
|
-
checks.push(await checkCredentialProfile("qmd-embedding", config.qmd.embedding, secretsPath));
|
|
148
|
+
checks.push(await checkCredentialProfile("qmd-embedding", config.qmd.embedding, secretsPath, authPath));
|
|
148
149
|
} else {
|
|
149
150
|
checks.push({ name: "qmd-embedding", status: "warn", message: "embedding enabled but no pi-para embedding profile is configured" });
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
if (config.qmd.rerank) {
|
|
153
|
-
checks.push(await checkCredentialProfile("qmd-rerank", config.qmd.rerank, secretsPath));
|
|
154
|
+
checks.push(await checkCredentialProfile("qmd-rerank", config.qmd.rerank, secretsPath, authPath));
|
|
154
155
|
} else {
|
|
155
156
|
checks.push({ name: "qmd-rerank", status: "ok", message: "rerank disabled" });
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
checks.push(await checkCaptureModel(config, secretsPath, options.testCaptureModel === true));
|
|
159
|
+
checks.push(await checkCaptureModel(config, secretsPath, authPath, options.testCaptureModel === true));
|
|
159
160
|
return checks;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
async function checkCredentialProfile(name: string, profile: ProviderCredentialRef, secretsPath: string): Promise<DoctorCheck> {
|
|
163
|
+
async function checkCredentialProfile(name: string, profile: ProviderCredentialRef, secretsPath: string, authPath: string): Promise<DoctorCheck> {
|
|
163
164
|
const label = `${profile.provider}/${profile.model ?? "model not set"}`;
|
|
164
165
|
if (profile.credentialRef === "none") {
|
|
165
166
|
return { name, status: "ok", message: `${label} uses no credential` };
|
|
166
167
|
}
|
|
167
|
-
const
|
|
168
|
+
const services = profile.credentialRef.startsWith("pi-auth:") ? await createPiModelServices({ authPath }) : null;
|
|
169
|
+
const resolved = await resolveCredentialRef(profile.credentialRef, { secretsPath, credentials: services?.credentials });
|
|
168
170
|
if (!resolved.ok) {
|
|
169
171
|
return { name, status: "warn", message: `${label} credential missing: ${resolved.error}` };
|
|
170
172
|
}
|
|
171
173
|
return { name, status: "ok", message: `${label} credential available via ${resolved.source}` };
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
async function checkCaptureModel(config: ParaUserConfig, secretsPath: string, testModel: boolean): Promise<DoctorCheck> {
|
|
176
|
+
async function checkCaptureModel(config: ParaUserConfig, secretsPath: string, authPath: string, testModel: boolean): Promise<DoctorCheck> {
|
|
175
177
|
const selection = getCaptureSelection(config);
|
|
176
178
|
if (!testModel) {
|
|
177
179
|
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);
|
|
180
|
+
return checkCredentialProfile("capture-model", selection, secretsPath, authPath);
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
try {
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
const model = resolveSelectedModel(selection,
|
|
184
|
+
const services = await createPiModelServices({ authPath });
|
|
185
|
+
if (!services) return { name: "capture-model", status: "warn", message: "Pi credential runtime is unavailable" };
|
|
186
|
+
const model = resolveSelectedModel(selection, services.modelRegistry, { preferredModelSpec: "anthropic/claude-sonnet-4-20250514" });
|
|
185
187
|
if (!model) return { name: "capture-model", status: "warn", message: "no capture model resolved" };
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
const key = await createModelApiKeyResolver(selection, services.modelRegistry, {
|
|
189
|
+
credentials: services.credentials,
|
|
190
|
+
secretsPath,
|
|
191
|
+
})(model.provider);
|
|
190
192
|
return key
|
|
191
193
|
? { name: "capture-model", status: "ok", message: `${model.provider}/${model.id} credential available` }
|
|
192
194
|
: { name: "capture-model", status: "warn", message: `${model.provider}/${model.id} resolved but credential is missing` };
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
enqueueCompletedSessionsFromRegistry,
|
|
31
31
|
} from "./scheduler/session-capture.js";
|
|
32
32
|
import { loadParaConfig, toLegacyRuntimeConfig, type ParaUserConfig } from "./config.js";
|
|
33
|
-
import { createModelApiKeyResolver,
|
|
33
|
+
import { createModelApiKeyResolver, createPiModelServices, getCaptureSelection, resolveSelectedModel } from "./model-resolver.js";
|
|
34
34
|
import { StateDB } from "./state.js";
|
|
35
35
|
|
|
36
36
|
// Re-export public types for consumers
|
|
@@ -169,6 +169,7 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
169
169
|
const loadedConfig = await loadConfig();
|
|
170
170
|
const config = loadedConfig.runtime;
|
|
171
171
|
const wikiDir = config.wikiDir;
|
|
172
|
+
const printMode = isPrintModeProcess();
|
|
172
173
|
|
|
173
174
|
let store: QMDStore | null = null;
|
|
174
175
|
let currentScope: ProjectScope | null = null;
|
|
@@ -246,7 +247,15 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
246
247
|
});
|
|
247
248
|
|
|
248
249
|
// Register tools, context injection, commands
|
|
249
|
-
registerTools(
|
|
250
|
+
registerTools(
|
|
251
|
+
pi,
|
|
252
|
+
wikiDir,
|
|
253
|
+
storeProxy,
|
|
254
|
+
getScope,
|
|
255
|
+
markContextDirty,
|
|
256
|
+
() => config.searchGraphBoost,
|
|
257
|
+
() => loadedConfig.userConfig?.lint ?? { autoFix: true, staleDays: 90 },
|
|
258
|
+
);
|
|
250
259
|
setupContextInjection(pi, wikiDir, storeProxy, () => currentScope, getConfig);
|
|
251
260
|
registerCommands(pi, wikiDir, storeProxy, getScope, setScope);
|
|
252
261
|
|
|
@@ -300,10 +309,10 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
300
309
|
const startupCatchupEnabled = autoCaptureEnabled && wikiConfig?.captureOnStartup !== false && schedulerConfig?.startupCatchup !== false;
|
|
301
310
|
try {
|
|
302
311
|
const loadedParaConfig = await loadParaConfig({ migrate: true });
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
312
|
+
const services = autoCaptureEnabled ? await createPiModelServices() : null;
|
|
313
|
+
if (services) {
|
|
305
314
|
const captureSelection = getCaptureSelection(loadedParaConfig.config);
|
|
306
|
-
const captureModel = resolveSelectedModel(captureSelection,
|
|
315
|
+
const captureModel = resolveSelectedModel(captureSelection, services.modelRegistry, {
|
|
307
316
|
legacyModelSpec: config.daemonModel,
|
|
308
317
|
preferredModelSpec: "anthropic/claude-sonnet-4-20250514",
|
|
309
318
|
});
|
|
@@ -312,8 +321,8 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
312
321
|
wikiDir,
|
|
313
322
|
storeProvider: () => store,
|
|
314
323
|
model: captureModel,
|
|
315
|
-
getApiKey: createModelApiKeyResolver(captureSelection,
|
|
316
|
-
|
|
324
|
+
getApiKey: createModelApiKeyResolver(captureSelection, services.modelRegistry, {
|
|
325
|
+
credentials: services.credentials,
|
|
317
326
|
secretsPath: loadedParaConfig.paths.secretsPath,
|
|
318
327
|
}),
|
|
319
328
|
});
|
|
@@ -324,7 +333,7 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
324
333
|
}
|
|
325
334
|
|
|
326
335
|
const embedEnabled = loadedConfig.userConfig?.qmd.embedEnabled !== false;
|
|
327
|
-
const schedulerEnabled = !
|
|
336
|
+
const schedulerEnabled = !printMode && schedulerConfig?.enabled !== false;
|
|
328
337
|
const scheduler = schedulerEnabled
|
|
329
338
|
? startWikiScheduler({
|
|
330
339
|
wikiDir,
|
|
@@ -372,15 +381,15 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
372
381
|
ctx.ui.setStatus("pi-para", `wiki: ${url}`);
|
|
373
382
|
} else {
|
|
374
383
|
ctx.ui.setStatus("pi-para", "wiki: ready (web UI disabled)");
|
|
375
|
-
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
384
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
376
385
|
}
|
|
377
386
|
} catch {
|
|
378
387
|
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
379
|
-
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
388
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
380
389
|
}
|
|
381
390
|
} else {
|
|
382
391
|
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
383
|
-
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
392
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
384
393
|
}
|
|
385
394
|
});
|
|
386
395
|
|
package/src/link-utils.ts
CHANGED
|
@@ -50,10 +50,14 @@ function findProtectedRanges(body: string): Array<[number, number]> {
|
|
|
50
50
|
ranges.push([m.index, m.index + m[0].length]);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Inline code
|
|
54
|
-
|
|
53
|
+
// Inline code. Keep spans on one line and skip fenced regions already
|
|
54
|
+
// recorded above. The old /`[^`]+`/ pattern could start at a closing fence
|
|
55
|
+
// backtick and protect hundreds of characters of ordinary Markdown.
|
|
56
|
+
const inlineCodeRe = /`+[^`\n]+`+/g;
|
|
55
57
|
while ((m = inlineCodeRe.exec(body)) !== null) {
|
|
56
|
-
|
|
58
|
+
const start = m.index;
|
|
59
|
+
const end = m.index + m[0].length;
|
|
60
|
+
if (!isProtected(start, end, ranges)) ranges.push([start, end]);
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
// Existing [[wikilinks]]
|
|
@@ -89,19 +93,40 @@ function isProtected(pos: number, end: number, ranges: Array<[number, number]>):
|
|
|
89
93
|
return false;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/** Find plain-text slug mentions that autoLinkSlugs can actually fix. */
|
|
97
|
+
export function findUnlinkedSlugs(
|
|
98
|
+
body: string,
|
|
99
|
+
allSlugs: Set<string>,
|
|
100
|
+
ownSlug?: string,
|
|
101
|
+
): string[] {
|
|
102
|
+
if (allSlugs.size === 0) return [];
|
|
103
|
+
const bodyLinks = new Set(extractWikilinks(body));
|
|
104
|
+
const protectedRanges = findProtectedRanges(body);
|
|
105
|
+
const unlinked: string[] = [];
|
|
106
|
+
|
|
107
|
+
const sortedSlugs = [...allSlugs]
|
|
108
|
+
.filter((slug) => slug !== ownSlug && !bodyLinks.has(slug))
|
|
109
|
+
.sort((a, b) => b.length - a.length);
|
|
110
|
+
|
|
111
|
+
for (const slug of sortedSlugs) {
|
|
112
|
+
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
+
const re = new RegExp(`(?<![a-z0-9-])${escaped}(?![a-z0-9-])`, "g");
|
|
114
|
+
let match: RegExpExecArray | null;
|
|
115
|
+
while ((match = re.exec(body)) !== null) {
|
|
116
|
+
const start = match.index;
|
|
117
|
+
const end = start + match[0].length;
|
|
118
|
+
if (!isProtected(start, end, protectedRanges)) {
|
|
119
|
+
unlinked.push(slug);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return unlinked;
|
|
125
|
+
}
|
|
126
|
+
|
|
92
127
|
/**
|
|
93
|
-
* Auto-link page slug mentions
|
|
94
|
-
*
|
|
95
|
-
* - Wraps unlinked slug mentions in [[slug]]
|
|
96
|
-
* - Skips code blocks, URLs, existing [[wikilinks]], headings
|
|
97
|
-
* - Matches longest slugs first to avoid partial matches
|
|
98
|
-
* - Uses word-boundary matching (won't match "pi-para" inside "pi-para-daemon")
|
|
99
|
-
* - Only links each slug once per page (first occurrence) to avoid noise
|
|
100
|
-
*
|
|
101
|
-
* @param body - Page body markdown
|
|
102
|
-
* @param allSlugs - Set of all existing page slugs
|
|
103
|
-
* @param ownSlug - This page's own slug (excluded from auto-linking)
|
|
104
|
-
* @returns Updated body with [[wikilinks]] inserted
|
|
128
|
+
* Auto-link page slug mentions while skipping code, URLs, headings, and
|
|
129
|
+
* existing wikilinks. Only the first fixable occurrence of each slug is linked.
|
|
105
130
|
*/
|
|
106
131
|
export function autoLinkSlugs(
|
|
107
132
|
body: string,
|
|
@@ -115,7 +140,6 @@ export function autoLinkSlugs(
|
|
|
115
140
|
.filter(s => s !== ownSlug)
|
|
116
141
|
.sort((a, b) => b.length - a.length);
|
|
117
142
|
|
|
118
|
-
const protectedRanges = findProtectedRanges(body);
|
|
119
143
|
const linkedSlugs = new Set(extractWikilinks(body));
|
|
120
144
|
|
|
121
145
|
let result = body;
|
package/src/lint.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
extractWikilinks,
|
|
25
25
|
removeWikilink,
|
|
26
26
|
autoLinkSlugs,
|
|
27
|
+
findUnlinkedSlugs,
|
|
27
28
|
syncFrontmatterLinks,
|
|
28
29
|
} from "./link-utils.js";
|
|
29
30
|
import { normalizeTags, normalizeScopes } from "./tag-registry.js";
|
|
@@ -415,22 +416,9 @@ function checkLinkSync(
|
|
|
415
416
|
): LintIssue[] {
|
|
416
417
|
const issues: LintIssue[] = [];
|
|
417
418
|
for (const p of pages) {
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
.filter(s => s !== p.slug)
|
|
422
|
-
.sort((a, b) => b.length - a.length);
|
|
423
|
-
|
|
424
|
-
const unlinked: string[] = [];
|
|
425
|
-
for (const slug of sortedSlugs) {
|
|
426
|
-
if (bodyLinks.has(slug)) continue;
|
|
427
|
-
// Check if slug appears as plain text in body (word-boundary match)
|
|
428
|
-
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
429
|
-
const re = new RegExp(`(?<![a-z0-9-])${escaped}(?![a-z0-9-])`);
|
|
430
|
-
if (re.test(p.body)) {
|
|
431
|
-
unlinked.push(slug);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
419
|
+
// Use the exact same protected-range semantics as the fixer so every
|
|
420
|
+
// auto-fixable issue is actionable (no code-block/heading false positives).
|
|
421
|
+
const unlinked = findUnlinkedSlugs(p.body, allSlugs, p.slug);
|
|
434
422
|
|
|
435
423
|
if (unlinked.length > 0) {
|
|
436
424
|
issues.push({
|
|
@@ -603,7 +591,9 @@ async function fixSecrets(
|
|
|
603
591
|
updated: new Date().toISOString(),
|
|
604
592
|
},
|
|
605
593
|
};
|
|
606
|
-
|
|
594
|
+
p.body = fixedPage.body;
|
|
595
|
+
p.frontmatter = fixedPage.frontmatter;
|
|
596
|
+
await writePage(wikiDir, p);
|
|
607
597
|
|
|
608
598
|
fixed.push({
|
|
609
599
|
severity: "error",
|
|
@@ -650,7 +640,9 @@ async function fixLinkSync(
|
|
|
650
640
|
updated: new Date().toISOString(),
|
|
651
641
|
},
|
|
652
642
|
};
|
|
653
|
-
|
|
643
|
+
p.body = fixedPage.body;
|
|
644
|
+
p.frontmatter = fixedPage.frontmatter;
|
|
645
|
+
await writePage(wikiDir, p);
|
|
654
646
|
|
|
655
647
|
if (addedLinks.length > 0) {
|
|
656
648
|
fixed.push({
|
|
@@ -729,7 +721,9 @@ async function fixBrokenLinks(
|
|
|
729
721
|
body: newBody,
|
|
730
722
|
frontmatter: { ...p.frontmatter, links: newFmLinks },
|
|
731
723
|
};
|
|
732
|
-
|
|
724
|
+
p.body = fixedPage.body;
|
|
725
|
+
p.frontmatter = fixedPage.frontmatter;
|
|
726
|
+
await writePage(wikiDir, p);
|
|
733
727
|
|
|
734
728
|
for (const link of broken) {
|
|
735
729
|
fixed.push({
|
|
@@ -931,11 +925,10 @@ export async function lintWiki(
|
|
|
931
925
|
|
|
932
926
|
// Load all pages
|
|
933
927
|
const refs = await listPages(wikiDir);
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
928
|
+
const loadedPages = await Promise.all(
|
|
929
|
+
refs.map((ref) => readPage(wikiDir, ref.category, ref.slug)),
|
|
930
|
+
);
|
|
931
|
+
const pages = loadedPages.filter((page): page is WikiPage => page !== null);
|
|
939
932
|
|
|
940
933
|
// Build slug set
|
|
941
934
|
const allSlugs = new Set(pages.map((p) => p.slug));
|
|
@@ -978,15 +971,38 @@ export async function lintWiki(
|
|
|
978
971
|
// Auto-fix if enabled
|
|
979
972
|
const fixed: LintIssue[] = [];
|
|
980
973
|
if (autoFix) {
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
974
|
+
// Only run a fixer against pages its check identified. Previously every
|
|
975
|
+
// fixer rescanned all P pages (and link sync scanned all S slugs per page)
|
|
976
|
+
// even when that category had zero issues.
|
|
977
|
+
const affectedPages = (category: LintIssue["category"]): WikiPage[] => {
|
|
978
|
+
const paths = new Set(
|
|
979
|
+
issues
|
|
980
|
+
.filter((issue) => issue.category === category && issue.autoFixable && issue.page)
|
|
981
|
+
.map((issue) => issue.page!),
|
|
982
|
+
);
|
|
983
|
+
return paths.size === 0
|
|
984
|
+
? []
|
|
985
|
+
: pages.filter((page) => paths.has(`${page.category}/${page.slug}`));
|
|
986
|
+
};
|
|
987
|
+
const hasIssue = (category: LintIssue["category"]): boolean =>
|
|
988
|
+
issues.some((issue) => issue.category === category && issue.autoFixable);
|
|
989
|
+
|
|
990
|
+
// Fix secrets first — highest priority.
|
|
991
|
+
const secretPages = affectedPages("secrets");
|
|
992
|
+
if (secretPages.length > 0) fixed.push(...(await fixSecrets(wikiDir, secretPages)));
|
|
993
|
+
const brokenPages = affectedPages("broken-link");
|
|
994
|
+
if (brokenPages.length > 0) fixed.push(...(await fixBrokenLinks(wikiDir, brokenPages, allSlugs)));
|
|
995
|
+
const linkPages = affectedPages("link-sync");
|
|
996
|
+
if (linkPages.length > 0) fixed.push(...(await fixLinkSync(wikiDir, linkPages, allSlugs)));
|
|
997
|
+
const tagPages = affectedPages("tag-health");
|
|
998
|
+
if (tagPages.length > 0) fixed.push(...(await fixTagHealth(wikiDir, tagPages)));
|
|
999
|
+
if (hasIssue("index-drift")) fixed.push(...(await fixIndexDrift(wikiDir, pages, indexContent)));
|
|
1000
|
+
const frontmatterPages = affectedPages("frontmatter");
|
|
1001
|
+
if (frontmatterPages.length > 0) fixed.push(...(await fixFrontmatter(wikiDir, frontmatterPages)));
|
|
1002
|
+
const scopePages = affectedPages("scope-drift");
|
|
1003
|
+
if (scopePages.length > 0) fixed.push(...(await fixScopeDrift(wikiDir, scopePages)));
|
|
1004
|
+
const schemaPages = affectedPages("schema-version");
|
|
1005
|
+
if (schemaPages.length > 0) fixed.push(...(await fixSchemaVersion(wikiDir, schemaPages)));
|
|
990
1006
|
}
|
|
991
1007
|
|
|
992
1008
|
// Recount broken links for stats
|
package/src/model-resolver.ts
CHANGED
|
@@ -3,15 +3,22 @@ import type { ProviderCredentialRef, ParaUserConfig } from "./config.js";
|
|
|
3
3
|
import { resolveCredentialRef } from "./credentials.js";
|
|
4
4
|
|
|
5
5
|
export interface ModelRegistryLike {
|
|
6
|
+
getAll(): Model<any>[];
|
|
6
7
|
getAvailable(): Model<any>[];
|
|
7
8
|
find(provider: string, modelId: string): Model<any> | undefined;
|
|
8
9
|
getApiKeyForProvider(provider: string): Promise<string | undefined>;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export interface
|
|
12
|
+
export interface PiCredentialReader {
|
|
13
|
+
hasStoredCredential(provider: string): boolean;
|
|
12
14
|
getApiKey(provider: string): Promise<string | undefined>;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export interface PiModelServices {
|
|
18
|
+
modelRegistry: ModelRegistryLike;
|
|
19
|
+
credentials: PiCredentialReader;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export type ModelSelection = ProviderCredentialRef | "auto";
|
|
16
23
|
|
|
17
24
|
export function parseProviderModelSpec(spec: string): { provider: string; modelId: string } | null {
|
|
@@ -20,6 +27,10 @@ export function parseProviderModelSpec(spec: string): { provider: string; modelI
|
|
|
20
27
|
return { provider: spec.slice(0, idx), modelId: spec.slice(idx + 1) };
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
function isRemoteModel(model: Model<any>): boolean {
|
|
31
|
+
return model.provider !== "node-llama-cpp" && model.provider !== "local" && !model.provider.includes("llama");
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
export function resolveSelectedModel(
|
|
24
35
|
selection: ModelSelection,
|
|
25
36
|
registry: ModelRegistryLike,
|
|
@@ -27,47 +38,50 @@ export function resolveSelectedModel(
|
|
|
27
38
|
): Model<any> | null {
|
|
28
39
|
if (selection !== "auto") {
|
|
29
40
|
if (selection.model) return registry.find(selection.provider, selection.model) ?? null;
|
|
30
|
-
return registry.getAvailable().find((model) => model.provider === selection.provider) ?? null;
|
|
41
|
+
return registry.getAvailable().find((model) => model.provider === selection.provider && isRemoteModel(model)) ?? null;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
const legacy = opts.legacyModelSpec ? parseProviderModelSpec(opts.legacyModelSpec) : null;
|
|
34
45
|
if (legacy) {
|
|
35
46
|
const model = registry.find(legacy.provider, legacy.modelId);
|
|
36
|
-
if (model) return model;
|
|
47
|
+
if (model && isRemoteModel(model)) return model;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
const preferred = opts.preferredModelSpec ? parseProviderModelSpec(opts.preferredModelSpec) : null;
|
|
40
51
|
if (preferred) {
|
|
41
52
|
const model = registry.find(preferred.provider, preferred.modelId);
|
|
42
|
-
if (model) return model;
|
|
53
|
+
if (model && isRemoteModel(model)) return model;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
|
-
return pickBestAvailableModel(registry.getAvailable());
|
|
56
|
+
return pickBestAvailableModel(registry.getAvailable().filter(isRemoteModel));
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
export function pickBestAvailableModel(models: Model<any>[]): Model<any> | null {
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
const remote = models.filter(isRemoteModel);
|
|
61
|
+
if (remote.length === 0) return null;
|
|
62
|
+
const sonnet = remote.find((model) => model.provider === "anthropic" && /sonnet/i.test(model.id));
|
|
51
63
|
if (sonnet) return sonnet;
|
|
52
|
-
return [...
|
|
64
|
+
return [...remote].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0))[0] ?? null;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
export function createModelApiKeyResolver(
|
|
56
68
|
selection: ModelSelection,
|
|
57
69
|
registry: ModelRegistryLike,
|
|
58
|
-
opts: {
|
|
70
|
+
opts: { credentials?: PiCredentialReader; secretsPath?: string } = {},
|
|
59
71
|
): (provider: string) => Promise<string | undefined> {
|
|
60
72
|
return async (provider: string) => {
|
|
61
|
-
if (selection === "auto")
|
|
73
|
+
if (selection === "auto") {
|
|
74
|
+
return opts.credentials ? opts.credentials.getApiKey(provider) : registry.getApiKeyForProvider(provider);
|
|
75
|
+
}
|
|
76
|
+
if (selection.provider !== provider) return undefined;
|
|
62
77
|
|
|
63
78
|
if (selection.credentialRef.startsWith("pi-auth:")) {
|
|
64
79
|
const configuredProvider = selection.credentialRef.slice("pi-auth:".length);
|
|
65
|
-
if (configuredProvider !== provider
|
|
66
|
-
return
|
|
80
|
+
if (configuredProvider !== provider) return undefined;
|
|
81
|
+
return opts.credentials ? opts.credentials.getApiKey(provider) : registry.getApiKeyForProvider(provider);
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
if (selection.credentialRef.startsWith("secret:")) {
|
|
70
|
-
if (selection.provider !== provider) return undefined;
|
|
71
85
|
const resolved = await resolveCredentialRef(selection.credentialRef, { secretsPath: opts.secretsPath });
|
|
72
86
|
return resolved.ok ? resolved.value : undefined;
|
|
73
87
|
}
|
|
@@ -80,17 +94,28 @@ export function getCaptureSelection(config: ParaUserConfig): ModelSelection {
|
|
|
80
94
|
return config.models.capture;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
export async function
|
|
97
|
+
export async function createPiModelServices(
|
|
98
|
+
options: { authPath?: string } = {},
|
|
99
|
+
): Promise<PiModelServices | null> {
|
|
84
100
|
try {
|
|
85
101
|
const mod = await import("@earendil-works/pi-coding-agent");
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
const runtime = await mod.ModelRuntime.create({ authPath: options.authPath, allowModelNetwork: false });
|
|
103
|
+
const modelRegistry = new mod.ModelRegistry(runtime);
|
|
104
|
+
await modelRegistry.refresh();
|
|
105
|
+
return {
|
|
106
|
+
modelRegistry,
|
|
107
|
+
credentials: {
|
|
108
|
+
hasStoredCredential: (provider) => Boolean(mod.readStoredCredential(provider, options.authPath)),
|
|
109
|
+
getApiKey: async (provider) => {
|
|
110
|
+
if (!mod.readStoredCredential(provider, options.authPath)) return undefined;
|
|
111
|
+
return (await runtime.getAuth(provider))?.auth.apiKey;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
89
114
|
};
|
|
90
|
-
if (!typed.AuthStorage || !typed.ModelRegistry) return null;
|
|
91
|
-
const authStorage = typed.AuthStorage.create();
|
|
92
|
-
return { authStorage, modelRegistry: typed.ModelRegistry.create(authStorage) };
|
|
93
115
|
} catch {
|
|
94
116
|
return null;
|
|
95
117
|
}
|
|
96
118
|
}
|
|
119
|
+
|
|
120
|
+
/** @deprecated Use createPiModelServices. */
|
|
121
|
+
export const createPiModelRegistry = createPiModelServices;
|
package/src/tools.ts
CHANGED
|
@@ -860,13 +860,23 @@ function createMoveExecute(
|
|
|
860
860
|
|
|
861
861
|
// -- Factory: wiki_lint execute ----------------------------------------------
|
|
862
862
|
|
|
863
|
-
function createLintExecute(
|
|
863
|
+
function createLintExecute(
|
|
864
|
+
wikiDir: string,
|
|
865
|
+
store: QMDStore,
|
|
866
|
+
markDirty: () => void,
|
|
867
|
+
getLintConfig?: () => { autoFix: boolean; staleDays: number },
|
|
868
|
+
) {
|
|
864
869
|
return async (
|
|
865
870
|
params: { autoFix?: boolean },
|
|
866
871
|
): Promise<AgentToolResult<WikiLintDetails>> => {
|
|
872
|
+
const configured = getLintConfig?.();
|
|
867
873
|
const report = await lintWiki(wikiDir, {
|
|
868
|
-
autoFix: params.autoFix ?? true,
|
|
874
|
+
autoFix: params.autoFix ?? configured?.autoFix ?? true,
|
|
875
|
+
staleDays: configured?.staleDays ?? 90,
|
|
869
876
|
});
|
|
877
|
+
if (report.fixed.length > 0) {
|
|
878
|
+
scheduleWikiMaintenance(wikiDir, store, markDirty);
|
|
879
|
+
}
|
|
870
880
|
|
|
871
881
|
const { issues, fixed, stats } = report;
|
|
872
882
|
|
|
@@ -1057,6 +1067,7 @@ export function registerTools(
|
|
|
1057
1067
|
getScope: () => ProjectScope,
|
|
1058
1068
|
markDirty: () => void,
|
|
1059
1069
|
getGraphBoost?: () => boolean,
|
|
1070
|
+
getLintConfig?: () => { autoFix: boolean; staleDays: number },
|
|
1060
1071
|
): void {
|
|
1061
1072
|
const ingestExec = createIngestExecute(wikiDir, store, getScope);
|
|
1062
1073
|
const queryExec = createQueryExecute(wikiDir, store, getScope, getGraphBoost);
|
|
@@ -1064,7 +1075,7 @@ export function registerTools(
|
|
|
1064
1075
|
const editExec = createEditExecute(wikiDir, store, markDirty);
|
|
1065
1076
|
const readExec = createReadExecute(wikiDir);
|
|
1066
1077
|
const moveExec = createMoveExecute(wikiDir, store, markDirty);
|
|
1067
|
-
const lintExec = createLintExecute(wikiDir);
|
|
1078
|
+
const lintExec = createLintExecute(wikiDir, store, markDirty, getLintConfig);
|
|
1068
1079
|
const migrateExec = createMigrateExecute(wikiDir);
|
|
1069
1080
|
const summarizeExec = createSummarizeExecute(wikiDir, getScope);
|
|
1070
1081
|
|
|
@@ -1306,7 +1317,8 @@ export function registerTools(
|
|
|
1306
1317
|
try { return await lintExec(params); } finally { ctx.ui.setStatus("pi-para", undefined); }
|
|
1307
1318
|
},
|
|
1308
1319
|
renderCall(args, theme) {
|
|
1309
|
-
const
|
|
1320
|
+
const configuredAutoFix = getLintConfig?.().autoFix ?? true;
|
|
1321
|
+
const mode = (args.autoFix ?? configuredAutoFix) ? "auto-fix" : "report-only";
|
|
1310
1322
|
return new Text(
|
|
1311
1323
|
theme.fg("toolTitle", theme.bold("wiki_lint ")) +
|
|
1312
1324
|
theme.fg("muted", `[${mode}]`),
|