@nklisch/pi-enhanced 0.4.1 → 0.4.2
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/node_modules/@nklisch/pi-astral-pocket/README.md +108 -52
- package/node_modules/@nklisch/pi-astral-pocket/package.json +3 -1
- package/node_modules/@nklisch/pi-astral-pocket/src/config.ts +40 -23
- package/node_modules/@nklisch/pi-astral-pocket/src/controller.ts +78 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/distiller.ts +182 -73
- package/node_modules/@nklisch/pi-astral-pocket/src/guidance.ts +12 -5
- package/node_modules/@nklisch/pi-astral-pocket/src/index.ts +172 -77
- package/node_modules/@nklisch/pi-astral-pocket/src/provider.ts +128 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/scope.ts +33 -0
- package/node_modules/@nklisch/pi-astral-pocket/src/sessions.ts +111 -99
- package/node_modules/@nklisch/pi-astral-pocket/src/store.ts +319 -119
- package/node_modules/@nklisch/pi-astral-pocket/src/tools.ts +43 -13
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
|
@@ -2,47 +2,43 @@ import { join } from "node:path";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
import { recomputeActivation, type ActivationState } from "./activation.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_DISTILLER_MODEL,
|
|
7
|
+
DEFAULT_DISTILLER_REASONING,
|
|
8
|
+
isModelSpec,
|
|
9
|
+
isReasoningLevel,
|
|
10
|
+
loadConfig,
|
|
11
|
+
saveConfig,
|
|
12
|
+
type PocketConfig,
|
|
13
|
+
} from "./config.js";
|
|
14
|
+
import { DistillerController } from "./controller.js";
|
|
6
15
|
import { runDistillerPass } from "./distiller.js";
|
|
7
16
|
import { buildPocketGuidance } from "./guidance.js";
|
|
8
|
-
import {
|
|
17
|
+
import { createDistillerModelClient, type DistillerModelStatus } from "./provider.js";
|
|
18
|
+
import { resolveProjectIdentity } from "./scope.js";
|
|
19
|
+
import { countNotes, defaultAgentDir, ensureLayout, pocketRoot, readScopedSummary } from "./store.js";
|
|
9
20
|
import { registerPocketTools } from "./tools.js";
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (message.stopReason === "error") throw new Error(message.errorMessage ?? "model error");
|
|
14
|
-
if (!Array.isArray(message.content)) return "";
|
|
15
|
-
return (message.content as { type?: string; text?: string }[])
|
|
16
|
-
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
17
|
-
.map((b) => b.text)
|
|
18
|
-
.join("");
|
|
22
|
+
function safeNotify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
23
|
+
try { ctx.ui.notify(message, level); } catch { /* lifecycle may revoke the command context */ }
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
? [config.distiller.model]
|
|
27
|
-
: DISTILLER_MODEL_PREFERENCE;
|
|
28
|
-
for (const ref of candidates) {
|
|
29
|
-
const slash = ref.indexOf("/");
|
|
30
|
-
const providerId = ref.slice(0, slash);
|
|
31
|
-
const modelId = ref.slice(slash + 1);
|
|
32
|
-
const model = ctx.modelRegistry.find(providerId, modelId);
|
|
33
|
-
if (!model) continue;
|
|
34
|
-
const provider = ctx.modelRegistry.getProvider(providerId) as {
|
|
35
|
-
streamSimple?: (m: unknown, c: unknown, o?: unknown) => { result(): Promise<unknown> };
|
|
36
|
-
} | null;
|
|
37
|
-
if (!provider?.streamSimple) continue;
|
|
38
|
-
return async (prompt: string) => {
|
|
39
|
-
const stream = provider.streamSimple!(model, {
|
|
40
|
-
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
41
|
-
});
|
|
42
|
-
return messageText((await stream.result()) as { stopReason?: string; errorMessage?: string; content: unknown });
|
|
43
|
-
};
|
|
26
|
+
function outcomeText(controller: DistillerController): string {
|
|
27
|
+
const outcome = controller.status();
|
|
28
|
+
if (outcome.state === "completed") {
|
|
29
|
+
if (outcome.result.skippedReason) return `skipped (${outcome.result.skippedReason})`;
|
|
30
|
+
return `completed (${outcome.result.processed} revision(s), digest ${outcome.result.digest}, ${outcome.result.errors.length} error(s))`;
|
|
44
31
|
}
|
|
45
|
-
return
|
|
32
|
+
if (outcome.state === "failed") return `failed (${outcome.error})`;
|
|
33
|
+
return outcome.state;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function modelStatus(ctx: ExtensionContext, config: PocketConfig): DistillerModelStatus {
|
|
37
|
+
return createDistillerModelClient(
|
|
38
|
+
ctx.modelRegistry,
|
|
39
|
+
config.distiller.model,
|
|
40
|
+
config.distiller.reasoning,
|
|
41
|
+
).status();
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
export default function extension(pi: ExtensionAPI): void {
|
|
@@ -50,7 +46,9 @@ export default function extension(pi: ExtensionAPI): void {
|
|
|
50
46
|
const root = pocketRoot(agentDir);
|
|
51
47
|
const sessionsDir = join(agentDir, "sessions");
|
|
52
48
|
const state: ActivationState = { active: false };
|
|
49
|
+
const controller = new DistillerController();
|
|
53
50
|
let config = loadConfig(root);
|
|
51
|
+
let currentProjectId: string | undefined;
|
|
54
52
|
|
|
55
53
|
registerPocketTools(pi, {
|
|
56
54
|
state,
|
|
@@ -59,69 +57,166 @@ export default function extension(pi: ExtensionAPI): void {
|
|
|
59
57
|
maxSessionAgeDays: () => config.distiller.maxSessionAgeDays,
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
function startPass(ctx: ExtensionContext, forceDigest = false): void {
|
|
61
|
+
if (!state.active || !currentProjectId) return;
|
|
62
|
+
const snapshot = structuredClone(config.distiller);
|
|
63
|
+
const projectId = currentProjectId;
|
|
64
|
+
const client = createDistillerModelClient(ctx.modelRegistry, snapshot.model, snapshot.reasoning);
|
|
65
|
+
const available = client.status().error === undefined;
|
|
66
|
+
void controller.start(
|
|
67
|
+
(signal) => runDistillerPass(root, sessionsDir, snapshot, {
|
|
68
|
+
callModel: available ? (prompt, requestSignal, maxTokens) => client.call(prompt, requestSignal, maxTokens) : null,
|
|
69
|
+
log: () => undefined,
|
|
70
|
+
signal,
|
|
71
|
+
forceDigest,
|
|
72
|
+
}, projectId),
|
|
73
|
+
(message, level) => safeNotify(ctx, message, level),
|
|
74
|
+
).catch(() => undefined);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Activation and command use reload config so external edits take effect. */
|
|
78
|
+
function activate(ctx: ExtensionContext, startOnActivation = true, replaceSession = false): void {
|
|
79
|
+
const previousConfig = config;
|
|
80
|
+
const previousProjectId = currentProjectId;
|
|
81
|
+
config = loadConfig(root);
|
|
82
|
+
currentProjectId = resolveProjectIdentity(ctx.cwd);
|
|
83
|
+
const effectiveConfigChanged = JSON.stringify(previousConfig) !== JSON.stringify(config);
|
|
84
|
+
const projectChanged = previousProjectId !== undefined && previousProjectId !== currentProjectId;
|
|
85
|
+
if (replaceSession || effectiveConfigChanged || projectChanged) controller.stop();
|
|
86
|
+
|
|
66
87
|
const becameActive = recomputeActivation(pi, ctx, state, config);
|
|
67
|
-
if (!state.active)
|
|
88
|
+
if (!state.active) {
|
|
89
|
+
controller.stop();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
68
92
|
ensureLayout(root);
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
callModel,
|
|
73
|
-
log: (msg) => ctx.ui.notify(msg, "info"),
|
|
74
|
-
})
|
|
75
|
-
.then((result) => {
|
|
76
|
-
if (result.errors.length > 0) {
|
|
77
|
-
ctx.ui.notify(`astral-pocket distiller: ${result.errors.length} session(s) failed`, "warning");
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
.catch(() => ctx.ui.notify("astral-pocket distiller pass failed; mechanical floor intact", "warning"));
|
|
93
|
+
if (startOnActivation && config.distiller.enabled && (replaceSession || becameActive || effectiveConfigChanged || projectChanged)) {
|
|
94
|
+
startPass(ctx);
|
|
95
|
+
}
|
|
81
96
|
}
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
pi.on("model_select", (_event, ctx) =>
|
|
87
|
-
|
|
98
|
+
// A new session invalidates the previous session's reporting context even
|
|
99
|
+
// when the selected model remains Astra.
|
|
100
|
+
pi.on("session_start", (_event, ctx) => activate(ctx, true, true));
|
|
101
|
+
pi.on("model_select", (_event, ctx) => activate(ctx));
|
|
102
|
+
pi.on("session_shutdown", () => {
|
|
103
|
+
state.active = false;
|
|
104
|
+
currentProjectId = undefined;
|
|
105
|
+
controller.stop();
|
|
88
106
|
});
|
|
89
107
|
|
|
90
|
-
pi.on("before_agent_start", (event,
|
|
108
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
91
109
|
if (!state.active) return undefined;
|
|
92
|
-
|
|
110
|
+
const projectId = currentProjectId ?? resolveProjectIdentity(ctx.cwd);
|
|
111
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${buildPocketGuidance(readScopedSummary(root, projectId))}` };
|
|
93
112
|
});
|
|
94
113
|
|
|
95
114
|
pi.registerCommand("pocket", {
|
|
96
|
-
description: "
|
|
115
|
+
description: "Manage Astral Pocket: status, on/off, distiller, model, reasoning, distill, rebuild",
|
|
97
116
|
getArgumentCompletions: (prefix) => {
|
|
98
|
-
const
|
|
99
|
-
const filtered =
|
|
117
|
+
const values = ["status", "on", "off", "distiller on", "distiller off", "model reset", "reasoning minimal", "distill", "rebuild"];
|
|
118
|
+
const filtered = values.filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value }));
|
|
100
119
|
return filtered.length > 0 ? filtered : null;
|
|
101
120
|
},
|
|
102
121
|
handler: async (args, ctx) => {
|
|
103
|
-
|
|
122
|
+
activate(ctx, false);
|
|
123
|
+
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
124
|
+
const verb = (words[0] ?? "status").toLowerCase();
|
|
125
|
+
|
|
104
126
|
if (verb === "on" || verb === "off") {
|
|
127
|
+
if (words.length !== 1) {
|
|
128
|
+
safeNotify(ctx, `Usage: /pocket ${verb}`, "warning");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
105
131
|
config = { ...config, enabled: verb === "on" };
|
|
106
132
|
saveConfig(root, config);
|
|
107
133
|
activate(ctx);
|
|
108
|
-
ctx.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
134
|
+
safeNotify(ctx, verb === "on" ? "Astral Pocket enabled for Astra sessions." : "Astral Pocket disabled.");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (verb === "distiller") {
|
|
139
|
+
const value = words[1]?.toLowerCase();
|
|
140
|
+
if ((value !== "on" && value !== "off") || words.length !== 2) {
|
|
141
|
+
safeNotify(ctx, "Usage: /pocket distiller on|off", "warning");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
config = { ...config, distiller: { ...config.distiller, enabled: value === "on" } };
|
|
145
|
+
saveConfig(root, config);
|
|
146
|
+
controller.stop();
|
|
147
|
+
if (value === "on" && state.active) startPass(ctx);
|
|
148
|
+
safeNotify(ctx, `Astral Pocket distiller ${value === "on" ? "enabled" : "disabled"}.`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (verb === "model") {
|
|
153
|
+
const value = words.slice(1).join(" ").trim();
|
|
154
|
+
const model = value === "reset" || value === "" ? DEFAULT_DISTILLER_MODEL : value;
|
|
155
|
+
if (words.length > 2 || !isModelSpec(model)) {
|
|
156
|
+
safeNotify(ctx, "Usage: /pocket model provider/modelId (or /pocket model reset)", "warning");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
config = { ...config, distiller: { ...config.distiller, model } };
|
|
160
|
+
saveConfig(root, config);
|
|
161
|
+
controller.stop();
|
|
162
|
+
if (state.active && config.distiller.enabled) startPass(ctx);
|
|
163
|
+
const status = modelStatus(ctx, config);
|
|
164
|
+
safeNotify(ctx, status.error ? `Distiller model saved but unavailable: ${status.error}` : `Distiller model: ${status.resolvedModel}.` , status.error ? "warning" : "info");
|
|
114
165
|
return;
|
|
115
166
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
[
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
|
|
168
|
+
if (verb === "reasoning") {
|
|
169
|
+
const value = words[1]?.toLowerCase() ?? "reset";
|
|
170
|
+
const reasoning = value === "reset" ? DEFAULT_DISTILLER_REASONING : value;
|
|
171
|
+
if (words.length > 2 || !isReasoningLevel(reasoning)) {
|
|
172
|
+
safeNotify(ctx, "Usage: /pocket reasoning off|minimal|low|medium|high|xhigh|max|reset", "warning");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
config = { ...config, distiller: { ...config.distiller, reasoning } };
|
|
176
|
+
saveConfig(root, config);
|
|
177
|
+
controller.stop();
|
|
178
|
+
if (state.active && config.distiller.enabled) startPass(ctx);
|
|
179
|
+
const status = modelStatus(ctx, config);
|
|
180
|
+
safeNotify(ctx, `Distiller reasoning requested: ${reasoning}; effective: ${status.effectiveReasoning ?? "unavailable"}.`, status.error ? "warning" : "info");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (verb === "distill" || verb === "rebuild") {
|
|
185
|
+
if (words.length !== 1) {
|
|
186
|
+
safeNotify(ctx, `Usage: /pocket ${verb}`, "warning");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!state.active) {
|
|
190
|
+
safeNotify(ctx, "Astral Pocket distillation is available only in an active Astra session.", "warning");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (!config.distiller.enabled) {
|
|
194
|
+
safeNotify(ctx, "The distiller is disabled. Run /pocket distiller on first.", "warning");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
startPass(ctx, verb === "rebuild");
|
|
198
|
+
safeNotify(ctx, verb === "rebuild" ? "Astral Pocket digest rebuild started." : "Astral Pocket distillation started.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (verb !== "status" || words.length > 1) {
|
|
203
|
+
safeNotify(ctx, "Usage: /pocket status|on|off|distiller on|off|model <provider/modelId>|reasoning <level>|distill|rebuild", "warning");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const notes = countNotes(root);
|
|
208
|
+
const status = modelStatus(ctx, config);
|
|
209
|
+
const reasoning = status.effectiveReasoning && status.effectiveReasoning !== status.requestedReasoning
|
|
210
|
+
? `${status.requestedReasoning} → ${status.effectiveReasoning}`
|
|
211
|
+
: status.requestedReasoning;
|
|
212
|
+
safeNotify(ctx, [
|
|
213
|
+
`Pocket: ${config.enabled ? "enabled" : "disabled"}; ${state.active ? "active" : "inactive (Astra only)"}`,
|
|
214
|
+
`Notes: ${notes}. Distiller: ${config.distiller.enabled ? "on" : "off"}`,
|
|
215
|
+
`Model: requested ${status.requestedModel}; resolved ${status.resolvedModel ?? "unavailable"}`,
|
|
216
|
+
`Reasoning: ${reasoning}${status.error ? `; ${status.error}` : ""}`,
|
|
217
|
+
`Last pass: ${outcomeText(controller)}`,
|
|
218
|
+
`Store: ${root}`,
|
|
219
|
+
].join("\n"), status.error ? "warning" : "info");
|
|
125
220
|
},
|
|
126
221
|
});
|
|
127
222
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
Context,
|
|
5
|
+
Model,
|
|
6
|
+
ModelThinkingLevel,
|
|
7
|
+
Provider,
|
|
8
|
+
SimpleStreamOptions,
|
|
9
|
+
} from "@earendil-works/pi-ai";
|
|
10
|
+
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
11
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
export interface DistillerModelStatus {
|
|
14
|
+
requestedModel: string;
|
|
15
|
+
resolvedModel?: string;
|
|
16
|
+
requestedReasoning: ModelThinkingLevel;
|
|
17
|
+
effectiveReasoning?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DistillerModelClient {
|
|
22
|
+
status(): DistillerModelStatus;
|
|
23
|
+
call(prompt: string, signal: AbortSignal, maxTokens: number): Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ModelInvoker = (
|
|
27
|
+
provider: Provider,
|
|
28
|
+
model: Model<Api>,
|
|
29
|
+
context: Context,
|
|
30
|
+
options: SimpleStreamOptions,
|
|
31
|
+
) => Promise<AssistantMessage>;
|
|
32
|
+
|
|
33
|
+
function parseModelSpec(spec: string): { provider: string; modelId: string } | undefined {
|
|
34
|
+
const slash = spec.indexOf("/");
|
|
35
|
+
if (slash <= 0 || slash === spec.length - 1) return undefined;
|
|
36
|
+
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveReasoning(model: Model<Api>, requested: ModelThinkingLevel): {
|
|
40
|
+
request?: SimpleStreamOptions["reasoning"];
|
|
41
|
+
effective: string;
|
|
42
|
+
} {
|
|
43
|
+
const clamped = clampThinkingLevel(model, requested);
|
|
44
|
+
const mapped = model.thinkingLevelMap?.[clamped];
|
|
45
|
+
return {
|
|
46
|
+
...(clamped === "off" ? {} : { request: clamped }),
|
|
47
|
+
effective: mapped ?? clamped,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
52
|
+
if (signal.aborted) return Promise.reject(new Error("distiller request aborted"));
|
|
53
|
+
return new Promise<T>((resolve, reject) => {
|
|
54
|
+
const onAbort = () => reject(new Error("distiller request aborted"));
|
|
55
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
56
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractSuccessfulText(message: AssistantMessage): string {
|
|
61
|
+
if (message.stopReason !== "stop" || message.errorMessage !== undefined) {
|
|
62
|
+
throw new Error(`distiller model ended with ${message.errorMessage ?? message.stopReason}`);
|
|
63
|
+
}
|
|
64
|
+
const text = message.content
|
|
65
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
66
|
+
.map((part) => part.text)
|
|
67
|
+
.join("\n")
|
|
68
|
+
.trim();
|
|
69
|
+
if (text.length === 0) throw new Error("distiller model returned no text");
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const defaultInvoker: ModelInvoker = async (provider, model, context, options) =>
|
|
74
|
+
provider.streamSimple(model, context, options).result();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve one exact model and use Pi's registry for fresh authentication on
|
|
78
|
+
* every request. Direct provider calls need these request options; invoking a
|
|
79
|
+
* provider without them breaks OAuth-backed providers such as openai-codex.
|
|
80
|
+
*/
|
|
81
|
+
export function createDistillerModelClient(
|
|
82
|
+
registry: Pick<ModelRegistry, "find" | "getProvider" | "getApiKeyAndHeaders">,
|
|
83
|
+
requestedModel: string,
|
|
84
|
+
requestedReasoning: ModelThinkingLevel,
|
|
85
|
+
invoker: ModelInvoker = defaultInvoker,
|
|
86
|
+
): DistillerModelClient {
|
|
87
|
+
const parsed = parseModelSpec(requestedModel);
|
|
88
|
+
const model = parsed ? registry.find(parsed.provider, parsed.modelId) : undefined;
|
|
89
|
+
const provider = parsed ? registry.getProvider(parsed.provider) : undefined;
|
|
90
|
+
const reasoning = model ? resolveReasoning(model, requestedReasoning) : undefined;
|
|
91
|
+
const unavailable = !parsed
|
|
92
|
+
? `invalid model "${requestedModel}"; use provider/modelId`
|
|
93
|
+
: !model
|
|
94
|
+
? `model ${requestedModel} is not in the Pi model registry`
|
|
95
|
+
: !provider
|
|
96
|
+
? `provider ${parsed.provider} is not available`
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
status: () => ({
|
|
101
|
+
requestedModel,
|
|
102
|
+
...(model ? { resolvedModel: `${model.provider}/${model.id}` } : {}),
|
|
103
|
+
requestedReasoning,
|
|
104
|
+
...(reasoning ? { effectiveReasoning: reasoning.effective } : {}),
|
|
105
|
+
...(unavailable ? { error: unavailable } : {}),
|
|
106
|
+
}),
|
|
107
|
+
async call(prompt, signal, maxTokens) {
|
|
108
|
+
if (!model || !provider || !reasoning) throw new Error(unavailable ?? "distiller model unavailable");
|
|
109
|
+
if (signal.aborted) throw new Error("distiller request aborted");
|
|
110
|
+
const auth = await raceWithSignal(registry.getApiKeyAndHeaders(model), signal);
|
|
111
|
+
if (signal.aborted) throw new Error("distiller request aborted");
|
|
112
|
+
if (!auth.ok) throw new Error(`authentication failed for ${requestedModel}: ${auth.error}`);
|
|
113
|
+
const options: SimpleStreamOptions = {
|
|
114
|
+
signal,
|
|
115
|
+
maxTokens,
|
|
116
|
+
...(reasoning.request ? { reasoning: reasoning.request } : {}),
|
|
117
|
+
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
118
|
+
...(auth.headers ? { headers: auth.headers } : {}),
|
|
119
|
+
...(auth.env ? { env: auth.env } : {}),
|
|
120
|
+
};
|
|
121
|
+
const message = await invoker(provider, model, {
|
|
122
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
123
|
+
}, options);
|
|
124
|
+
if (signal.aborted) throw new Error("distiller request aborted");
|
|
125
|
+
return extractSuccessfulText(message);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const cache = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
function canonicalPath(path: string): string {
|
|
8
|
+
try { return realpathSync(path); } catch { return resolve(path); }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Identify a local repository by Git's common directory, which is shared by
|
|
13
|
+
* subdirectories and linked worktrees. Non-Git directories use their resolved
|
|
14
|
+
* cwd. Remote URLs and basenames are deliberately not identity inputs.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveProjectIdentity(cwd: string): string {
|
|
17
|
+
const canonicalCwd = canonicalPath(cwd);
|
|
18
|
+
const cached = cache.get(canonicalCwd);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
let identity = canonicalCwd;
|
|
21
|
+
try {
|
|
22
|
+
const commonDir = execFileSync(
|
|
23
|
+
"git",
|
|
24
|
+
["-C", canonicalCwd, "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
25
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2_000 },
|
|
26
|
+
).trim();
|
|
27
|
+
if (commonDir) identity = canonicalPath(commonDir);
|
|
28
|
+
} catch {
|
|
29
|
+
// Not being a Git repository is a supported mode, not an activation error.
|
|
30
|
+
}
|
|
31
|
+
cache.set(canonicalCwd, identity);
|
|
32
|
+
return identity;
|
|
33
|
+
}
|