@phi-code-admin/phi-code 0.86.0 → 0.87.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/docs/fork-policy.md +18 -0
  3. package/extensions/phi/agents.ts +7 -4
  4. package/extensions/phi/benchmark.ts +129 -49
  5. package/extensions/phi/browser.ts +10 -37
  6. package/extensions/phi/btw/btw.ts +1 -7
  7. package/extensions/phi/chrome/index.ts +1283 -741
  8. package/extensions/phi/commit.ts +9 -14
  9. package/extensions/phi/goal/index.ts +10 -33
  10. package/extensions/phi/init.ts +37 -47
  11. package/extensions/phi/keys.ts +2 -7
  12. package/extensions/phi/mcp/callback-server.ts +162 -165
  13. package/extensions/phi/mcp/config.ts +122 -136
  14. package/extensions/phi/mcp/errors.ts +18 -23
  15. package/extensions/phi/mcp/index.ts +322 -355
  16. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  17. package/extensions/phi/mcp/server-manager.ts +390 -413
  18. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  19. package/extensions/phi/memory.ts +2 -2
  20. package/extensions/phi/models.ts +27 -26
  21. package/extensions/phi/orchestrator.ts +338 -229
  22. package/extensions/phi/productivity.ts +4 -2
  23. package/extensions/phi/providers/agent-def.ts +1 -1
  24. package/extensions/phi/providers/alibaba.ts +56 -7
  25. package/extensions/phi/providers/context-window.ts +4 -1
  26. package/extensions/phi/providers/live-models.ts +5 -20
  27. package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
  28. package/extensions/phi/providers/phase-machine.ts +220 -0
  29. package/extensions/phi/setup.ts +196 -169
  30. package/extensions/phi/skill-loader.ts +14 -17
  31. package/extensions/phi/smart-router.ts +6 -6
  32. package/extensions/phi/web-search.ts +90 -50
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.87.0] - 2026-07-10
4
+
5
+ ### Added
6
+
7
+ - **The /plan orchestrator's phase state machine is now unit-tested.** Its
8
+ decision core — what to do after each phase given the messages and the report
9
+ (user abort > auth error > transient retry > BLOCKED pause > review-FAIL fix
10
+ cycle > continue) — is extracted into a pure module
11
+ (`extensions/phi/providers/phase-machine.ts`) and covered by 32 tests,
12
+ including the cases where a model deviates from the text contract: a missing
13
+ or unparseable `VERDICT:` line is now surfaced to the user instead of being
14
+ silently treated as a pass, and zero-tool-call / queue-race paths are pinned.
15
+ The `agent_end` hook keeps the exact same behavior but now only interprets
16
+ the decision (I/O), it no longer decides.
17
+ - **Section parsing tolerates the header shapes models actually emit.**
18
+ `extractSection` (HANDOFF / BLOCKING) now handles a markdown heading, a
19
+ standalone bold label (`**HANDOFF**`) or a plain `LABEL:` line, and splits a
20
+ report written entirely with bold labels — previously a bold-only `**HANDOFF**`
21
+ returned nothing. Added regression tests for these deviations.
22
+
23
+ ### Changed
24
+
25
+ - **extensions/ and the sigma-\* packages are now linted.** Biome previously
26
+ skipped them entirely (typecheck covered them, lint did not); they are in the
27
+ linted set and the whole repo is clean (`biome ci .` passes on 752 files,
28
+ zero warnings). Idiomatic-but-flagged regex-exec loops in web-search were
29
+ rewritten through an `execAll` generator rather than silenced with rule
30
+ overrides.
31
+ - **Releases are reproducible.** `npm run build` no longer regenerates the
32
+ model catalog from models.dev — the committed `packages/ai/src/*.generated.ts`
33
+ is the source of truth and `build` just compiles it, so a publish ships
34
+ exactly what is in git. Regeneration is an explicit `npm run generate
35
+ --workspace=packages/ai` step; a scheduled `refresh-models.yml` workflow
36
+ opens a PR weekly so the catalog still stays fresh with a human in the loop.
37
+
38
+ ### CI / process
39
+
40
+ - **CI now gates lint, format, types and catalog-cleanliness on every PR**
41
+ (`biome ci`, `tsgo --noEmit`, and `git diff --exit-code` on the generated
42
+ catalog), not just build + test — fork PRs that bypass the local pre-commit
43
+ hook are covered. A pull-request template makes the discipline checklist
44
+ explicit, and `docs/fork-policy.md` documents the reproducible-catalog flow.
45
+
3
46
  ## [0.86.0] - 2026-07-10
4
47
 
5
48
  ### Changed
@@ -61,3 +61,21 @@ use these constants, never a hardcoded "pi" or "phi".**
61
61
  `agents/`, `skills/`, `config/`, the `sigma-*` packages, and the browser and
62
62
  camoufox packages are phi-code territory: normal engineering rules apply,
63
63
  no merge constraints.
64
+
65
+ ## Reproducible releases: the generated model catalog
66
+
67
+ `packages/ai/src/models.generated.ts` (and `image-models.generated.ts`) are
68
+ **committed source files and the source of truth for every build**. `npm run
69
+ build` compiles them with `tsgo`; it does NOT fetch models.dev, so a build/
70
+ publish always ships exactly what is in git — releases are reproducible.
71
+
72
+ Refreshing the catalog is a separate, explicit action:
73
+
74
+ - `npm run generate --workspace=packages/ai` regenerates from models.dev.
75
+ - The maintainer reviews the diff, commits it, and bumps `packages/ai`.
76
+ - `.github/workflows/refresh-models.yml` does this weekly and opens a PR, so the
77
+ catalog never rots without a human in the loop.
78
+
79
+ CI enforces the invariant: the `check` job fails if the committed catalog
80
+ differs from a clean checkout (`git diff --exit-code`), catching both a stray
81
+ hand-edit and any build that regenerated it.
@@ -23,15 +23,18 @@ export default function agentsExtension(pi: ExtensionAPI) {
23
23
  const arg = args.trim().toLowerCase();
24
24
 
25
25
  if (agents.length === 0) {
26
- ctx.ui.notify("No agent definitions found.\n\nCreate agent files in:\n- `.phi/agents/` (project)\n- `~/.phi/agent/agents/` (global)\n\nFormat: Markdown with YAML frontmatter (name, description, tools, model).", "info");
26
+ ctx.ui.notify(
27
+ "No agent definitions found.\n\nCreate agent files in:\n- `.phi/agents/` (project)\n- `~/.phi/agent/agents/` (global)\n\nFormat: Markdown with YAML frontmatter (name, description, tools, model).",
28
+ "info",
29
+ );
27
30
  return;
28
31
  }
29
32
 
30
33
  // Show specific agent details
31
34
  if (arg && arg !== "list") {
32
- const agent = agents.find(a => a.name.toLowerCase() === arg);
35
+ const agent = agents.find((a) => a.name.toLowerCase() === arg);
33
36
  if (!agent) {
34
- ctx.ui.notify(`Agent "${arg}" not found. Available: ${agents.map(a => a.name).join(", ")}`, "warning");
37
+ ctx.ui.notify(`Agent "${arg}" not found. Available: ${agents.map((a) => a.name).join(", ")}`, "warning");
35
38
  return;
36
39
  }
37
40
 
@@ -39,7 +42,7 @@ export default function agentsExtension(pi: ExtensionAPI) {
39
42
 
40
43
  📝 ${agent.description}
41
44
  🤖 Model: \`${agent.model}\`
42
- 🔧 Tools: ${agent.tools.map(t => `\`${t}\``).join(", ")}
45
+ 🔧 Tools: ${agent.tools.map((t) => `\`${t}\``).join(", ")}
43
46
  📁 Source: ${agent.source} (\`${agent.filePath}\`)
44
47
 
45
48
  **System Prompt:**
@@ -17,10 +17,10 @@
17
17
  * - /benchmark clear — Clear all results
18
18
  */
19
19
 
20
- import type { ExtensionAPI, ExtensionContext } from "phi-code";
21
- import { writeFile, mkdir, readFile, access } from "node:fs/promises";
22
- import { join } from "node:path";
20
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
23
21
  import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
+ import type { ExtensionAPI, ExtensionContext } from "phi-code";
24
24
 
25
25
  // ─── Types ───────────────────────────────────────────────────────────────
26
26
 
@@ -88,12 +88,12 @@ Respond with ONLY the function code, no explanations.`,
88
88
  { test: /(<=\s*0|===?\s*0|<\s*1)/.test(code), detail: "Handles edge case n=0" },
89
89
  { test: /(===?\s*1|<=\s*1)/.test(code), detail: "Handles edge case n=1" },
90
90
  ];
91
- const passed = checks.filter(c => c.test).length;
91
+ const passed = checks.filter((c) => c.test).length;
92
92
  const total = checks.length;
93
93
  return {
94
94
  passed: passed >= 5,
95
95
  score: Math.round((passed / total) * 100),
96
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
96
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
97
97
  };
98
98
  },
99
99
  },
@@ -125,16 +125,29 @@ Explain the bug and provide the fixed code.`,
125
125
  validate: (response: string) => {
126
126
  const lower = response.toLowerCase();
127
127
  const checks = [
128
- { test: /reference|shallow|copy|spread|\[\.\.\./.test(lower), detail: "Identifies reference/copy issue" },
129
- { test: /\[\.\.\.arr1\]|\[\.\.\.arr1,|Array\.from|\.slice\(\)|structuredClone|concat/.test(response), detail: "Uses spread/copy/concat fix" },
128
+ {
129
+ test: /reference|shallow|copy|spread|\[\.\.\./.test(lower),
130
+ detail: "Identifies reference/copy issue",
131
+ },
132
+ {
133
+ test: /\[\.\.\.arr1\]|\[\.\.\.arr1,|Array\.from|\.slice\(\)|structuredClone|concat/.test(response),
134
+ detail: "Uses spread/copy/concat fix",
135
+ },
130
136
  { test: /mutate|modify|original|side.?effect/.test(lower), detail: "Explains the mutation problem" },
131
- { test: /const result\s*=\s*\[/.test(response) || /\.slice\(/.test(response) || /\.concat\(/.test(response) || /Array\.from/.test(response), detail: "Creates new array in fix" },
137
+ {
138
+ test:
139
+ /const result\s*=\s*\[/.test(response) ||
140
+ /\.slice\(/.test(response) ||
141
+ /\.concat\(/.test(response) ||
142
+ /Array\.from/.test(response),
143
+ detail: "Creates new array in fix",
144
+ },
132
145
  ];
133
- const passed = checks.filter(c => c.test).length;
146
+ const passed = checks.filter((c) => c.test).length;
134
147
  return {
135
148
  passed: passed >= 3,
136
149
  score: Math.round((passed / checks.length) * 100),
137
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
150
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
138
151
  };
139
152
  },
140
153
  },
@@ -170,11 +183,11 @@ Provide a structured plan with specific files to create/modify, dependencies to
170
183
  { test: /env|secret|config/.test(lower), detail: "Addresses secret management" },
171
184
  { test: /step|phase|\d\.|create|modify|add/.test(lower), detail: "Provides structured steps" },
172
185
  ];
173
- const passed = checks.filter(c => c.test).length;
186
+ const passed = checks.filter((c) => c.test).length;
174
187
  return {
175
188
  passed: passed >= 6,
176
189
  score: Math.round((passed / checks.length) * 100),
177
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
190
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
178
191
  };
179
192
  },
180
193
  },
@@ -219,9 +232,15 @@ Output ONLY the JSON.`,
219
232
  checks.push({ test: obj?.name?.first === "Alice", detail: 'name.first = "Alice"' });
220
233
  checks.push({ test: obj?.name?.last === "Smith", detail: 'name.last = "Smith"' });
221
234
  checks.push({ test: obj?.email === "alice@example.com", detail: "Correct email" });
222
- checks.push({ test: typeof obj?.profile?.age === "number" && obj.profile.age === 28, detail: "Age is number 28" });
235
+ checks.push({
236
+ test: typeof obj?.profile?.age === "number" && obj.profile.age === 28,
237
+ detail: "Age is number 28",
238
+ });
223
239
  checks.push({ test: obj?.preferences?.theme === "dark", detail: 'theme = "dark"' });
224
- checks.push({ test: obj?.preferences?.notifications?.email === true, detail: "email notifications = true" });
240
+ checks.push({
241
+ test: obj?.preferences?.notifications?.email === true,
242
+ detail: "email notifications = true",
243
+ });
225
244
  } catch {
226
245
  checks.push({ test: false, detail: "Valid JSON (parse failed)" });
227
246
  checks.push({ test: false, detail: "name.first" });
@@ -232,11 +251,11 @@ Output ONLY the JSON.`,
232
251
  checks.push({ test: false, detail: "notifications" });
233
252
  }
234
253
 
235
- const passed = checks.filter(c => c.test).length;
254
+ const passed = checks.filter((c) => c.test).length;
236
255
  return {
237
256
  passed: passed >= 5,
238
257
  score: Math.round((passed / checks.length) * 100),
239
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
258
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
240
259
  };
241
260
  },
242
261
  },
@@ -248,7 +267,11 @@ Output ONLY the JSON.`,
248
267
  weight: 1,
249
268
  prompt: `Reply with exactly this text and nothing else: "Hello, World!"`,
250
269
  validate: (response: string) => {
251
- const trimmed = response.trim().replace(/^["']|["']$/g, "").replace(/```\w*\n?/g, "").trim();
270
+ const trimmed = response
271
+ .trim()
272
+ .replace(/^["']|["']$/g, "")
273
+ .replace(/```\w*\n?/g, "")
274
+ .trim();
252
275
  const exact = trimmed === "Hello, World!";
253
276
  const close = trimmed.toLowerCase().includes("hello, world");
254
277
  return {
@@ -287,15 +310,23 @@ Be specific and technical.`,
287
310
  { test: /memory.?leak|leak/.test(lower), detail: "Identifies memory leak" },
288
311
  { test: /stream|buffer|file|upload|temp|cleanup/.test(lower), detail: "Links to file upload handling" },
289
312
  { test: /close|destroy|cleanup|dispose|gc|garbage/.test(lower), detail: "Suggests resource cleanup" },
290
- { test: /event.?listener|handler|remove|off/.test(lower) || /stream|pipe/.test(lower), detail: "Checks for handler/stream leaks" },
291
- { test: /heapdump|heap.?snapshot|inspect|profile|--max-old-space/.test(lower) || /process\.memoryUsage/.test(lower), detail: "Suggests debugging tools" },
313
+ {
314
+ test: /event.?listener|handler|remove|off/.test(lower) || /stream|pipe/.test(lower),
315
+ detail: "Checks for handler/stream leaks",
316
+ },
317
+ {
318
+ test:
319
+ /heapdump|heap.?snapshot|inspect|profile|--max-old-space/.test(lower) ||
320
+ /process\.memoryUsage/.test(lower),
321
+ detail: "Suggests debugging tools",
322
+ },
292
323
  { test: /monitor|alert|metric|prometheus|grafana|threshold/.test(lower), detail: "Suggests monitoring" },
293
324
  ];
294
- const passed = checks.filter(c => c.test).length;
325
+ const passed = checks.filter((c) => c.test).length;
295
326
  return {
296
327
  passed: passed >= 4,
297
328
  score: Math.round((passed / checks.length) * 100),
298
- details: checks.map(c => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
329
+ details: checks.map((c) => `${c.test ? "✅" : "❌"} ${c.detail}`).join("\n"),
299
330
  };
300
331
  },
301
332
  },
@@ -322,7 +353,16 @@ function getProviderConfigs(): ProviderConfig[] {
322
353
  name: "alibaba-codingplan",
323
354
  envVar: "ALIBABA_CODING_PLAN_KEY",
324
355
  baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
325
- models: ["qwen3.5-plus", "qwen3-max-2026-01-23", "qwen3-coder-plus", "qwen3-coder-next", "kimi-k2.5", "glm-5", "glm-4.7", "MiniMax-M2.5"],
356
+ models: [
357
+ "qwen3.5-plus",
358
+ "qwen3-max-2026-01-23",
359
+ "qwen3-coder-plus",
360
+ "qwen3-coder-next",
361
+ "kimi-k2.5",
362
+ "glm-5",
363
+ "glm-4.7",
364
+ "MiniMax-M2.5",
365
+ ],
326
366
  },
327
367
  {
328
368
  name: "openai",
@@ -386,14 +426,16 @@ async function getAvailableModels(): Promise<Array<{ id: string; provider: strin
386
426
  for (const m of providerConfig.models) {
387
427
  const modelId = typeof m === "string" ? m : m.id;
388
428
  // Skip if already added from env vars
389
- if (!models.some(existing => existing.id === modelId && existing.baseUrl === baseUrl)) {
429
+ if (!models.some((existing) => existing.id === modelId && existing.baseUrl === baseUrl)) {
390
430
  models.push({ id: modelId, provider: id, baseUrl, apiKey });
391
431
  }
392
432
  }
393
433
  }
394
434
  }
395
435
  }
396
- } catch { /* ignore parse errors */ }
436
+ } catch {
437
+ /* ignore parse errors */
438
+ }
397
439
  }
398
440
 
399
441
  // 3. Try to detect LM Studio (port 1234) and Ollama (port 11434) directly
@@ -402,7 +444,7 @@ async function getAvailableModels(): Promise<Array<{ id: string; provider: strin
402
444
  { name: "ollama", port: 11434, baseUrl: "http://localhost:11434/v1" },
403
445
  ]) {
404
446
  // Skip if already discovered via models.json
405
- if (models.some(m => m.baseUrl === local.baseUrl)) continue;
447
+ if (models.some((m) => m.baseUrl === local.baseUrl)) continue;
406
448
 
407
449
  try {
408
450
  const controller = new AbortController();
@@ -411,16 +453,18 @@ async function getAvailableModels(): Promise<Array<{ id: string; provider: strin
411
453
  clearTimeout(timeout);
412
454
 
413
455
  if (resp.ok) {
414
- const data = await resp.json() as any;
456
+ const data = (await resp.json()) as any;
415
457
  const modelList = data?.data || [];
416
458
  for (const m of modelList) {
417
459
  const modelId = m.id || m.name;
418
- if (modelId && !models.some(existing => existing.id === modelId)) {
460
+ if (modelId && !models.some((existing) => existing.id === modelId)) {
419
461
  models.push({ id: modelId, provider: local.name, baseUrl: local.baseUrl, apiKey: "local" });
420
462
  }
421
463
  }
422
464
  }
423
- } catch { /* not running */ }
465
+ } catch {
466
+ /* not running */
467
+ }
424
468
  }
425
469
 
426
470
  return models;
@@ -514,7 +558,7 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
514
558
  for (let testIdx = 0; testIdx < tests.length; testIdx++) {
515
559
  const test = tests[testIdx];
516
560
  // Rate limiting: 1.5s between API calls to avoid throttling
517
- if (testIdx > 0) await new Promise(r => setTimeout(r, 1500));
561
+ if (testIdx > 0) await new Promise((r) => setTimeout(r, 1500));
518
562
  ctx.ui.notify(` ⏳ ${test.category}: ${test.name}...`, "info");
519
563
 
520
564
  try {
@@ -586,15 +630,24 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
586
630
  report += "**Leaderboard:**\n";
587
631
  sorted.forEach((r, i) => {
588
632
  const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `${i + 1}.`;
589
- const tier = r.totalScore >= 80 ? "S" : r.totalScore >= 65 ? "A" : r.totalScore >= 50 ? "B" : r.totalScore >= 35 ? "C" : "D";
633
+ const tier =
634
+ r.totalScore >= 80
635
+ ? "S"
636
+ : r.totalScore >= 65
637
+ ? "A"
638
+ : r.totalScore >= 50
639
+ ? "B"
640
+ : r.totalScore >= 35
641
+ ? "C"
642
+ : "D";
590
643
  report += `${medal} **${r.modelId}** — ${r.totalScore}/100 [${tier}] (avg ${r.avgTimeMs}ms)\n`;
591
644
  });
592
645
 
593
646
  // Category breakdown
594
647
  report += "\n**Category Breakdown:**\n```\n";
595
- const header = "Model".padEnd(25) + categories.map(c => c.substring(0, 8).padEnd(10)).join("") + "TOTAL\n";
648
+ const header = `${"Model".padEnd(25) + categories.map((c) => c.substring(0, 8).padEnd(10)).join("")}TOTAL\n`;
596
649
  report += header;
597
- report += "-".repeat(header.length) + "\n";
650
+ report += `${"-".repeat(header.length)}\n`;
598
651
 
599
652
  for (const r of sorted) {
600
653
  let line = r.modelId.substring(0, 24).padEnd(25);
@@ -603,7 +656,7 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
603
656
  line += String(score).padEnd(10);
604
657
  }
605
658
  line += String(r.totalScore);
606
- report += line + "\n";
659
+ report += `${line}\n`;
607
660
  }
608
661
  report += "```\n";
609
662
 
@@ -627,7 +680,8 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
627
680
  // ─── Command ─────────────────────────────────────────────────────────
628
681
 
629
682
  pi.registerCommand("benchmark", {
630
- description: "Run AI model benchmarks (6 categories: code-gen, debug, planning, tool-calling, speed, orchestration)",
683
+ description:
684
+ "Run AI model benchmarks (6 categories: code-gen, debug, planning, tool-calling, speed, orchestration)",
631
685
  handler: async (args, ctx) => {
632
686
  const arg = args.trim().toLowerCase();
633
687
 
@@ -658,7 +712,8 @@ export default function benchmarkExtension(pi: ExtensionAPI) {
658
712
 
659
713
  // Help
660
714
  if (arg === "help" || arg === "?") {
661
- ctx.ui.notify(`**Phi Code Benchmark** — 6 categories, real API calls
715
+ ctx.ui.notify(
716
+ `**Phi Code Benchmark** — 6 categories, real API calls
662
717
 
663
718
  Commands:
664
719
  /benchmark Run on current model
@@ -676,7 +731,9 @@ Categories tested (weighted):
676
731
  ⏱️ speed (×1) — Response latency
677
732
  🧩 orchestration (×2) — Multi-step analysis
678
733
 
679
- Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
734
+ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`,
735
+ "info",
736
+ );
680
737
  return;
681
738
  }
682
739
 
@@ -684,8 +741,13 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
684
741
  const available = await getAvailableModels();
685
742
  if (available.length === 0) {
686
743
  const providers = getProviderConfigs();
687
- const hint = providers.map(p => ` ${p.envVar}: ${process.env[p.envVar] ? "set but no models configured" : "not set"}`).join("\n");
688
- ctx.ui.notify(`❌ No benchmarkable models found.\n\nProvider status:\n${hint}\n\nSet at least one API key with known models.`, "warning");
744
+ const hint = providers
745
+ .map((p) => ` ${p.envVar}: ${process.env[p.envVar] ? "set but no models configured" : "not set"}`)
746
+ .join("\n");
747
+ ctx.ui.notify(
748
+ `❌ No benchmarkable models found.\n\nProvider status:\n${hint}\n\nSet at least one API key with known models.`,
749
+ "warning",
750
+ );
689
751
  return;
690
752
  }
691
753
 
@@ -700,7 +762,7 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
700
762
  const result = await benchmarkModel(model.id, model.provider, model.baseUrl, model.apiKey, ctx);
701
763
 
702
764
  // Replace existing result for this model
703
- store.results = store.results.filter(r => r.modelId !== model.id);
765
+ store.results = store.results.filter((r) => r.modelId !== model.id);
704
766
  store.results.push(result);
705
767
  await saveStore(store);
706
768
  }
@@ -713,21 +775,27 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
713
775
  if (arg) {
714
776
  // Benchmark specific model: prefer exact id match, then require an
715
777
  // unambiguous substring match to avoid benchmarking the wrong model.
716
- const exact = available.find(m => m.id.toLowerCase() === arg);
717
- const matches = available.filter(m => m.id.toLowerCase().includes(arg));
778
+ const exact = available.find((m) => m.id.toLowerCase() === arg);
779
+ const matches = available.filter((m) => m.id.toLowerCase().includes(arg));
718
780
  const model = exact ?? (matches.length === 1 ? matches[0] : undefined);
719
781
  if (!model) {
720
782
  if (!exact && matches.length > 1) {
721
- ctx.ui.notify(`Model "${arg}" is ambiguous. Did you mean:\n${matches.map(m => ` - ${m.id} (${m.provider})`).join("\n")}`, "warning");
783
+ ctx.ui.notify(
784
+ `Model "${arg}" is ambiguous. Did you mean:\n${matches.map((m) => ` - ${m.id} (${m.provider})`).join("\n")}`,
785
+ "warning",
786
+ );
722
787
  return;
723
788
  }
724
- ctx.ui.notify(`Model "${arg}" not found or no API key. Available:\n${available.map(m => ` - ${m.id} (${m.provider})`).join("\n")}`, "warning");
789
+ ctx.ui.notify(
790
+ `Model "${arg}" not found or no API key. Available:\n${available.map((m) => ` - ${m.id} (${m.provider})`).join("\n")}`,
791
+ "warning",
792
+ );
725
793
  return;
726
794
  }
727
795
 
728
796
  ctx.ui.notify(`🧪 Benchmarking **${model.id}** (6 categories)...\n`, "info");
729
797
  const result = await benchmarkModel(model.id, model.provider, model.baseUrl, model.apiKey, ctx);
730
- store.results = store.results.filter(r => r.modelId !== model.id);
798
+ store.results = store.results.filter((r) => r.modelId !== model.id);
731
799
  store.results.push(result);
732
800
  await saveStore(store);
733
801
 
@@ -739,11 +807,17 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
739
807
  // Try to find current model in available list
740
808
  const currentModel = ctx.model;
741
809
  if (currentModel) {
742
- const modelConfig = available.find(m => m.id === currentModel.id);
810
+ const modelConfig = available.find((m) => m.id === currentModel.id);
743
811
  if (modelConfig) {
744
812
  ctx.ui.notify(`🧪 Benchmarking current model **${currentModel.id}** (6 categories)...\n`, "info");
745
- const result = await benchmarkModel(modelConfig.id, modelConfig.provider, modelConfig.baseUrl, modelConfig.apiKey, ctx);
746
- store.results = store.results.filter(r => r.modelId !== modelConfig.id);
813
+ const result = await benchmarkModel(
814
+ modelConfig.id,
815
+ modelConfig.provider,
816
+ modelConfig.baseUrl,
817
+ modelConfig.apiKey,
818
+ ctx,
819
+ );
820
+ store.results = store.results.filter((r) => r.modelId !== modelConfig.id);
747
821
  store.results.push(result);
748
822
  await saveStore(store);
749
823
  ctx.ui.notify(`\n✅ **${currentModel.id}** — Total: ${result.totalScore}/100`, "info");
@@ -752,7 +826,10 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
752
826
  }
753
827
 
754
828
  // Fallback: show available models
755
- ctx.ui.notify(`Available models for benchmark:\n${available.map(m => ` - ${m.id} (${m.provider})`).join("\n")}\n\nUsage: /benchmark <model-id> or /benchmark all`, "info");
829
+ ctx.ui.notify(
830
+ `Available models for benchmark:\n${available.map((m) => ` - ${m.id} (${m.provider})`).join("\n")}\n\nUsage: /benchmark <model-id> or /benchmark all`,
831
+ "info",
832
+ );
756
833
  },
757
834
  });
758
835
 
@@ -761,7 +838,10 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
761
838
  try {
762
839
  const store = await loadStore();
763
840
  if (store.results.length > 0) {
764
- ctx.ui.notify(`🧪 ${store.results.length} benchmark results available. /benchmark results to view.`, "info");
841
+ ctx.ui.notify(
842
+ `🧪 ${store.results.length} benchmark results available. /benchmark results to view.`,
843
+ "info",
844
+ );
765
845
  }
766
846
  } catch {
767
847
  // ignore
@@ -22,8 +22,8 @@
22
22
  * user keeps the legacy `web_search` / `fetch_url` only).
23
23
  */
24
24
 
25
- import { createRequire } from "node:module";
26
25
  import { existsSync } from "node:fs";
26
+ import { createRequire } from "node:module";
27
27
  import { dirname, join } from "node:path";
28
28
  import { pathToFileURL } from "node:url";
29
29
  import { Type } from "@sinclair/typebox";
@@ -129,11 +129,7 @@ export default function browserExtension(pi: ExtensionAPI) {
129
129
  url: Type.String({ description: "Full URL (https://...)" }),
130
130
  tabId: Type.Optional(Type.String()),
131
131
  waitUntil: Type.Optional(
132
- Type.Union([
133
- Type.Literal("load"),
134
- Type.Literal("domcontentloaded"),
135
- Type.Literal("networkidle"),
136
- ]),
132
+ Type.Union([Type.Literal("load"), Type.Literal("domcontentloaded"), Type.Literal("networkidle")]),
137
133
  ),
138
134
  timeoutMs: Type.Optional(Type.Number()),
139
135
  }),
@@ -161,13 +157,7 @@ export default function browserExtension(pi: ExtensionAPI) {
161
157
  parameters: Type.Object({
162
158
  tabId: Type.Optional(Type.String()),
163
159
  url: Type.Optional(Type.String()),
164
- mode: Type.Optional(
165
- Type.Union([
166
- Type.Literal("readability"),
167
- Type.Literal("html"),
168
- Type.Literal("text"),
169
- ]),
170
- ),
160
+ mode: Type.Optional(Type.Union([Type.Literal("readability"), Type.Literal("html"), Type.Literal("text")])),
171
161
  }),
172
162
  execute: async (_toolCallId, params) => {
173
163
  const api = await getBrowserApi();
@@ -181,9 +171,9 @@ export default function browserExtension(pi: ExtensionAPI) {
181
171
  name: "browser_screenshot",
182
172
  description:
183
173
  "Capture a PNG screenshot of an open tab. Use this whenever the user asks " +
184
- "to *see* a page, when a visual proof is requested (e.g. \"show me what " +
185
- "this looks like\", \"is the layout broken\", \"did the bot detection page " +
186
- "trigger?\"), or to confirm a UI state after `browser_click` / " +
174
+ 'to *see* a page, when a visual proof is requested (e.g. "show me what ' +
175
+ 'this looks like", "is the layout broken", "did the bot detection page ' +
176
+ 'trigger?"), or to confirm a UI state after `browser_click` / ' +
187
177
  "`browser_type`. Requires a `tabId` from a prior `browser_navigate`. " +
188
178
  "Returns the image as base64 under `bytesBase64` with `mimeType: image/png`.",
189
179
  parameters: Type.Object({
@@ -208,16 +198,10 @@ export default function browserExtension(pi: ExtensionAPI) {
208
198
  "the rendered search engine UI (e.g. featured snippets, knowledge cards, " +
209
199
  "AI Overview boxes). Slower than `web_search` and requires the Camoufox " +
210
200
  "browser to boot. Defaults to DuckDuckGo (least restrictive); pass " +
211
- "`engine: \"google\"` only when you need Google-specific results.",
201
+ '`engine: "google"` only when you need Google-specific results.',
212
202
  parameters: Type.Object({
213
203
  query: Type.String(),
214
- engine: Type.Optional(
215
- Type.Union([
216
- Type.Literal("google"),
217
- Type.Literal("duckduckgo"),
218
- Type.Literal("bing"),
219
- ]),
220
- ),
204
+ engine: Type.Optional(Type.Union([Type.Literal("google"), Type.Literal("duckduckgo"), Type.Literal("bing")])),
221
205
  }),
222
206
  execute: async (_toolCallId, params) => {
223
207
  const api = await getBrowserApi();
@@ -243,13 +227,7 @@ export default function browserExtension(pi: ExtensionAPI) {
243
227
  tabId: Type.String(),
244
228
  ref: Type.Optional(Type.String()),
245
229
  selector: Type.Optional(Type.String()),
246
- button: Type.Optional(
247
- Type.Union([
248
- Type.Literal("left"),
249
- Type.Literal("right"),
250
- Type.Literal("middle"),
251
- ]),
252
- ),
230
+ button: Type.Optional(Type.Union([Type.Literal("left"), Type.Literal("right"), Type.Literal("middle")])),
253
231
  }),
254
232
  execute: async (_toolCallId, params) => {
255
233
  const api = await getBrowserApi();
@@ -296,12 +274,7 @@ export default function browserExtension(pi: ExtensionAPI) {
296
274
  "content.",
297
275
  parameters: Type.Object({
298
276
  tabId: Type.String(),
299
- direction: Type.Union([
300
- Type.Literal("up"),
301
- Type.Literal("down"),
302
- Type.Literal("left"),
303
- Type.Literal("right"),
304
- ]),
277
+ direction: Type.Union([Type.Literal("up"), Type.Literal("down"), Type.Literal("left"), Type.Literal("right")]),
305
278
  ref: Type.Optional(Type.String()),
306
279
  pixels: Type.Optional(Type.Number()),
307
280
  }),
@@ -9,13 +9,6 @@
9
9
 
10
10
  import { readFileSync } from "node:fs";
11
11
  import { fileURLToPath } from "node:url";
12
- import {
13
- type AssistantMessage,
14
- completeSimple,
15
- type Message,
16
- type StopReason,
17
- type UserMessage,
18
- } from "phi-code-ai";
19
12
  import {
20
13
  convertToLlm,
21
14
  type ExtensionAPI,
@@ -23,6 +16,7 @@ import {
23
16
  type ExtensionContext,
24
17
  type SessionEntry,
25
18
  } from "phi-code";
19
+ import { type AssistantMessage, completeSimple, type Message, type StopReason, type UserMessage } from "phi-code-ai";
26
20
  import { showBtwOverlay } from "./btw-ui.js";
27
21
 
28
22
  // ---------------------------------------------------------------------------