pi-bro 0.22.2 → 0.23.1
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 +39 -0
- package/README.md +5 -7
- package/THIRD_PARTY_NOTICES.md +57 -0
- package/docs/pig-compatibility.md +1 -1
- package/docs/user-guide.md +19 -8
- package/package.json +25 -18
- package/src/advisor.ts +573 -0
- package/{backend.ts → src/backend.ts} +35 -0
- package/src/bro.ts +424 -0
- package/src/btw.ts +572 -0
- package/{config-ui.ts → src/config-ui.ts} +17 -7
- package/src/doctor.ts +541 -0
- package/src/explain-modal.ts +758 -0
- package/{prompt.ts → src/prompt.ts} +3 -1
- package/{review-ui.ts → src/review-ui.ts} +5 -1
- package/{settings.ts → src/settings.ts} +45 -3
- package/src/show.ts +243 -0
- package/{ui-capabilities.ts → src/ui-capabilities.ts} +25 -0
- package/{util.ts → src/util.ts} +16 -0
- package/src/vendor/mermaid.min.js +4328 -0
- package/bro.ts +0 -2967
- /package/{review-modal.ts → src/review-modal.ts} +0 -0
- /package/{review-source.ts → src/review-source.ts} +0 -0
- /package/{review.ts → src/review.ts} +0 -0
- /package/{sources.ts → src/sources.ts} +0 -0
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
ADVISOR_COMPATIBILITY,
|
|
7
|
+
ADVISOR_STEERING_FILE,
|
|
8
|
+
ADVISOR_TOOL_NAME,
|
|
9
|
+
advisorAgyCompatible,
|
|
10
|
+
readAdvisorSteering,
|
|
11
|
+
resolveAdvisorState,
|
|
12
|
+
} from "./advisor.ts";
|
|
13
|
+
import { agyFailureMessage, reviewBackendError } from "./backend.ts";
|
|
14
|
+
import { createConfigModal } from "./config-ui.ts";
|
|
15
|
+
import {
|
|
16
|
+
type AgyEffort,
|
|
17
|
+
type AgyModelFamily,
|
|
18
|
+
type BackendName,
|
|
19
|
+
type BroSettings,
|
|
20
|
+
CAPABILITIES,
|
|
21
|
+
CAPABILITY_LABELS,
|
|
22
|
+
EFFORTS,
|
|
23
|
+
capabilityBackend,
|
|
24
|
+
capabilityOverride,
|
|
25
|
+
capabilityPair,
|
|
26
|
+
isClaudeEffort,
|
|
27
|
+
isCodexEffort,
|
|
28
|
+
isGrokEffort,
|
|
29
|
+
isMuseEffort,
|
|
30
|
+
readPreferences,
|
|
31
|
+
readSettings,
|
|
32
|
+
resolveCapabilitySettings,
|
|
33
|
+
resolveModelEffort,
|
|
34
|
+
writeSettings,
|
|
35
|
+
} from "./settings.ts";
|
|
36
|
+
import { hasBroCustomUi } from "./ui-capabilities.ts";
|
|
37
|
+
import { errorMessage, isRecord, withDoctor } from "./util.ts";
|
|
38
|
+
|
|
39
|
+
export function formatAgyUsage(value: unknown): string {
|
|
40
|
+
if (!isRecord(value) || value.status !== "SUCCESS" || typeof value.response !== "string") {
|
|
41
|
+
throw new Error("Agy returned invalid usage data.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const groups = new Map<string, string[]>();
|
|
45
|
+
for (const line of value.response.trim().split("\n")) {
|
|
46
|
+
const [group, limit, remaining, resetTime] = line.split("\t");
|
|
47
|
+
if (!group || !limit || !remaining) throw new Error("Agy returned invalid usage data.");
|
|
48
|
+
const reset = resetTime ? new Date(resetTime) : undefined;
|
|
49
|
+
const resetText = reset && !Number.isNaN(reset.getTime()) ? ` — resets ${reset.toLocaleString()}` : "";
|
|
50
|
+
const items = groups.get(group) ?? [];
|
|
51
|
+
items.push(`- **${limit}:** ${remaining}${resetText}`);
|
|
52
|
+
groups.set(group, items);
|
|
53
|
+
}
|
|
54
|
+
if (!groups.size) throw new Error("Agy returned no usage information.");
|
|
55
|
+
|
|
56
|
+
const sections = [...groups].map(([group, items]) => `## ${group}\n\n${items.join("\n")}`);
|
|
57
|
+
return `# Agy usage\n\n${sections.join("\n\n")}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseAgyModels(output: string): AgyModelFamily[] {
|
|
61
|
+
// ponytail: Agy 1.1.13 exposes a tab-separated variant list; use structured catalog data when available here.
|
|
62
|
+
const families = new Map<string, AgyModelFamily>();
|
|
63
|
+
for (const line of output.split(/\r?\n/)) {
|
|
64
|
+
const [rawId, ...rawLabel] = line.split("\t");
|
|
65
|
+
if (!rawId?.trim() || !rawLabel.length) continue;
|
|
66
|
+
const id = rawId.trim();
|
|
67
|
+
const label = rawLabel.join(" ").trim();
|
|
68
|
+
const effort = (["low", "medium", "high"] as const).find(
|
|
69
|
+
(value) => id.endsWith(`-${value}`) && label.endsWith(`(${value[0].toUpperCase()}${value.slice(1)})`),
|
|
70
|
+
);
|
|
71
|
+
const familyId = effort ? id.slice(0, -effort.length - 1) : id;
|
|
72
|
+
const family = families.get(familyId) ?? {
|
|
73
|
+
id: familyId,
|
|
74
|
+
label: effort ? label.replace(/\s+\((Low|Medium|High)\)$/, "") : label,
|
|
75
|
+
efforts: [],
|
|
76
|
+
variants: [],
|
|
77
|
+
};
|
|
78
|
+
if (effort && !family.efforts.includes(effort)) family.efforts.push(effort);
|
|
79
|
+
family.variants.push({ id, effort });
|
|
80
|
+
families.set(familyId, family);
|
|
81
|
+
}
|
|
82
|
+
if (!families.size) throw new Error("Agy returned no available models.");
|
|
83
|
+
for (const family of families.values()) {
|
|
84
|
+
family.efforts.sort((a, b) => EFFORTS.indexOf(a) - EFFORTS.indexOf(b));
|
|
85
|
+
}
|
|
86
|
+
return [...families.values()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function listAgyModels(pi: ExtensionAPI, signal?: AbortSignal): Promise<AgyModelFamily[]> {
|
|
90
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
91
|
+
try {
|
|
92
|
+
const result = await pi.exec("agy", ["models"], { cwd: runDirectory, signal, timeout: 30_000 });
|
|
93
|
+
if (signal?.aborted) throw new Error("Canceled.");
|
|
94
|
+
if (result.killed || result.code !== 0) throw new Error(agyFailureMessage("list models", result));
|
|
95
|
+
try {
|
|
96
|
+
return parseAgyModels(result.stdout);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new Error(withDoctor(error));
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function checkAgyVersion(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
106
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
107
|
+
try {
|
|
108
|
+
const result = await pi.exec("agy", ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
109
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
110
|
+
if (result.killed || result.code !== 0) throw new Error(agyFailureMessage("start", result));
|
|
111
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
112
|
+
if (!version) throw new Error("Agy returned no version information. Update Agy, then run `/bro doctor` again.");
|
|
113
|
+
return version;
|
|
114
|
+
} finally {
|
|
115
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveCatalogSettings(
|
|
120
|
+
settings: BroSettings,
|
|
121
|
+
families: AgyModelFamily[],
|
|
122
|
+
): { settings: BroSettings; family?: AgyModelFamily } {
|
|
123
|
+
const resolved = resolveModelEffort({ model: settings.model, effort: settings.effort }, families);
|
|
124
|
+
if (!resolved.family) return { settings };
|
|
125
|
+
return { family: resolved.family, settings: { ...settings, ...resolved.pair } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function checkCliVersion(pi: ExtensionAPI, binary: string, displayName: string, signal: AbortSignal): Promise<string> {
|
|
129
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
130
|
+
try {
|
|
131
|
+
const result = await pi.exec(binary, ["--version"], { cwd: runDirectory, signal, timeout: 10_000 });
|
|
132
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
133
|
+
if (result.killed || result.code !== 0) {
|
|
134
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
135
|
+
throw new Error(
|
|
136
|
+
detail
|
|
137
|
+
? `${displayName} could not start: ${detail}\n\nRun \`/bro doctor\` for setup help.`
|
|
138
|
+
: `${displayName} could not start. Make sure ${displayName} is installed and on PATH, then run \`/bro doctor\`.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const version = result.stdout.trim() || result.stderr.trim();
|
|
142
|
+
if (!version) throw new Error(`${displayName} returned no version information. Update ${displayName}, then run \`/bro doctor\` again.`);
|
|
143
|
+
return version;
|
|
144
|
+
} finally {
|
|
145
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Auth status only: a version/auth answer without a model request. A passing answer here
|
|
150
|
+
// says the CLI starts and reports signed-in state -- it does not imply connectivity.
|
|
151
|
+
async function checkClaudeAuth(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
152
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
153
|
+
try {
|
|
154
|
+
const result = await pi.exec("claude", ["auth", "status"], { cwd: runDirectory, signal, timeout: 15_000 });
|
|
155
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
156
|
+
if (result.killed || result.code !== 0) {
|
|
157
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
158
|
+
throw new Error(detail ? `Claude auth status: ${detail}` : "Claude auth status is unknown. Sign in, then run `/bro doctor`.");
|
|
159
|
+
}
|
|
160
|
+
let status: unknown;
|
|
161
|
+
try { status = JSON.parse(result.stdout); } catch { throw new Error("Claude returned unreadable auth status; sign in and retry /bro doctor."); }
|
|
162
|
+
if (!isRecord(status) || status.loggedIn !== true) throw new Error("Claude is not signed in. Run `claude auth login`.");
|
|
163
|
+
return "authentication configured (not a connectivity test)";
|
|
164
|
+
} finally {
|
|
165
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Auth status only: a version/auth answer without a model request. A passing answer here
|
|
170
|
+
// says the CLI starts and reports signed-in state -- it does not imply connectivity.
|
|
171
|
+
async function checkCodexAuth(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
172
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
173
|
+
try {
|
|
174
|
+
const result = await pi.exec("codex", ["login", "status"], { cwd: runDirectory, signal, timeout: 15_000 });
|
|
175
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
176
|
+
if (result.killed || result.code !== 0) {
|
|
177
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
178
|
+
throw new Error(detail ? `Codex auth status: ${detail}` : "Codex is not logged in. Run `codex login`.");
|
|
179
|
+
}
|
|
180
|
+
const status = result.stdout.trim() || result.stderr.trim();
|
|
181
|
+
if (!status || !/logged in/i.test(status)) throw new Error("Codex is not logged in. Run `codex login`.");
|
|
182
|
+
return "authentication configured (not a connectivity test)";
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
189
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
190
|
+
try {
|
|
191
|
+
const result = await pi.exec(
|
|
192
|
+
"agy",
|
|
193
|
+
["-p", "/usage", "--output-format", "json", "--print-timeout", "30s", "--sandbox"],
|
|
194
|
+
{ cwd: runDirectory, signal, timeout: 35_000 },
|
|
195
|
+
);
|
|
196
|
+
if (signal.aborted) throw new Error("Canceled.");
|
|
197
|
+
if (result.killed || result.code !== 0) throw new Error(agyFailureMessage("check account usage", result));
|
|
198
|
+
try {
|
|
199
|
+
return formatAgyUsage(JSON.parse(result.stdout));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new Error(withDoctor(error instanceof SyntaxError ? "Agy returned invalid usage data." : error));
|
|
202
|
+
}
|
|
203
|
+
} finally {
|
|
204
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function doctorReport(pi: ExtensionAPI, ctx: ExtensionCommandContext, signal: AbortSignal): Promise<string> {
|
|
209
|
+
const lines: string[] = [];
|
|
210
|
+
let failed = false;
|
|
211
|
+
let settings: BroSettings | undefined;
|
|
212
|
+
let models: AgyModelFamily[] | undefined;
|
|
213
|
+
let agyVersion: string | undefined;
|
|
214
|
+
const pass = (name: string, detail: string) => lines.push(`- ✓ **${name}:** ${detail}`);
|
|
215
|
+
const fail = (name: string, error: unknown) => {
|
|
216
|
+
failed = true;
|
|
217
|
+
lines.push(`- ✗ **${name}:** ${errorMessage(error)}`);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
settings = await readSettings();
|
|
222
|
+
pass("Settings", `valid · mode: ${settings.mode}`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
fail("Settings", error);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const preferences = await readPreferences();
|
|
229
|
+
pass("Preferences", preferences ? `${preferences.length.toLocaleString("en-US")} characters · used by explain, show, btw` : "none — /bro preferences to add some");
|
|
230
|
+
} catch (error) {
|
|
231
|
+
fail("Preferences", error);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Probe only the backends some feature actually selects: a Claude/Grok/Codex/Muse-only setup never
|
|
235
|
+
// requires Agy to be installed, and vice versa.
|
|
236
|
+
const backendInUse = (backend: BackendName) => settings
|
|
237
|
+
? CAPABILITIES.some(capability => capabilityBackend(settings, capability) === backend)
|
|
238
|
+
: backend === "agy";
|
|
239
|
+
const agyInUse = backendInUse("agy");
|
|
240
|
+
const claudeInUse = backendInUse("claude");
|
|
241
|
+
const grokInUse = backendInUse("grok");
|
|
242
|
+
const codexInUse = backendInUse("codex");
|
|
243
|
+
const museInUse = backendInUse("muse");
|
|
244
|
+
|
|
245
|
+
let agyStarted = false;
|
|
246
|
+
if (agyInUse) {
|
|
247
|
+
try {
|
|
248
|
+
agyVersion = await checkAgyVersion(pi, signal);
|
|
249
|
+
pass("Agy", agyVersion);
|
|
250
|
+
agyStarted = true;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (signal.aborted) throw error;
|
|
253
|
+
fail("Agy", error);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (agyStarted) {
|
|
257
|
+
try {
|
|
258
|
+
models = await listAgyModels(pi, signal);
|
|
259
|
+
pass("Model catalog", `${models.length} model${models.length === 1 ? "" : "s"} available`);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (signal.aborted) throw error;
|
|
262
|
+
fail("Model catalog", error);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await checkAgyUsage(pi, signal);
|
|
267
|
+
pass("Account", "connected");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (signal.aborted) throw error;
|
|
270
|
+
fail("Account", error);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
pass("Agy", "not probed — no feature selects the Agy backend");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Version and auth status only, never a model request. A passing answer says the CLI
|
|
278
|
+
// starts and reports signed-in state -- it does not imply connectivity.
|
|
279
|
+
if (claudeInUse) {
|
|
280
|
+
try {
|
|
281
|
+
pass("Claude", await checkCliVersion(pi, "claude", "Claude", signal));
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (signal.aborted) throw error;
|
|
284
|
+
fail("Claude", error);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
pass("Claude auth", await checkClaudeAuth(pi, signal));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (signal.aborted) throw error;
|
|
290
|
+
fail("Claude auth", error);
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
pass("Claude", "not probed — no feature selects the Claude backend");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Version only, never a model request. No Grok auth probe exists, so a passing
|
|
297
|
+
// version says the CLI starts — it says nothing about auth or connectivity.
|
|
298
|
+
if (grokInUse) {
|
|
299
|
+
try {
|
|
300
|
+
pass("Grok", await checkCliVersion(pi, "grok", "Grok", signal));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (signal.aborted) throw error;
|
|
303
|
+
fail("Grok", error);
|
|
304
|
+
}
|
|
305
|
+
pass("Grok auth", "unverified — no auth probe exists (version does not imply auth or connectivity)");
|
|
306
|
+
} else {
|
|
307
|
+
pass("Grok", "not probed — no feature selects the Grok backend");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Version and auth status only, never a model request.
|
|
311
|
+
if (codexInUse) {
|
|
312
|
+
try {
|
|
313
|
+
pass("Codex", await checkCliVersion(pi, "codex", "Codex", signal));
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (signal.aborted) throw error;
|
|
316
|
+
fail("Codex", error);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
pass("Codex auth", await checkCodexAuth(pi, signal));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (signal.aborted) throw error;
|
|
322
|
+
fail("Codex auth", error);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
pass("Codex", "not probed — no feature selects the Codex backend");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Version only, never a model request. No Muse auth probe exists, so a passing
|
|
329
|
+
// version says the CLI starts — it says nothing about auth or connectivity.
|
|
330
|
+
if (museInUse) {
|
|
331
|
+
try {
|
|
332
|
+
pass("Muse", await checkCliVersion(pi, "muse", "Muse", signal));
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (signal.aborted) throw error;
|
|
335
|
+
fail("Muse", error);
|
|
336
|
+
}
|
|
337
|
+
pass("Muse auth", "unverified — no auth probe exists (version does not imply auth or connectivity)");
|
|
338
|
+
} else {
|
|
339
|
+
pass("Muse", "not probed — no feature selects the Muse backend");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (settings) {
|
|
343
|
+
const defaultBackend = settings.backend ?? "agy";
|
|
344
|
+
if (defaultBackend === "claude") {
|
|
345
|
+
pass("Selected model", `claude \`${settings.model}\``);
|
|
346
|
+
if (!isClaudeEffort(settings.effort)) {
|
|
347
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
348
|
+
} else if (settings.effort === "default") {
|
|
349
|
+
pass("Reasoning effort", "built into the selected model");
|
|
350
|
+
} else {
|
|
351
|
+
pass("Reasoning effort", settings.effort);
|
|
352
|
+
}
|
|
353
|
+
} else if (defaultBackend === "grok") {
|
|
354
|
+
pass("Selected model", `grok \`${settings.model}\``);
|
|
355
|
+
if (!isGrokEffort(settings.effort)) {
|
|
356
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
357
|
+
} else if (settings.effort === "default") {
|
|
358
|
+
pass("Reasoning effort", "built into the selected model");
|
|
359
|
+
} else {
|
|
360
|
+
pass("Reasoning effort", settings.effort);
|
|
361
|
+
}
|
|
362
|
+
} else if (defaultBackend === "codex") {
|
|
363
|
+
pass("Selected model", `codex \`${settings.model}\``);
|
|
364
|
+
if (!isCodexEffort(settings.effort)) {
|
|
365
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
366
|
+
} else if (settings.effort === "default") {
|
|
367
|
+
pass("Reasoning effort", "built into the selected model");
|
|
368
|
+
} else {
|
|
369
|
+
pass("Reasoning effort", settings.effort);
|
|
370
|
+
}
|
|
371
|
+
} else if (defaultBackend === "muse") {
|
|
372
|
+
pass("Selected model", `muse \`${settings.model}\``);
|
|
373
|
+
if (!isMuseEffort(settings.effort)) {
|
|
374
|
+
fail("Reasoning effort", `\`${settings.effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
375
|
+
} else if (settings.effort === "default") {
|
|
376
|
+
pass("Reasoning effort", "built into the selected model");
|
|
377
|
+
} else {
|
|
378
|
+
pass("Reasoning effort", settings.effort);
|
|
379
|
+
}
|
|
380
|
+
} else if (models) {
|
|
381
|
+
const current = resolveCatalogSettings(settings, models);
|
|
382
|
+
if (!current.family) {
|
|
383
|
+
fail("Selected model", `\`${settings.model}\` is unavailable. Run \`/bro config\` to choose another.`);
|
|
384
|
+
} else {
|
|
385
|
+
pass("Selected model", `agy \`${current.family.id}\``);
|
|
386
|
+
const effort = current.settings.effort;
|
|
387
|
+
if (!current.family.efforts.length && effort === "default") {
|
|
388
|
+
pass("Reasoning effort", "built into the selected model");
|
|
389
|
+
} else if (effort !== "default" && current.family.efforts.includes(effort as AgyEffort)) {
|
|
390
|
+
pass("Reasoning effort", effort);
|
|
391
|
+
} else {
|
|
392
|
+
fail("Reasoning effort", `\`${effort}\` is unsupported. Run \`/bro config\` to choose another.`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const capability of CAPABILITIES) {
|
|
398
|
+
const label = CAPABILITY_LABELS[capability];
|
|
399
|
+
const override = capabilityOverride(settings, capability);
|
|
400
|
+
const backend = capabilityBackend(settings, capability);
|
|
401
|
+
const pair = capabilityPair(settings, capability);
|
|
402
|
+
const reviewError = capability === "review" ? reviewBackendError(backend) : undefined;
|
|
403
|
+
if (reviewError) { lines.push(`- ℹ **${label}:** needs a Claude or Muse override in \`/bro config\` for model calls; saved reviews remain readable.`); continue; }
|
|
404
|
+
if (backend === "claude") {
|
|
405
|
+
if (!isClaudeEffort(pair.effort)) {
|
|
406
|
+
fail(label, `\`${pair.effort}\` is unsupported for claude \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
pass(
|
|
410
|
+
label,
|
|
411
|
+
override
|
|
412
|
+
? `override claude \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
413
|
+
: `claude \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
414
|
+
);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (backend === "grok") {
|
|
418
|
+
if (!isGrokEffort(pair.effort)) {
|
|
419
|
+
fail(label, `\`${pair.effort}\` is unsupported for grok \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
pass(
|
|
423
|
+
label,
|
|
424
|
+
override
|
|
425
|
+
? `override grok \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
426
|
+
: `grok \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
427
|
+
);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (backend === "codex") {
|
|
431
|
+
if (!isCodexEffort(pair.effort)) {
|
|
432
|
+
fail(label, `\`${pair.effort}\` is unsupported for codex \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
pass(
|
|
436
|
+
label,
|
|
437
|
+
override
|
|
438
|
+
? `override codex \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
439
|
+
: `codex \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
440
|
+
);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (backend === "muse") {
|
|
444
|
+
if (!isMuseEffort(pair.effort)) {
|
|
445
|
+
fail(label, `\`${pair.effort}\` is unsupported for muse \`${pair.model}\`. Run \`/bro config\` to fix this.`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
pass(
|
|
449
|
+
label,
|
|
450
|
+
override
|
|
451
|
+
? `override muse \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`}`
|
|
452
|
+
: `muse \`${pair.model}\`${pair.effort === "default" ? "" : ` (${pair.effort})`} · using the shared default`,
|
|
453
|
+
);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (!models) continue;
|
|
457
|
+
const resolved = resolveCapabilitySettings(settings, capability, models);
|
|
458
|
+
if (!resolved.family) {
|
|
459
|
+
fail(label, `\`${resolved.pair.model}\` is unavailable. Run \`/bro config\` to fix this override.`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const effortOk = !resolved.family.efforts.length
|
|
463
|
+
? resolved.pair.effort === "default"
|
|
464
|
+
: resolved.pair.effort !== "default" && resolved.family.efforts.includes(resolved.pair.effort as AgyEffort);
|
|
465
|
+
if (!effortOk) {
|
|
466
|
+
fail(label, `\`${resolved.pair.effort}\` is unsupported for \`${resolved.family.id}\`. Run \`/bro config\` to fix this.`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
pass(
|
|
470
|
+
label,
|
|
471
|
+
override
|
|
472
|
+
? `override agy \`${resolved.family.id}\`${resolved.pair.effort === "default" ? "" : ` (${resolved.pair.effort})`}`
|
|
473
|
+
: "using the shared default",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// bro_advisor has no on/off preference to compare against -- it is only ever gated by this
|
|
479
|
+
// host's own tool restrictions, which this reads via pi.getAllTools()/pi.getActiveTools() and
|
|
480
|
+
// never overrides. Exposed-but-inactive is the one genuine anomaly worth failing on.
|
|
481
|
+
const advisorExposed = pi.getAllTools().some((tool) => tool.name === ADVISOR_TOOL_NAME);
|
|
482
|
+
const advisorActive = pi.getActiveTools().includes(ADVISOR_TOOL_NAME);
|
|
483
|
+
if (advisorExposed && !advisorActive) fail("Advisor tool", "bro_advisor is exposed but not active in this session. Run /reload.");
|
|
484
|
+
else pass("Advisor tool", advisorExposed ? "bro_advisor is exposed and active" : "bro_advisor is not exposed by this host (tool restriction, or the extension has not finished loading)");
|
|
485
|
+
const sessionSteering = resolveAdvisorState(ctx.sessionManager.getBranch()).steering.trim();
|
|
486
|
+
pass("Advisor steering (session)", sessionSteering ? `${sessionSteering.length.toLocaleString("en-US")} characters · /bro advisor-steer` : "none — /bro advisor-steer to set for this session");
|
|
487
|
+
try {
|
|
488
|
+
const durable = await readAdvisorSteering();
|
|
489
|
+
pass("Advisor steering (durable)", durable ? `${durable.length.toLocaleString("en-US")} characters · ${ADVISOR_STEERING_FILE}` : `none — ${ADVISOR_STEERING_FILE}`);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
fail("Advisor steering (durable)", error);
|
|
492
|
+
}
|
|
493
|
+
if (settings && capabilityBackend(settings, "advisor") === "claude") {
|
|
494
|
+
pass("Advisor compatibility", "Claude backend — no Agy version floor applies");
|
|
495
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "grok") {
|
|
496
|
+
pass("Advisor compatibility", "Grok backend — no Agy version floor applies; auth/connectivity unverified (no probe)");
|
|
497
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "codex") {
|
|
498
|
+
pass("Advisor compatibility", "Codex backend — no Agy version floor applies");
|
|
499
|
+
} else if (settings && capabilityBackend(settings, "advisor") === "muse") {
|
|
500
|
+
pass("Advisor compatibility", "Muse backend — no Agy version floor applies; auth/connectivity unverified (no probe)");
|
|
501
|
+
} else if (agyVersion && advisorAgyCompatible(agyVersion)) pass("Advisor compatibility", ADVISOR_COMPATIBILITY);
|
|
502
|
+
else if (agyVersion) fail("Advisor compatibility", `installed \`${agyVersion}\`; requires Agy >=1.1.15. Run \`agy update\`.`);
|
|
503
|
+
|
|
504
|
+
return `# Bro doctor\n\n${lines.join("\n")}\n\n**${failed ? "Bro needs attention." : "Bro is ready."}**\n\n${
|
|
505
|
+
failed
|
|
506
|
+
? "Fix the failed items, then press **R** to check again."
|
|
507
|
+
: "No assistant response was sent and no model turn was run. Version/auth checks do not imply connectivity (Grok and Muse auth are unverified — no probe exists)."
|
|
508
|
+
}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export async function showBroConfigModal(ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
512
|
+
if (!hasBroCustomUi(ctx)) {
|
|
513
|
+
ctx.ui.notify("Use /bro config in Pi's interactive UI.", "warning");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
let settings: BroSettings;
|
|
517
|
+
try {
|
|
518
|
+
settings = await readSettings();
|
|
519
|
+
} catch (error) {
|
|
520
|
+
ctx.ui.notify(withDoctor(error), "error");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// Keep configuration repair accessible even when the Agy catalog is unavailable.
|
|
524
|
+
let families: AgyModelFamily[];
|
|
525
|
+
try {
|
|
526
|
+
families = await listAgyModels(pi);
|
|
527
|
+
} catch {
|
|
528
|
+
families = [];
|
|
529
|
+
ctx.ui.notify("Agy model catalog unavailable — showing other backend settings without Agy choices.", "warning");
|
|
530
|
+
}
|
|
531
|
+
await ctx.ui.custom<void>(createConfigModal(settings, families, writeSettings), {
|
|
532
|
+
overlay: true,
|
|
533
|
+
overlayOptions: {
|
|
534
|
+
width: "78%",
|
|
535
|
+
minWidth: 48,
|
|
536
|
+
maxHeight: "78%",
|
|
537
|
+
anchor: "top-center",
|
|
538
|
+
margin: { top: 1, left: 2, right: 2 },
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
}
|