pi-bro 0.25.1 → 0.26.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/CHANGELOG.md +29 -0
- package/README.md +12 -10
- package/docs/getting-started.md +2 -2
- package/docs/guided-review.md +4 -4
- package/docs/reference/data-and-permissions.md +16 -3
- package/docs/reference/settings.md +7 -2
- package/docs/reference/troubleshooting.md +5 -0
- package/package.json +5 -3
- package/src/advisor.ts +4 -2
- package/src/backend.ts +511 -11
- package/src/bro.ts +5 -0
- package/src/btw.ts +28 -4
- package/src/config-ui.ts +149 -51
- package/src/doctor.ts +142 -3
- package/src/explain-modal.ts +2 -2
- package/src/settings.ts +85 -6
package/src/settings.ts
CHANGED
|
@@ -9,20 +9,22 @@ import {
|
|
|
9
9
|
GROK_EFFORTS,
|
|
10
10
|
CODEX_EFFORTS,
|
|
11
11
|
MUSE_EFFORTS,
|
|
12
|
+
PI_EFFORTS,
|
|
12
13
|
} from "./backend.ts";
|
|
13
14
|
import { type BroMode, DEFAULT_BRO_MODE, MAX_PREFERENCES_CHARS, parseBroMode } from "./prompt.ts";
|
|
14
15
|
import { errorMessage, isRecord } from "./util.ts";
|
|
15
16
|
|
|
16
17
|
export const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
17
|
-
export const BACKENDS = ["agy", "claude", "grok", "codex", "muse"] as const;
|
|
18
|
+
export const BACKENDS = ["agy", "claude", "grok", "codex", "muse", "pi"] as const;
|
|
18
19
|
export type BackendName = (typeof BACKENDS)[number];
|
|
19
20
|
export type BroEffort = "default" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
20
21
|
export type AgyEffort = Exclude<BroEffort, "default" | "minimal" | "xhigh" | "max">;
|
|
21
22
|
export type ClaudeEffort = (typeof CLAUDE_EFFORTS)[number];
|
|
22
|
-
export { GROK_EFFORTS, CODEX_EFFORTS, MUSE_EFFORTS };
|
|
23
|
+
export { GROK_EFFORTS, CODEX_EFFORTS, MUSE_EFFORTS, PI_EFFORTS };
|
|
23
24
|
export type GrokEffort = (typeof GROK_EFFORTS)[number];
|
|
24
25
|
export type CodexEffort = (typeof CODEX_EFFORTS)[number];
|
|
25
26
|
export type MuseEffort = (typeof MUSE_EFFORTS)[number];
|
|
27
|
+
export type PiEffort = (typeof PI_EFFORTS)[number];
|
|
26
28
|
|
|
27
29
|
export const CAPABILITIES = ["explain", "show", "btw", "advisor", "review"] as const;
|
|
28
30
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
@@ -75,7 +77,7 @@ export const DEFAULT_SHOW_TURNS = 1;
|
|
|
75
77
|
|
|
76
78
|
export function parseBackend(value: unknown, context: string): BackendName {
|
|
77
79
|
if (typeof value !== "string" || !BACKENDS.some((backend) => backend === value)) {
|
|
78
|
-
throw new Error(`${context} backend must be "agy", "claude", "grok", "codex", or "
|
|
80
|
+
throw new Error(`${context} backend must be "agy", "claude", "grok", "codex", "muse", or "pi".`);
|
|
79
81
|
}
|
|
80
82
|
return value as BackendName;
|
|
81
83
|
}
|
|
@@ -100,6 +102,12 @@ export function isMuseEffort(effort: unknown): effort is MuseEffort | "default"
|
|
|
100
102
|
return effort === "default" || MUSE_EFFORTS.some((item) => item === effort);
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
// The in-process Pi backend accepts the whole BroEffort range; createAgentSession clamps it to
|
|
106
|
+
// what the chosen physical model actually supports.
|
|
107
|
+
export function isPiEffort(effort: unknown): effort is BroEffort {
|
|
108
|
+
return effort === "default" || PI_EFFORTS.some((item) => item === effort);
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
// Sonnet/opus aliases resolve case-insensitively; any other non-empty string passes
|
|
104
112
|
// through untouched as an explicit user-entered model ID.
|
|
105
113
|
export function resolveClaudeModel(input: string): string {
|
|
@@ -133,7 +141,18 @@ export function resolveMuseModel(input: string): string {
|
|
|
133
141
|
return trimmed;
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
|
|
144
|
+
// Pi models are physical registry entries addressed as a non-empty "<provider>/<id>" pair;
|
|
145
|
+
// only the first "/" separates the two so model IDs containing slashes survive intact.
|
|
146
|
+
export function resolvePiModel(input: string): string {
|
|
147
|
+
const trimmed = input.trim();
|
|
148
|
+
const separator = trimmed.indexOf("/");
|
|
149
|
+
if (separator <= 0 || separator === trimmed.length - 1) {
|
|
150
|
+
throw new Error("Pi model must be a non-empty <provider>/<id> pair from your Pi model catalog.");
|
|
151
|
+
}
|
|
152
|
+
return trimmed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type ExternalBackend = "claude" | "grok" | "codex" | "muse" | "pi";
|
|
137
156
|
|
|
138
157
|
export interface ExternalBackendMeta {
|
|
139
158
|
readonly name: ExternalBackend;
|
|
@@ -144,6 +163,8 @@ export interface ExternalBackendMeta {
|
|
|
144
163
|
readonly effortHelp: string;
|
|
145
164
|
isEffort(effort: unknown): effort is BroEffort;
|
|
146
165
|
resolveModel(input: string): string;
|
|
166
|
+
/** How this backend's stored model reads in the config list. Defaults to `prefix + model`. */
|
|
167
|
+
displayModel?(model: string): string;
|
|
147
168
|
}
|
|
148
169
|
|
|
149
170
|
export const EXTERNAL_BACKENDS: Record<ExternalBackend, ExternalBackendMeta> = {
|
|
@@ -187,8 +208,35 @@ export const EXTERNAL_BACKENDS: Record<ExternalBackend, ExternalBackendMeta> = {
|
|
|
187
208
|
isEffort: isMuseEffort,
|
|
188
209
|
resolveModel: resolveMuseModel,
|
|
189
210
|
},
|
|
211
|
+
// Pi models are addressed by "<provider>/<id>" and resolved live from the host registry,
|
|
212
|
+
// so there are no fixed seed models and no prefix form. The `pi:` prefix is only the transport
|
|
213
|
+
// tag the picker uses to carry the choice back into applyModelChange; the stored and displayed
|
|
214
|
+
// value is the bare "<provider>/<id>".
|
|
215
|
+
pi: {
|
|
216
|
+
name: "pi",
|
|
217
|
+
label: "Pi",
|
|
218
|
+
prefix: "pi:",
|
|
219
|
+
efforts: PI_EFFORTS,
|
|
220
|
+
models: [],
|
|
221
|
+
effortHelp: '"default", "minimal", "low", "medium", "high", "xhigh", or "max"',
|
|
222
|
+
isEffort: isPiEffort,
|
|
223
|
+
resolveModel: resolvePiModel,
|
|
224
|
+
displayModel: (model) => model,
|
|
225
|
+
},
|
|
190
226
|
};
|
|
191
227
|
|
|
228
|
+
/** The config-list text for a backend's stored model, using the backend's own display rule. */
|
|
229
|
+
export function externalModelDisplay(backend: ExternalBackend, model: string): string {
|
|
230
|
+
const meta = EXTERNAL_BACKENDS[backend];
|
|
231
|
+
return meta.displayModel ? meta.displayModel(model) : `${meta.prefix}${model}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Narrows a BackendName to one that has an EXTERNAL_BACKENDS catalog entry. Agy is the only
|
|
235
|
+
// backend without one, so this is exactly "not Agy" and keeps index lookups type-safe.
|
|
236
|
+
export function isExternalBackend(backend: BackendName): backend is ExternalBackend {
|
|
237
|
+
return backend !== "agy";
|
|
238
|
+
}
|
|
239
|
+
|
|
192
240
|
export function parseBackendOption(value: string): { backend: ExternalBackend; id: string } | undefined {
|
|
193
241
|
for (const meta of Object.values(EXTERNAL_BACKENDS)) {
|
|
194
242
|
if (value.startsWith(meta.prefix) && value.length > meta.prefix.length) {
|
|
@@ -198,6 +246,37 @@ export function parseBackendOption(value: string): { backend: ExternalBackend; i
|
|
|
198
246
|
return undefined;
|
|
199
247
|
}
|
|
200
248
|
|
|
249
|
+
// A single selectable Pi model: a physical, deduplicated "<provider>/<id>" entry.
|
|
250
|
+
export interface PiModelChoice {
|
|
251
|
+
/** Canonical "<provider>/<id>" value stored in settings. */
|
|
252
|
+
value: string;
|
|
253
|
+
/** Human label for the picker. */
|
|
254
|
+
label: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Builds the `value` for a Pi model, splitting on the first "/" only. */
|
|
258
|
+
export function piModelValue(providerId: string, modelId: string): string {
|
|
259
|
+
return `${providerId}/${modelId}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Turns resolved scoped-model entries (or registry models) into deduplicated, physical-only choices.
|
|
263
|
+
// Models reported as "pi-virtual" are dropped: they route elsewhere at request time and cannot be
|
|
264
|
+
// driven as a standalone in-process session.
|
|
265
|
+
export function toPiModelChoices(
|
|
266
|
+
entries: readonly { model: { provider: string; id: string; name?: string; api?: string } }[],
|
|
267
|
+
): PiModelChoice[] {
|
|
268
|
+
const seen = new Set<string>();
|
|
269
|
+
const choices: PiModelChoice[] = [];
|
|
270
|
+
for (const { model } of entries) {
|
|
271
|
+
if (model.api === "pi-virtual") continue;
|
|
272
|
+
const value = piModelValue(model.provider, model.id);
|
|
273
|
+
if (seen.has(value)) continue;
|
|
274
|
+
seen.add(value);
|
|
275
|
+
choices.push({ value, label: model.name ? `${model.name} · ${value}` : value });
|
|
276
|
+
}
|
|
277
|
+
return choices;
|
|
278
|
+
}
|
|
279
|
+
|
|
201
280
|
export function parseModelEffortPair(value: unknown, context: string): ModelEffortPair {
|
|
202
281
|
if (!isRecord(value) || typeof value.model !== "string" || !value.model.trim()) {
|
|
203
282
|
throw new Error(`${context} must contain a model and effort set to "default", "low", "medium", or "high".`);
|
|
@@ -373,8 +452,8 @@ export function capabilityBackend(settings: BroSettings, capability: Capability)
|
|
|
373
452
|
export function selectionForCapability(settings: BroSettings, capability: Capability): BackendSelection {
|
|
374
453
|
const pair = capabilityPair(settings, capability);
|
|
375
454
|
const backend = capabilityBackend(settings, capability);
|
|
376
|
-
if (backend
|
|
377
|
-
const meta = EXTERNAL_BACKENDS[backend
|
|
455
|
+
if (isExternalBackend(backend)) {
|
|
456
|
+
const meta = EXTERNAL_BACKENDS[backend];
|
|
378
457
|
if (!meta.isEffort(pair.effort)) {
|
|
379
458
|
throw new Error(`\`${pair.effort}\` is not supported on the ${meta.label} backend. Run \`/bro config\` to fix this.`);
|
|
380
459
|
}
|