@phi-code-admin/phi-code 0.62.1 → 0.62.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.
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Phi Init Extension - Interactive setup wizard for Phi Code
3
3
  *
4
- * Three modes:
5
- * - auto: Use Alibaba Coding Plan defaults (instant, recommended)
6
- * - benchmark: Test available models with /benchmark, then assign (10-15 min)
7
- * - manual: User assigns each model role interactively
4
+ * Detects providers (API keys + local endpoints), then lets the user
5
+ * manually assign models to each agent role (code, debug, plan, explore, test, review).
8
6
  *
9
7
  * Creates ~/.phi/agent/ structure with routing, agents, and memory.
10
8
  */
@@ -13,7 +11,7 @@ import type { ExtensionAPI } from "phi-code";
13
11
  import { writeFile, mkdir, copyFile, readdir, access, readFile } from "node:fs/promises";
14
12
  import { join, resolve } from "node:path";
15
13
  import { homedir } from "node:os";
16
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { existsSync } from "node:fs";
17
15
 
18
16
  // ─── Types ───────────────────────────────────────────────────────────────
19
17
 
@@ -367,270 +365,10 @@ _Edit this file to customize Phi Code's behavior for your project._
367
365
 
368
366
  // ─── MODE: Auto ──────────────────────────────────────────────────
369
367
 
370
- // ─── Model Intelligence Database ─────────────────────────────────
371
-
372
- interface ModelProfile {
373
- id: string;
374
- capabilities: {
375
- coding: number; // 0-100 score for code generation
376
- reasoning: number; // 0-100 score for debugging/planning
377
- speed: number; // 0-100 score for fast tasks
378
- general: number; // 0-100 overall score
379
- };
380
- hasReasoning: boolean;
381
- }
382
-
383
368
  /**
384
- * Fetch model profiles from OpenRouter's free API.
385
- * Classifies each model based on its description, name, and supported parameters.
386
- * Falls back to name-based heuristics if OpenRouter is unreachable.
369
+ * Manual mode is the only setup mode.
370
+ * User assigns each model to each agent role interactively.
387
371
  */
388
- async function fetchModelProfiles(modelIds: string[]): Promise<Map<string, ModelProfile>> {
389
- const profiles = new Map<string, ModelProfile>();
390
-
391
- try {
392
- const controller = new AbortController();
393
- const timeout = setTimeout(() => controller.abort(), 5000);
394
- const res = await fetch("https://openrouter.ai/api/v1/models", {
395
- signal: controller.signal,
396
- });
397
- clearTimeout(timeout);
398
-
399
- if (res.ok) {
400
- const data = await res.json() as any;
401
- const orModels: any[] = data.data || [];
402
-
403
- for (const modelId of modelIds) {
404
- // Try exact match first, then fuzzy match by base name
405
- const baseName = modelId.replace(/:.+$/, "").split("/").pop()?.toLowerCase() || modelId.toLowerCase();
406
- const match = orModels.find((m: any) => {
407
- const mId = m.id?.toLowerCase() || "";
408
- const mName = m.name?.toLowerCase() || "";
409
- return mId.includes(baseName) || mName.includes(baseName);
410
- });
411
-
412
- if (match) {
413
- const desc = (match.description || "").toLowerCase();
414
- const name = (match.name || "").toLowerCase();
415
- const hasReasoning = (match.supported_parameters || []).includes("reasoning")
416
- || (match.supported_parameters || []).includes("include_reasoning");
417
-
418
- // Score based on description keywords and model characteristics
419
- let coding = 50, reasoning = 50, speed = 50, general = 60;
420
-
421
- // Coding signals
422
- if (/cod(e|ing|ex)|program|implement|refactor|software engineer/.test(desc) || /coder|codex|codestral/.test(name)) {
423
- coding = 85;
424
- }
425
- // Reasoning signals
426
- if (hasReasoning || /reason|think|logic|step.by.step|complex/.test(desc) || /o1|o3|pro|opus/.test(name)) {
427
- reasoning = 85;
428
- }
429
- // Speed signals (smaller/cheaper models)
430
- const pricing = match.pricing || {};
431
- const promptCost = parseFloat(pricing.prompt || "0.01");
432
- if (promptCost < 0.001 || /fast|flash|mini|small|haiku|lite|instant/.test(name)) {
433
- speed = 85;
434
- }
435
- // General quality (larger context = usually better)
436
- const ctx = match.context_length || 0;
437
- if (ctx >= 200000) general = 80;
438
- if (ctx >= 1000000) general = 90;
439
- if (/frontier|flagship|most.advanced|best|state.of.the.art/.test(desc)) general = 90;
440
-
441
- profiles.set(modelId, { id: modelId, capabilities: { coding, reasoning, speed, general }, hasReasoning });
442
- }
443
- }
444
- }
445
- } catch {
446
- // OpenRouter unreachable — will fall back to heuristics
447
- }
448
-
449
- // Fill in any models not found in OpenRouter with name-based heuristics
450
- for (const modelId of modelIds) {
451
- if (!profiles.has(modelId)) {
452
- profiles.set(modelId, classifyByName(modelId));
453
- }
454
- }
455
-
456
- return profiles;
457
- }
458
-
459
- /**
460
- * Fallback: classify model by name patterns when OpenRouter data is unavailable.
461
- */
462
- function classifyByName(modelId: string): ModelProfile {
463
- const l = modelId.toLowerCase();
464
- let coding = 50, reasoning = 50, speed = 50, general = 55;
465
- let hasReasoning = false;
466
-
467
- if (/coder|code|codestral/.test(l)) coding = 80;
468
- if (/max|pro|plus|opus|large|o1|o3/.test(l)) { reasoning = 80; general = 75; }
469
- if (/mini|flash|fast|small|haiku|lite/.test(l)) { speed = 80; }
470
- if (/o1|o3|deepseek-r1|qwq/.test(l)) { hasReasoning = true; reasoning = 85; }
471
-
472
- return { id: modelId, capabilities: { coding, reasoning, speed, general }, hasReasoning };
473
- }
474
-
475
- /**
476
- * Auto-assign models using OpenRouter rankings + models.dev data.
477
- * Works with ANY provider — cloud, local, or mixed.
478
- *
479
- * Strategy:
480
- * 1. Fetch model profiles from OpenRouter (free, no API key needed)
481
- * 2. Score each model for coding, reasoning, speed, and general tasks
482
- * 3. Assign best model per role based on scores
483
- * 4. Fall back to name-based heuristics if OpenRouter is unreachable
484
- * 5. Single model? → everything uses that model (still works!)
485
- */
486
- async function autoMode(availableModels: string[], ctx?: any): Promise<Record<string, { preferred: string; fallback: string }>> {
487
- const assignments: Record<string, { preferred: string; fallback: string }> = {};
488
-
489
- if (availableModels.length === 0) {
490
- const fb = { preferred: "default", fallback: "default" };
491
- for (const role of TASK_ROLES) assignments[role.key] = fb;
492
- assignments["default"] = fb;
493
- return assignments;
494
- }
495
-
496
- if (availableModels.length === 1) {
497
- const single = { preferred: availableModels[0], fallback: availableModels[0] };
498
- for (const role of TASK_ROLES) assignments[role.key] = single;
499
- assignments["default"] = single;
500
- return assignments;
501
- }
502
-
503
- // Fetch intelligence from OpenRouter
504
- if (ctx) ctx.ui.notify("📊 Fetching model rankings from OpenRouter...", "info");
505
- const profiles = await fetchModelProfiles(availableModels);
506
-
507
- // Find best model for each capability
508
- function bestFor(capability: keyof ModelProfile["capabilities"]): string {
509
- let best = availableModels[0], bestScore = 0;
510
- for (const id of availableModels) {
511
- const p = profiles.get(id);
512
- if (p && p.capabilities[capability] > bestScore) {
513
- bestScore = p.capabilities[capability];
514
- best = id;
515
- }
516
- }
517
- return best;
518
- }
519
-
520
- function secondBestFor(capability: keyof ModelProfile["capabilities"], excludeId: string): string {
521
- let best = availableModels.find(m => m !== excludeId) || excludeId;
522
- let bestScore = 0;
523
- for (const id of availableModels) {
524
- if (id === excludeId) continue;
525
- const p = profiles.get(id);
526
- if (p && p.capabilities[capability] > bestScore) {
527
- bestScore = p.capabilities[capability];
528
- best = id;
529
- }
530
- }
531
- return best;
532
- }
533
-
534
- const bestCoder = bestFor("coding");
535
- const bestReasoner = bestFor("reasoning");
536
- const bestFast = bestFor("speed");
537
- const bestGeneral = bestFor("general");
538
-
539
- assignments["code"] = { preferred: bestCoder, fallback: secondBestFor("coding", bestCoder) };
540
- assignments["debug"] = { preferred: bestReasoner, fallback: secondBestFor("reasoning", bestReasoner) };
541
- assignments["plan"] = { preferred: bestReasoner, fallback: secondBestFor("reasoning", bestReasoner) };
542
- assignments["explore"] = { preferred: bestFast, fallback: secondBestFor("speed", bestFast) };
543
- assignments["test"] = { preferred: bestFast, fallback: secondBestFor("speed", bestFast) };
544
- assignments["review"] = { preferred: bestGeneral, fallback: secondBestFor("general", bestGeneral) };
545
- assignments["default"] = { preferred: bestGeneral, fallback: secondBestFor("general", bestGeneral) };
546
-
547
- // Show what was assigned and why
548
- if (ctx) {
549
- ctx.ui.notify("📊 Model rankings applied:", "info");
550
- for (const role of TASK_ROLES) {
551
- const a = assignments[role.key];
552
- const p = profiles.get(a.preferred);
553
- const scores = p ? `(coding:${p.capabilities.coding} reasoning:${p.capabilities.reasoning} speed:${p.capabilities.speed})` : "";
554
- ctx.ui.notify(` ${role.label}: ${a.preferred} ${scores}`, "info");
555
- }
556
- }
557
-
558
- return assignments;
559
- }
560
-
561
- // ─── MODE: Benchmark ─────────────────────────────────────────────
562
-
563
- async function benchmarkMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
564
- // Check if benchmark results already exist
565
- const benchmarkPath = join(phiDir, "benchmark", "results.json");
566
- let existingResults: any = null;
567
- try {
568
- await access(benchmarkPath);
569
- const content = await readFile(benchmarkPath, "utf-8");
570
- existingResults = JSON.parse(content);
571
- } catch {
572
- // No existing results
573
- }
574
-
575
- if (existingResults?.results?.length > 0) {
576
- const useExisting = await ctx.ui.confirm(
577
- "Use existing benchmarks?",
578
- `Found ${existingResults.results.length} benchmark results from a previous run. Use them?`
579
- );
580
- if (useExisting) {
581
- ctx.ui.notify("📊 Using existing benchmark results for model assignment.\n", "info");
582
- return assignFromBenchmark(existingResults.results, availableModels);
583
- }
584
- }
585
-
586
- // No existing results or user declined — run benchmarks now
587
- ctx.ui.notify("🧪 Benchmark mode: launching model tests...", "info");
588
- ctx.ui.notify("This tests each model with 6 coding tasks via real API calls.", "info");
589
- ctx.ui.notify("⏱️ Estimated time: 2-3 minutes per model.\n", "info");
590
-
591
- // Trigger benchmark via sendUserMessage — this runs /benchmark all
592
- // which saves results to the same results.json path
593
- pi.sendUserMessage("/benchmark all");
594
- ctx.ui.notify("⏳ Benchmarks started. Once complete, run `/phi-init` again and select benchmark mode to use the results.\n", "info");
595
- ctx.ui.notify("💡 The benchmark runs in the background. You'll see live results in the terminal.\n", "info");
596
-
597
- // Return auto mode assignments as temporary defaults
598
- // (will be overwritten when user re-runs /phi-init with benchmark results)
599
- ctx.ui.notify("📋 Setting auto-mode defaults while benchmarks run...\n", "info");
600
- return autoMode(availableModels, ctx);
601
- }
602
-
603
- function assignFromBenchmark(results: any[], availableModels: string[]): Record<string, { preferred: string; fallback: string }> {
604
- const assignments: Record<string, { preferred: string; fallback: string }> = {};
605
-
606
- // Sort by total score
607
- const sorted = [...results].sort((a: any, b: any) => (b.totalScore || 0) - (a.totalScore || 0));
608
- const bestOverall = sorted[0]?.modelId || availableModels[0];
609
- const secondBest = sorted[1]?.modelId || bestOverall;
610
-
611
- // Find best per category
612
- function bestForCategory(category: string): string {
613
- let best = { id: bestOverall, score: 0 };
614
- for (const r of results) {
615
- const catScore = r.categories?.[category]?.score ?? 0;
616
- if (catScore > best.score) {
617
- best = { id: r.modelId, score: catScore };
618
- }
619
- }
620
- return best.id;
621
- }
622
-
623
- assignments["code"] = { preferred: bestForCategory("code-gen"), fallback: secondBest };
624
- assignments["debug"] = { preferred: bestForCategory("debug"), fallback: secondBest };
625
- assignments["plan"] = { preferred: bestForCategory("planning"), fallback: secondBest };
626
- assignments["explore"] = { preferred: bestForCategory("speed"), fallback: secondBest };
627
- assignments["test"] = { preferred: bestForCategory("speed"), fallback: secondBest };
628
- assignments["review"] = { preferred: bestForCategory("orchestration"), fallback: secondBest };
629
- assignments["default"] = { preferred: bestOverall, fallback: secondBest };
630
-
631
- return assignments;
632
- }
633
-
634
372
  // ─── MODE: Manual ────────────────────────────────────────────────
635
373
 
636
374
  async function manualMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
@@ -670,7 +408,7 @@ _Edit this file to customize Phi Code's behavior for your project._
670
408
  // ─── Command ─────────────────────────────────────────────────────
671
409
 
672
410
  pi.registerCommand("phi-init", {
673
- description: "Initialize Phi Code — interactive setup wizard (3 modes: auto, benchmark, manual)",
411
+ description: "Initialize Phi Code — interactive setup wizard",
674
412
  handler: async (args, ctx) => {
675
413
  try {
676
414
  ctx.ui.notify("╔══════════════════════════════════════╗", "info");
@@ -811,29 +549,10 @@ _Edit this file to customize Phi Code's behavior for your project._
811
549
  const allModels = getAllAvailableModels(providers);
812
550
  ctx.ui.notify(`\n✅ **${allModels.length} models** available from ${available.length} provider(s).\n`, "info");
813
551
 
814
- // 2. Choose mode
815
- const modeOptions = [
816
- "auto — Use optimal defaults (instant)",
817
- "benchmark Test models first, assign by results (10-15 min)",
818
- "manual — Choose each model yourself",
819
- ];
820
- const modeChoice = await ctx.ui.select("Setup mode", modeOptions);
821
- const mode = (modeChoice ?? "").startsWith("benchmark") ? "benchmark"
822
- : (modeChoice ?? "").startsWith("manual") ? "manual"
823
- : "auto";
824
-
825
- ctx.ui.notify(`\n📋 Mode: **${mode}**\n`, "info");
826
-
827
- // 3. Get assignments based on mode
828
- let assignments: Record<string, { preferred: string; fallback: string }>;
829
-
830
- if (mode === "auto") {
831
- assignments = await autoMode(allModels, ctx);
832
- } else if (mode === "benchmark") {
833
- assignments = await benchmarkMode(allModels, ctx);
834
- } else {
835
- assignments = await manualMode(allModels, ctx);
836
- }
552
+ // 2. Assign models to agents (manual)
553
+ ctx.ui.notify(`\n📋 **Assign a model to each agent role:**\n`, "info");
554
+
555
+ const assignments = await manualMode(allModels, ctx);
837
556
 
838
557
  // 4. Create directory structure
839
558
  ctx.ui.notify("\n📁 Creating directories...", "info");
@@ -882,130 +601,4 @@ _Edit this file to customize Phi Code's behavior for your project._
882
601
  },
883
602
  });
884
603
 
885
- // ─── API Key Management Command ─────────────────────────────────
886
-
887
- pi.registerCommand("api-key", {
888
- description: "Set or view API keys (usage: /api-key set <provider> <key> | /api-key list)",
889
- handler: async (args, ctx) => {
890
- const parts = args.trim().split(/\s+/);
891
- const action = parts[0]?.toLowerCase();
892
-
893
- const PROVIDERS: Record<string, { envVar: string; name: string }> = {
894
- alibaba: { envVar: "ALIBABA_CODING_PLAN_KEY", name: "Alibaba Coding Plan" },
895
- dashscope: { envVar: "DASHSCOPE_API_KEY", name: "DashScope (Alibaba)" },
896
- openai: { envVar: "OPENAI_API_KEY", name: "OpenAI" },
897
- anthropic: { envVar: "ANTHROPIC_API_KEY", name: "Anthropic" },
898
- google: { envVar: "GOOGLE_API_KEY", name: "Google" },
899
- openrouter: { envVar: "OPENROUTER_API_KEY", name: "OpenRouter" },
900
- groq: { envVar: "GROQ_API_KEY", name: "Groq" },
901
- brave: { envVar: "BRAVE_API_KEY", name: "Brave Search" },
902
- };
903
-
904
- if (!action || action === "help") {
905
- ctx.ui.notify(`**🔑 API Key Management**
906
-
907
- Usage:
908
- /api-key set <provider> <key> — Set an API key for this session
909
- /api-key list — Show configured providers
910
- /api-key providers — List all supported providers
911
-
912
- Supported providers: ${Object.keys(PROVIDERS).join(", ")}
913
-
914
- Example:
915
- /api-key set alibaba sk-sp-xxx
916
- /api-key set openai sk-xxx
917
-
918
- Keys set with /api-key are active for the current session.
919
- For persistence, set environment variables:
920
- • Windows: setx ALIBABA_CODING_PLAN_KEY "your-key"
921
- • Linux/Mac: export ALIBABA_CODING_PLAN_KEY="your-key"`, "info");
922
- return;
923
- }
924
-
925
- if (action === "list") {
926
- let found = 0;
927
- let msg = "**🔑 Configured API Keys:**\n";
928
- for (const [id, info] of Object.entries(PROVIDERS)) {
929
- const key = process.env[info.envVar];
930
- if (key) {
931
- const masked = key.substring(0, 6) + "..." + key.substring(key.length - 4);
932
- msg += ` ✅ ${info.name} (${id}): ${masked}\n`;
933
- found++;
934
- }
935
- }
936
- if (found === 0) {
937
- msg += " ❌ No API keys configured\n";
938
- msg += "\nUse `/api-key set <provider> <key>` to add one.";
939
- }
940
- ctx.ui.notify(msg, "info");
941
- return;
942
- }
943
-
944
- if (action === "providers") {
945
- let msg = "**Supported Providers:**\n";
946
- for (const [id, info] of Object.entries(PROVIDERS)) {
947
- const status = process.env[info.envVar] ? "✅" : "⬜";
948
- msg += ` ${status} **${id}** — ${info.name} (${info.envVar})\n`;
949
- }
950
- ctx.ui.notify(msg, "info");
951
- return;
952
- }
953
-
954
- if (action === "set") {
955
- const provider = parts[1]?.toLowerCase();
956
- const key = parts.slice(2).join(" ").trim();
957
-
958
- if (!provider || !key) {
959
- ctx.ui.notify("Usage: /api-key set <provider> <key>\nExample: /api-key set alibaba sk-sp-xxx", "warning");
960
- return;
961
- }
962
-
963
- const info = PROVIDERS[provider];
964
- if (!info) {
965
- ctx.ui.notify(`Unknown provider "${provider}". Supported: ${Object.keys(PROVIDERS).join(", ")}`, "error");
966
- return;
967
- }
968
-
969
- // Set in current process environment
970
- process.env[info.envVar] = key;
971
- if (provider === "alibaba") {
972
- process.env.DASHSCOPE_API_KEY = key;
973
- }
974
-
975
- // Persist to models.json
976
- const modelsJsonPath = join(homedir(), ".phi", "agent", "models.json");
977
- let modelsConfig: any = { providers: {} };
978
- try {
979
- const existing = readFileSync(modelsJsonPath, "utf-8");
980
- modelsConfig = JSON.parse(existing);
981
- } catch { /* file doesn't exist yet */ }
982
-
983
- // Find provider config from detectProviders
984
- const providerDefs = detectProviders();
985
- const providerDef = providerDefs.find(p => p.envVar === info.envVar);
986
- if (providerDef) {
987
- const providerId = providerDef.name.toLowerCase().replace(/\s+/g, "-");
988
- modelsConfig.providers[providerId] = {
989
- baseUrl: providerDef.baseUrl,
990
- api: "openai-completions",
991
- apiKey: key,
992
- models: await Promise.all(providerDef.models.map(async (id: string) => {
993
- const spec = await getModelSpec(id);
994
- return {
995
- id, name: id, reasoning: spec.reasoning, input: ["text"],
996
- contextWindow: spec.contextWindow, maxTokens: spec.maxTokens,
997
- };
998
- })),
999
- };
1000
- writeFileSync(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
1001
- }
1002
-
1003
- const masked = key.substring(0, 6) + "..." + key.substring(key.length - 4);
1004
- ctx.ui.notify(`✅ **${info.name}** API key saved: ${masked}\n\n📁 Saved to \`~/.phi/agent/models.json\` (persistent)\n⚠️ **Restart phi** for the new models to load.`, "info");
1005
- return;
1006
- }
1007
-
1008
- ctx.ui.notify("Unknown action. Use: /api-key set|list|providers|help", "warning");
1009
- },
1010
- });
1011
604
  }
@@ -477,7 +477,7 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
477
477
  const specFile = `spec-${ts}.md`;
478
478
  await writeFile(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
479
479
 
480
- ctx.ui.notify(`📋 Executing project with agent workflow...`, "info");
480
+ ctx.ui.notify(`📋 Launching orchestrator...`, "info");
481
481
 
482
482
  // Build a single comprehensive prompt with all agent phases
483
483
  const prompt = `# Project: ${description}
@@ -505,7 +505,8 @@ Check code quality. Fix any issues. Ensure everything is polished.
505
505
  - Announce each phase as you start it (e.g. "## Phase 1 — Exploring...")
506
506
  - Do NOT stop after planning — implement everything`;
507
507
 
508
- ctx.ui.pasteToEditor(prompt);
508
+ // Send after handler returns (agent goes idle after command completes)
509
+ setTimeout(() => pi.sendUserMessage(prompt), 200);
509
510
  },
510
511
  });
511
512
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.62.1",
3
+ "version": "0.62.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {