pi-para 0.6.5 → 0.6.7
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 +22 -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/context.d.ts.map +1 -1
- package/dist/context.js +2 -1
- package/dist/context.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 +14 -10
- package/dist/index.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/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +9 -3
- package/dist/scheduler/index.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +48 -16
- package/dist/store.js.map +1 -1
- package/dist/wiki-tool-guidance.js +2 -2
- package/dist/wiki-tool-guidance.js.map +1 -1
- package/package.json +9 -9
- package/src/cli.ts +69 -81
- package/src/commands.ts +15 -6
- package/src/context.ts +2 -1
- package/src/credentials.ts +7 -14
- package/src/doctor.ts +18 -16
- package/src/index.ts +11 -10
- package/src/model-resolver.ts +45 -20
- package/src/scheduler/index.ts +9 -3
- package/src/store.ts +52 -15
- package/src/wiki-tool-guidance.ts +2 -2
package/src/cli.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
11
12
|
|
|
@@ -17,123 +18,108 @@ import { formatLegacyDaemonWarning, isLegacyDaemonCommand } from "./cli-legacy.j
|
|
|
17
18
|
// -- Model setup -------------------------------------------------------------
|
|
18
19
|
// Use MiniMax via the same config as qmd-engine
|
|
19
20
|
|
|
20
|
-
async function createModel(modelArg?: string) {
|
|
21
|
+
export async function createModel(modelArg?: string) {
|
|
21
22
|
const { parse } = await import("yaml");
|
|
22
23
|
const { readFileSync } = await import("node:fs");
|
|
23
|
-
const {
|
|
24
|
+
const { getProviders, getEnvApiKey } = await import("@earendil-works/pi-ai/compat");
|
|
25
|
+
const { createPiModelServices, parseProviderModelSpec } = await import("./model-resolver.js");
|
|
26
|
+
const { loadParaConfig } = await import("./config.js");
|
|
27
|
+
const { resolveCredentialRef } = await import("./credentials.js");
|
|
24
28
|
|
|
25
|
-
// 0. Try config.json daemonModel
|
|
26
29
|
if (!modelArg) {
|
|
27
30
|
try {
|
|
28
31
|
const configPath = join(homedir(), ".pi", "wiki", "config.json");
|
|
29
|
-
const
|
|
30
|
-
const config = JSON.parse(raw);
|
|
32
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
31
33
|
if (config.daemonModel) {
|
|
32
34
|
modelArg = config.daemonModel;
|
|
33
35
|
console.log(`[daemon] Using model from config.json: ${modelArg}`);
|
|
34
36
|
}
|
|
35
37
|
} catch {
|
|
36
|
-
// No
|
|
38
|
+
// No legacy daemon model configured.
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
const services = await createPiModelServices();
|
|
43
|
+
let configuredCapture: Awaited<ReturnType<typeof loadParaConfig>> | null = null;
|
|
44
|
+
try { configuredCapture = await loadParaConfig(); } catch { /* optional fallback */ }
|
|
45
|
+
const configuredSelection = configuredCapture?.config.models.capture;
|
|
46
|
+
if (!modelArg && configuredSelection && configuredSelection !== "auto" && configuredSelection.model) {
|
|
47
|
+
modelArg = `${configuredSelection.provider}/${configuredSelection.model}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const configuredSecret = async (provider: string): Promise<string | undefined> => {
|
|
51
|
+
const selection = configuredCapture?.config.models.capture;
|
|
52
|
+
if (!selection || selection === "auto" || selection.provider !== provider || !selection.credentialRef.startsWith("secret:")) return undefined;
|
|
53
|
+
const resolved = await resolveCredentialRef(selection.credentialRef, { secretsPath: configuredCapture?.paths.secretsPath });
|
|
54
|
+
return resolved.ok ? resolved.value : undefined;
|
|
55
|
+
};
|
|
56
|
+
const getDurableKey = async (provider: string): Promise<string | undefined> =>
|
|
57
|
+
await services?.credentials.getApiKey(provider) ?? await configuredSecret(provider);
|
|
58
|
+
|
|
59
|
+
if (modelArg && !services) {
|
|
60
|
+
console.error("Error: Pi credential runtime is unavailable.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (modelArg && services) {
|
|
65
|
+
const spec = parseProviderModelSpec(modelArg);
|
|
66
|
+
const model = spec ? services.modelRegistry.find(spec.provider, spec.modelId) : undefined;
|
|
67
|
+
if (model && model.provider !== "node-llama-cpp" && model.provider !== "local") {
|
|
68
|
+
const key = await getDurableKey(model.provider) ?? getEnvApiKey(model.provider);
|
|
69
|
+
if (key) {
|
|
70
|
+
const getApiKey = async (provider: string) => await getDurableKey(provider) ?? getEnvApiKey(provider);
|
|
71
|
+
console.log(`[daemon] Using pi model: ${model.provider}/${model.id} (context: ${model.contextWindow})`);
|
|
61
72
|
return { model, getApiKey };
|
|
62
73
|
}
|
|
74
|
+
console.error(`Error: No credential available for Pi model: ${modelArg}`);
|
|
75
|
+
process.exit(1);
|
|
63
76
|
}
|
|
77
|
+
console.error(`Error: Unknown Pi model: ${modelArg}`);
|
|
78
|
+
process.exit(1);
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const { AuthStorage } = await import("@earendil-works/pi-coding-agent");
|
|
69
|
-
const authStorage = AuthStorage.create();
|
|
70
|
-
const { getModels } = await import("@earendil-works/pi-ai/compat");
|
|
71
|
-
|
|
72
|
-
// Check providers in preference order: anthropic first (best quality)
|
|
81
|
+
if (services) {
|
|
73
82
|
const preferredProviders = ["anthropic", "openai", "openrouter", "google-antigravity", "github-copilot"];
|
|
74
83
|
for (const provider of preferredProviders) {
|
|
75
|
-
if (!
|
|
76
|
-
const
|
|
77
|
-
if (!key) continue;
|
|
78
|
-
|
|
79
|
-
const models = getModels(provider as any);
|
|
84
|
+
if (!services.credentials.hasStoredCredential(provider) && !await configuredSecret(provider)) continue;
|
|
85
|
+
const models = services.modelRegistry.getAll().filter((model) => model.provider === provider && model.provider !== "node-llama-cpp" && model.provider !== "local");
|
|
80
86
|
const sorted = [...models].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
|
81
|
-
const picked = sorted.find(
|
|
87
|
+
const picked = sorted.find((model) => !model.reasoning) ?? sorted[0];
|
|
82
88
|
if (picked) {
|
|
83
|
-
const getApiKey = async (
|
|
84
|
-
|
|
85
|
-
return k ?? getEnvApiKey(p) ?? "";
|
|
86
|
-
};
|
|
87
|
-
console.log(`[daemon] Using pi auth: ${provider}/${picked.id} (context: ${picked.contextWindow})`);
|
|
89
|
+
const getApiKey = async (requestedProvider: string) => await getDurableKey(requestedProvider) ?? getEnvApiKey(requestedProvider);
|
|
90
|
+
console.log(`[daemon] Using persisted Pi auth: ${provider}/${picked.id} (context: ${picked.contextWindow})`);
|
|
88
91
|
return { model: picked, getApiKey };
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
|
-
} catch {
|
|
92
|
-
// AuthStorage not available — continue to env/qmd fallback
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
-
const
|
|
97
|
-
for (const provider of providers) {
|
|
96
|
+
// Optional legacy environment compatibility.
|
|
97
|
+
for (const provider of getProviders()) {
|
|
98
98
|
const key = getEnvApiKey(provider);
|
|
99
|
-
if (key)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (picked) {
|
|
105
|
-
const getApiKey = async (p: string) => getEnvApiKey(p) ?? "";
|
|
106
|
-
console.log(`[daemon] Using env key: ${provider}/${picked.id} (context: ${picked.contextWindow})`);
|
|
107
|
-
return { model: picked, getApiKey };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
99
|
+
if (!key) continue;
|
|
100
|
+
const models = services?.modelRegistry.getAll().filter((model) => model.provider === provider) ?? [];
|
|
101
|
+
const sorted = [...models].sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
|
102
|
+
const picked = sorted.find((model) => !model.reasoning) ?? sorted[0];
|
|
103
|
+
if (picked) return { model: picked, getApiKey: async (requestedProvider: string) => getEnvApiKey(requestedProvider) };
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
//
|
|
106
|
+
// Preserve the legacy CLI's QMD compatibility fallback.
|
|
113
107
|
const configPath = join(homedir(), ".config", "qmd", "index.yml");
|
|
114
108
|
if (existsSync(configPath)) {
|
|
115
109
|
const cfg = parse(readFileSync(configPath, "utf-8"));
|
|
116
110
|
const chat = cfg?.providers?.chat;
|
|
117
111
|
if (chat?.url && chat?.key) {
|
|
118
112
|
const model = {
|
|
119
|
-
id: chat.model || "MiniMax-M2.7-highspeed",
|
|
120
|
-
name: chat.model || "MiniMax-M2.7-highspeed",
|
|
121
|
-
provider: "custom",
|
|
113
|
+
id: chat.model || "MiniMax-M2.7-highspeed", name: chat.model || "MiniMax-M2.7-highspeed", provider: "custom",
|
|
122
114
|
api: chat.api === "anthropic" ? "anthropic-messages" as const : "openai-completions" as const,
|
|
123
|
-
baseUrl: chat.url,
|
|
124
|
-
|
|
125
|
-
input: ["text" as const],
|
|
126
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
127
|
-
contextWindow: 196000,
|
|
128
|
-
maxTokens: 8192,
|
|
115
|
+
baseUrl: chat.url, reasoning: false, input: ["text" as const],
|
|
116
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 196000, maxTokens: 8192,
|
|
129
117
|
};
|
|
130
|
-
|
|
131
|
-
console.log(`[daemon] Using qmd provider: ${chat.model} at ${chat.url}`);
|
|
132
|
-
return { model, getApiKey };
|
|
118
|
+
return { model, getApiKey: async (_provider: string) => chat.key as string };
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
121
|
|
|
136
|
-
console.error("Error: No LLM available.
|
|
122
|
+
console.error("Error: No LLM available. Configure persisted Pi auth or ~/.pi/para/secrets.json (legacy env/QMD configuration is also supported).");
|
|
137
123
|
process.exit(1);
|
|
138
124
|
}
|
|
139
125
|
|
|
@@ -233,7 +219,7 @@ async function main() {
|
|
|
233
219
|
const secrets = await readSecretStore();
|
|
234
220
|
const names = Object.keys(secrets.secrets);
|
|
235
221
|
console.log("pi-para providers");
|
|
236
|
-
console.log(" Credential policy: Pi
|
|
222
|
+
console.log(" Credential policy: persisted Pi auth preferred; pi-para secrets fallback; env vars not used by setup.");
|
|
237
223
|
if (names.length === 0) console.log(" No pi-para local secrets configured.");
|
|
238
224
|
for (const name of names) console.log(` secret:${name} = ${redactCredential(secrets.secrets[name])}`);
|
|
239
225
|
}
|
|
@@ -440,12 +426,14 @@ Legacy compatibility commands:
|
|
|
440
426
|
history Show processing history (--scope NAME to filter)
|
|
441
427
|
|
|
442
428
|
Credential policy:
|
|
443
|
-
Use Pi
|
|
429
|
+
Use persisted Pi auth or ~/.pi/para/secrets.json. Setup does not use env vars for API keys.`);
|
|
444
430
|
break;
|
|
445
431
|
}
|
|
446
432
|
}
|
|
447
433
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
434
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
435
|
+
main().catch((err) => {
|
|
436
|
+
console.error(err);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
});
|
|
439
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -25,6 +25,15 @@ import {
|
|
|
25
25
|
validateFrontmatter,
|
|
26
26
|
} from "./frontmatter.js";
|
|
27
27
|
import { appendCompletedSession } from "./scheduler/session-capture.js";
|
|
28
|
+
import { createPiModelServices, type PiModelServices } from "./model-resolver.js";
|
|
29
|
+
|
|
30
|
+
export function listStoredCaptureProviders(services: PiModelServices): string[] {
|
|
31
|
+
return [...new Set(
|
|
32
|
+
services.modelRegistry.getAll()
|
|
33
|
+
.map((model) => model.provider)
|
|
34
|
+
.filter((provider) => services.credentials.hasStoredCredential(provider)),
|
|
35
|
+
)];
|
|
36
|
+
}
|
|
28
37
|
|
|
29
38
|
// -- Helpers -----------------------------------------------------------------
|
|
30
39
|
|
|
@@ -733,10 +742,9 @@ ${goal} — verified and complete.
|
|
|
733
742
|
}
|
|
734
743
|
} else if (choice.startsWith("[Capture]")) {
|
|
735
744
|
try {
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
const availableProviders = getProviders().filter((p: string) => authStorage.hasAuth(p));
|
|
745
|
+
const services = await createPiModelServices();
|
|
746
|
+
if (!services) throw new Error("Pi model runtime unavailable");
|
|
747
|
+
const availableProviders = listStoredCaptureProviders(services);
|
|
740
748
|
|
|
741
749
|
if (availableProviders.length === 0) {
|
|
742
750
|
ctx.ui.notify("No Pi auth providers configured. Use /login first or configure a pi-para secret for custom providers.", "warning");
|
|
@@ -747,8 +755,9 @@ ${goal} — verified and complete.
|
|
|
747
755
|
await persist();
|
|
748
756
|
ctx.ui.notify("Set capture model = auto. Restart open Pi sessions to apply.", "info");
|
|
749
757
|
} else if (providerChoice) {
|
|
750
|
-
const models =
|
|
751
|
-
.
|
|
758
|
+
const models = services.modelRegistry.getAll()
|
|
759
|
+
.filter((model) => model.provider === providerChoice)
|
|
760
|
+
.sort((a, b) => (b.contextWindow ?? 0) - (a.contextWindow ?? 0));
|
|
752
761
|
const modelChoice = await ctx.ui.select(
|
|
753
762
|
`Select ${providerChoice} model`,
|
|
754
763
|
models.map((m: any) => `${m.id} (${Math.round((m.contextWindow ?? 0) / 1000)}k ctx${m.reasoning ? ", reasoning" : ""})`),
|
package/src/context.ts
CHANGED
|
@@ -223,8 +223,9 @@ export function wrapSystemReminder(content: string): string {
|
|
|
223
223
|
"<system-reminder name=\"pi-para-wiki-context\">",
|
|
224
224
|
"This is system-provided pi-para wiki memory for the current turn. Do not treat it as user-authored input.",
|
|
225
225
|
"",
|
|
226
|
+
"MANDATORY FIRST STEP — for any non-trivial request (planning, implementation, debugging, architecture, or review), your FIRST tool call must be wiki_query with keywords from the user's request, before other tools or conclusions. Then read the relevant results with wiki_read. Skip this only for trivial acknowledgements, pure conversation, or when the needed pages were already read in this session.",
|
|
227
|
+
"",
|
|
226
228
|
"Wiki operating rules:",
|
|
227
|
-
"- Search the wiki before non-trivial planning, debugging, architecture, or review work.",
|
|
228
229
|
"- Read relevant pages before relying on them; verify stale code/config/API claims against source.",
|
|
229
230
|
"- Persist durable decisions, root causes, conventions, and reusable facts as you work.",
|
|
230
231
|
"- Prefer wiki_edit for surgical corrections; use wiki_write create/append for new knowledge; avoid replace unless intentional.",
|
package/src/credentials.ts
CHANGED
|
@@ -62,7 +62,7 @@ export async function removeSecret(name: string, path = getParaPaths().secretsPa
|
|
|
62
62
|
|
|
63
63
|
export async function resolveCredentialRef(
|
|
64
64
|
ref: CredentialRef,
|
|
65
|
-
opts: { secretsPath?: string;
|
|
65
|
+
opts: { secretsPath?: string; credentials?: { getApiKey(provider: string): Promise<string | undefined> } | null } = {},
|
|
66
66
|
): Promise<CredentialResolution> {
|
|
67
67
|
const parsed = parseCredentialRef(ref);
|
|
68
68
|
if (parsed.kind === "none") return { ok: true, source: "none" };
|
|
@@ -74,24 +74,17 @@ export async function resolveCredentialRef(
|
|
|
74
74
|
return { ok: true, source: "secret", value };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const credentials = Object.hasOwn(opts, "credentials")
|
|
78
|
+
? opts.credentials
|
|
79
|
+
: (await (await import("./model-resolver.js")).createPiModelServices())?.credentials;
|
|
80
|
+
if (!credentials) {
|
|
81
|
+
return { ok: false, source: "missing", error: "Pi credential runtime is unavailable." };
|
|
80
82
|
}
|
|
81
|
-
const value = await
|
|
83
|
+
const value = await credentials.getApiKey(parsed.name ?? "");
|
|
82
84
|
if (!value) return { ok: false, source: "missing", error: `Pi auth not configured for provider: ${parsed.name}` };
|
|
83
85
|
return { ok: true, source: "pi-auth", value };
|
|
84
86
|
}
|
|
85
87
|
|
|
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
88
|
|
|
96
89
|
export function redactCredential(value: string | undefined): string {
|
|
97
90
|
if (!value) return "not set";
|
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;
|
|
@@ -308,10 +309,10 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
308
309
|
const startupCatchupEnabled = autoCaptureEnabled && wikiConfig?.captureOnStartup !== false && schedulerConfig?.startupCatchup !== false;
|
|
309
310
|
try {
|
|
310
311
|
const loadedParaConfig = await loadParaConfig({ migrate: true });
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
312
|
+
const services = autoCaptureEnabled ? await createPiModelServices() : null;
|
|
313
|
+
if (services) {
|
|
313
314
|
const captureSelection = getCaptureSelection(loadedParaConfig.config);
|
|
314
|
-
const captureModel = resolveSelectedModel(captureSelection,
|
|
315
|
+
const captureModel = resolveSelectedModel(captureSelection, services.modelRegistry, {
|
|
315
316
|
legacyModelSpec: config.daemonModel,
|
|
316
317
|
preferredModelSpec: "anthropic/claude-sonnet-4-20250514",
|
|
317
318
|
});
|
|
@@ -320,8 +321,8 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
320
321
|
wikiDir,
|
|
321
322
|
storeProvider: () => store,
|
|
322
323
|
model: captureModel,
|
|
323
|
-
getApiKey: createModelApiKeyResolver(captureSelection,
|
|
324
|
-
|
|
324
|
+
getApiKey: createModelApiKeyResolver(captureSelection, services.modelRegistry, {
|
|
325
|
+
credentials: services.credentials,
|
|
325
326
|
secretsPath: loadedParaConfig.paths.secretsPath,
|
|
326
327
|
}),
|
|
327
328
|
});
|
|
@@ -332,7 +333,7 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
const embedEnabled = loadedConfig.userConfig?.qmd.embedEnabled !== false;
|
|
335
|
-
const schedulerEnabled = !
|
|
336
|
+
const schedulerEnabled = !printMode && schedulerConfig?.enabled !== false;
|
|
336
337
|
const scheduler = schedulerEnabled
|
|
337
338
|
? startWikiScheduler({
|
|
338
339
|
wikiDir,
|
|
@@ -380,15 +381,15 @@ export default async function piPara(pi: ExtensionAPI): Promise<void> {
|
|
|
380
381
|
ctx.ui.setStatus("pi-para", `wiki: ${url}`);
|
|
381
382
|
} else {
|
|
382
383
|
ctx.ui.setStatus("pi-para", "wiki: ready (web UI disabled)");
|
|
383
|
-
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
384
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 5000);
|
|
384
385
|
}
|
|
385
386
|
} catch {
|
|
386
387
|
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
387
|
-
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
388
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
388
389
|
}
|
|
389
390
|
} else {
|
|
390
391
|
ctx.ui.setStatus("pi-para", "wiki: ready");
|
|
391
|
-
setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
392
|
+
if (!printMode) setTimeout(() => ctx.ui.setStatus("pi-para", undefined), 3000);
|
|
392
393
|
}
|
|
393
394
|
});
|
|
394
395
|
|
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/scheduler/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QMDStore } from "qmd-engine";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { gitCommit, rebuildIndex } from "../wiki.js";
|
|
4
|
-
import { reindex, storeHasApiProviders } from "../store.js";
|
|
4
|
+
import { captureQmdEmbedErrors, reindex, storeHasApiProviders } from "../store.js";
|
|
5
5
|
import { getParaPaths } from "../paths.js";
|
|
6
6
|
import { acquireLease, releaseLease } from "./leases.js";
|
|
7
7
|
import { SchedulerStateDB, type QueueItem } from "./state.js";
|
|
@@ -166,10 +166,16 @@ export class WikiScheduler {
|
|
|
166
166
|
const leaseKey = `qmd-embed:${this.wikiDir}`;
|
|
167
167
|
if (!acquireLease(this.state.db, leaseKey, this.holderId, { ttlMs: 15 * 60_000 })) return;
|
|
168
168
|
try {
|
|
169
|
-
const
|
|
169
|
+
const attempt = await captureQmdEmbedErrors(() => store.embed());
|
|
170
|
+
const diagnostics = [...new Set(attempt.diagnostics)].join(" | ");
|
|
171
|
+
if (!attempt.ok) {
|
|
172
|
+
const reason = attempt.error instanceof Error ? attempt.error.message : String(attempt.error);
|
|
173
|
+
throw new Error(diagnostics ? `${reason} — ${diagnostics}` : reason);
|
|
174
|
+
}
|
|
175
|
+
const result = attempt.value;
|
|
170
176
|
if (result.errors > 0) {
|
|
171
177
|
throw new Error(
|
|
172
|
-
`qmd embed finished with ${result.errors} error(s) (${result.chunksEmbedded} chunk(s) embedded, ${pending} doc(s) were pending)`,
|
|
178
|
+
`qmd embed finished with ${result.errors} error(s) (${result.chunksEmbedded} chunk(s) embedded, ${pending} doc(s) were pending)${diagnostics ? ` — ${diagnostics}` : ""}`,
|
|
173
179
|
);
|
|
174
180
|
}
|
|
175
181
|
} finally {
|