@phi-code-admin/phi-code 0.56.5 → 0.57.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.
@@ -1,331 +1,466 @@
1
1
  /**
2
2
  * Phi Init Extension - Interactive setup wizard for Phi Code
3
3
  *
4
- * Provides an interactive wizard to configure Phi Code with:
5
- * - API key detection and model discovery
6
- * - Intelligent model routing configuration
7
- * - Agent definitions setup
8
- * - Extension activation
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
9
8
  *
10
- * Features:
11
- * - /phi-init command for setup wizard
12
- * - Auto-detects available API keys
13
- * - Three setup modes: auto, benchmark, manual
14
- * - Creates ~/.phi/agent/ configuration structure
15
- * - Activates Phi Code extensions
16
- *
17
- * Usage:
18
- * 1. Run /phi-init to start the wizard
19
- * 2. Follow interactive prompts
20
- * 3. Configuration saved to ~/.phi/agent/
9
+ * Creates ~/.phi/agent/ structure with routing, agents, and memory.
21
10
  */
22
11
 
23
12
  import type { ExtensionAPI } from "phi-code";
24
- import { writeFile, mkdir, readFile, access, readdir, copyFile } from "node:fs/promises";
25
- import { join } from "node:path";
13
+ import { writeFile, mkdir, copyFile, readdir, access } from "node:fs/promises";
14
+ import { join, resolve } from "node:path";
26
15
  import { homedir } from "node:os";
16
+ import { existsSync } from "node:fs";
17
+
18
+ // ─── Types ───────────────────────────────────────────────────────────────
27
19
 
28
20
  interface DetectedProvider {
29
- name: string;
30
- envVar: string;
31
- models: string[];
32
- available: boolean;
21
+ name: string;
22
+ envVar: string;
23
+ baseUrl: string;
24
+ models: string[];
25
+ available: boolean;
33
26
  }
34
27
 
35
- interface ModelConfig {
36
- id: string;
37
- provider: string;
38
- apiKey?: string;
39
- baseUrl?: string;
28
+ interface RoutingConfig {
29
+ routes: Record<string, {
30
+ description: string;
31
+ keywords: string[];
32
+ preferredModel: string;
33
+ fallback: string;
34
+ agent: string;
35
+ }>;
36
+ default: { model: string; agent: string | null };
40
37
  }
41
38
 
42
- interface RoutingConfigData {
43
- routes: Record<string, {
44
- preferredModel: string;
45
- fallback: string;
46
- agent: string | null;
47
- keywords: string[];
48
- }>;
49
- default: { model: string; agent: string | null };
39
+ // ─── Provider Detection ──────────────────────────────────────────────────
40
+
41
+ function detectProviders(): DetectedProvider[] {
42
+ const providers: DetectedProvider[] = [
43
+ {
44
+ name: "Alibaba Coding Plan",
45
+ envVar: "ALIBABA_CODING_PLAN_KEY",
46
+ baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
47
+ 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"],
48
+ available: false,
49
+ },
50
+ {
51
+ name: "OpenAI",
52
+ envVar: "OPENAI_API_KEY",
53
+ baseUrl: "https://api.openai.com/v1",
54
+ models: ["gpt-4o", "gpt-4o-mini", "o1", "o3-mini"],
55
+ available: false,
56
+ },
57
+ {
58
+ name: "Anthropic",
59
+ envVar: "ANTHROPIC_API_KEY",
60
+ baseUrl: "https://api.anthropic.com/v1",
61
+ models: ["claude-sonnet-4-20250514", "claude-3-5-haiku-20241022"],
62
+ available: false,
63
+ },
64
+ {
65
+ name: "Google",
66
+ envVar: "GOOGLE_API_KEY",
67
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
68
+ models: ["gemini-2.5-pro", "gemini-2.5-flash"],
69
+ available: false,
70
+ },
71
+ {
72
+ name: "OpenRouter",
73
+ envVar: "OPENROUTER_API_KEY",
74
+ baseUrl: "https://openrouter.ai/api/v1",
75
+ models: [],
76
+ available: false,
77
+ },
78
+ {
79
+ name: "Groq",
80
+ envVar: "GROQ_API_KEY",
81
+ baseUrl: "https://api.groq.com/openai/v1",
82
+ models: ["llama-3.3-70b-versatile", "mixtral-8x7b-32768"],
83
+ available: false,
84
+ },
85
+ ];
86
+
87
+ for (const p of providers) {
88
+ p.available = !!process.env[p.envVar];
89
+ }
90
+
91
+ return providers;
50
92
  }
51
93
 
94
+ function getAllAvailableModels(providers: DetectedProvider[]): string[] {
95
+ return providers.filter(p => p.available).flatMap(p => p.models);
96
+ }
97
+
98
+ // ─── Routing Presets ─────────────────────────────────────────────────────
99
+
100
+ const TASK_ROLES = [
101
+ { key: "code", label: "Code Generation", desc: "Writing and modifying code", agent: "code", defaultModel: "qwen3-coder-plus" },
102
+ { key: "debug", label: "Debugging", desc: "Finding and fixing bugs", agent: "code", defaultModel: "qwen3-max-2026-01-23" },
103
+ { key: "plan", label: "Planning", desc: "Architecture and design", agent: "plan", defaultModel: "qwen3-max-2026-01-23" },
104
+ { key: "explore", label: "Exploration", desc: "Code reading and analysis", agent: "explore", defaultModel: "kimi-k2.5" },
105
+ { key: "test", label: "Testing", desc: "Running and writing tests", agent: "test", defaultModel: "kimi-k2.5" },
106
+ { key: "review", label: "Code Review", desc: "Quality and security review", agent: "review", defaultModel: "qwen3.5-plus" },
107
+ ] as const;
108
+
109
+ const KEYWORDS: Record<string, string[]> = {
110
+ code: ["implement", "create", "build", "refactor", "write", "add", "modify", "update", "generate"],
111
+ debug: ["fix", "bug", "error", "debug", "crash", "broken", "failing", "issue", "troubleshoot"],
112
+ explore: ["read", "analyze", "explain", "understand", "find", "search", "look", "show", "what", "how"],
113
+ plan: ["plan", "design", "architect", "spec", "structure", "organize", "strategy", "approach"],
114
+ test: ["test", "verify", "validate", "check", "assert", "coverage"],
115
+ review: ["review", "audit", "quality", "security", "improve", "optimize"],
116
+ };
117
+
118
+ function createRouting(assignments: Record<string, { preferred: string; fallback: string }>): RoutingConfig {
119
+ const routes: RoutingConfig["routes"] = {};
120
+ for (const role of TASK_ROLES) {
121
+ const assignment = assignments[role.key];
122
+ routes[role.key] = {
123
+ description: role.desc,
124
+ keywords: KEYWORDS[role.key] || [],
125
+ preferredModel: assignment?.preferred || role.defaultModel,
126
+ fallback: assignment?.fallback || "qwen3.5-plus",
127
+ agent: role.agent,
128
+ };
129
+ }
130
+ return {
131
+ routes,
132
+ default: { model: assignments["default"]?.preferred || "qwen3.5-plus", agent: null },
133
+ };
134
+ }
135
+
136
+ // ─── Extension ───────────────────────────────────────────────────────────
137
+
52
138
  export default function initExtension(pi: ExtensionAPI) {
53
- const phiAgentDir = join(homedir(), ".phi", "agent");
54
- const routingConfigPath = join(phiAgentDir, "routing.json");
55
- const modelsConfigPath = join(phiAgentDir, "models.json");
56
- const agentsDir = join(phiAgentDir, "agents");
57
- const extensionsDir = join(phiAgentDir, "extensions");
58
-
59
- /**
60
- * Détecte les clés API disponibles dans l'environnement
61
- */
62
- function detectProviders(): DetectedProvider[] {
63
- const providers: DetectedProvider[] = [
64
- {
65
- name: "Alibaba DashScope",
66
- envVar: "DASHSCOPE_API_KEY",
67
- models: [
68
- "qwen3.5-plus",
69
- "qwen3-max-2026-01-23",
70
- "qwen3-coder-plus",
71
- "qwen3-coder-next",
72
- "kimi-k2.5",
73
- "glm-5",
74
- "glm-4.7",
75
- "MiniMax-M2.5"
76
- ],
77
- available: false
78
- },
79
- {
80
- name: "OpenAI",
81
- envVar: "OPENAI_API_KEY",
82
- models: ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo", "gpt-4o"],
83
- available: false
84
- },
85
- {
86
- name: "Anthropic",
87
- envVar: "ANTHROPIC_API_KEY",
88
- models: ["claude-3.5-sonnet", "claude-3-opus", "claude-3-haiku"],
89
- available: false
90
- },
91
- {
92
- name: "Google",
93
- envVar: "GOOGLE_API_KEY",
94
- models: ["gemini-pro", "gemini-pro-vision"],
95
- available: false
96
- }
97
- ];
98
-
99
- // Vérifier la disponibilité des clés API
100
- for (const provider of providers) {
101
- provider.available = !!process.env[provider.envVar];
102
- }
103
-
104
- return providers;
105
- }
106
-
107
- /**
108
- * Crée la configuration par défaut (mode auto)
109
- */
110
- function createAutoConfig(availableProviders: DetectedProvider[]): { routing: RoutingConfigData; models: ModelConfig[] } {
111
- // Priorité: Alibaba (gratuit) > Anthropic > OpenAI > Google
112
- const preferenceOrder = ["Alibaba DashScope", "Anthropic", "OpenAI", "Google"];
113
-
114
- let primaryProvider: DetectedProvider | null = null;
115
-
116
- for (const providerName of preferenceOrder) {
117
- const provider = availableProviders.find(p => p.name === providerName && p.available);
118
- if (provider) {
119
- primaryProvider = provider;
120
- break;
121
- }
122
- }
123
-
124
- if (!primaryProvider) {
125
- throw new Error("Aucun provider disponible détecté");
126
- }
127
-
128
- const models: ModelConfig[] = primaryProvider.models.map(modelId => ({
129
- id: modelId,
130
- provider: primaryProvider!.name,
131
- apiKey: process.env[primaryProvider!.envVar]
132
- }));
133
-
134
- const routing: RoutingConfigData = {
135
- routes: {
136
- code: {
137
- preferredModel: primaryProvider.models.find(m => m.includes("coder")) || primaryProvider.models[0],
138
- fallback: primaryProvider.models[0],
139
- agent: null,
140
- keywords: ["code", "implement", "write", "create", "build", "develop", "function", "class"]
141
- },
142
- debug: {
143
- preferredModel: primaryProvider.models[0],
144
- fallback: primaryProvider.models[1] || primaryProvider.models[0],
145
- agent: null,
146
- keywords: ["debug", "fix", "error", "bug", "broken", "issue", "problem", "repair"]
147
- },
148
- plan: {
149
- preferredModel: primaryProvider.models.find(m => m.includes("max")) || primaryProvider.models[0],
150
- fallback: primaryProvider.models[0],
151
- agent: null,
152
- keywords: ["plan", "design", "architecture", "strategy", "structure", "organize"]
153
- },
154
- review: {
155
- preferredModel: primaryProvider.models[0],
156
- fallback: primaryProvider.models[1] || primaryProvider.models[0],
157
- agent: null,
158
- keywords: ["review", "audit", "check", "validate", "quality", "improve", "optimize"]
159
- },
160
- test: {
161
- preferredModel: primaryProvider.models.find(m => m.includes("fast")) || primaryProvider.models[0],
162
- fallback: primaryProvider.models[0],
163
- agent: null,
164
- keywords: ["test", "testing", "unit", "integration", "verify", "validate"]
165
- },
166
- explore: {
167
- preferredModel: primaryProvider.models.find(m => m.includes("fast")) || primaryProvider.models[0],
168
- fallback: primaryProvider.models[0],
169
- agent: null,
170
- keywords: ["explore", "understand", "analyze", "examine", "investigate"]
171
- },
172
- general: {
173
- preferredModel: primaryProvider.models[0],
174
- fallback: primaryProvider.models[1] || primaryProvider.models[0],
175
- agent: null,
176
- keywords: ["help", "explain", "what", "how", "why", "question"]
177
- }
178
- },
179
- default: {
180
- model: primaryProvider.models[0],
181
- agent: null
182
- }
183
- };
184
-
185
- return { routing, models };
186
- }
187
-
188
- /**
189
- * Crée les répertoires nécessaires
190
- */
191
- async function ensureDirectories(): Promise<void> {
192
- await mkdir(phiAgentDir, { recursive: true });
193
- await mkdir(agentsDir, { recursive: true });
194
- await mkdir(extensionsDir, { recursive: true });
195
- }
196
-
197
- /**
198
- * Copie les définitions d'agents par défaut
199
- */
200
- async function copyDefaultAgents(): Promise<void> {
201
- // Pour l'instant, nous créons un agent par défaut simple
202
- const defaultAgent = `---
203
- name: general-assistant
204
- description: General purpose assistant for various tasks
205
- model: qwen3.5-plus
206
- tools: [read, write, exec, web_search]
207
- maxTokens: 4096
139
+ const phiDir = join(homedir(), ".phi");
140
+ const agentDir = join(phiDir, "agent");
141
+ const agentsDir = join(agentDir, "agents");
142
+ const memoryDir = join(phiDir, "memory");
143
+
144
+ /**
145
+ * Create all necessary directories
146
+ */
147
+ async function ensureDirs() {
148
+ for (const dir of [agentDir, agentsDir, join(agentDir, "skills"), join(agentDir, "extensions"), memoryDir, join(memoryDir, "ontology")]) {
149
+ await mkdir(dir, { recursive: true });
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Copy bundled agent definitions to user directory
155
+ */
156
+ async function copyBundledAgents() {
157
+ const bundledDir = resolve(join(__dirname, "..", "..", "..", "agents"));
158
+ if (!existsSync(bundledDir)) return;
159
+
160
+ try {
161
+ const files = await readdir(bundledDir);
162
+ for (const file of files) {
163
+ if (file.endsWith(".md")) {
164
+ const dest = join(agentsDir, file);
165
+ if (!existsSync(dest)) {
166
+ await copyFile(join(bundledDir, file), dest);
167
+ }
168
+ }
169
+ }
170
+ } catch {
171
+ // bundled dir not available
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Create AGENTS.md template
177
+ */
178
+ async function createAgentsTemplate() {
179
+ const agentsMdPath = join(memoryDir, "AGENTS.md");
180
+ if (existsSync(agentsMdPath)) return; // Don't overwrite
181
+
182
+ await writeFile(agentsMdPath, `# AGENTS.md — Persistent Instructions
183
+
184
+ This file is loaded at the start of every session. Use it to store:
185
+ - Project conventions and rules
186
+ - Recurring instructions
187
+ - Important context the agent should always know
188
+
189
+ ## Project
190
+
191
+ - Name: (your project name)
192
+ - Language: TypeScript
193
+ - Framework: (your framework)
194
+
195
+ ## Conventions
196
+
197
+ - (your coding conventions)
198
+ - (your naming rules)
199
+ - (your commit format)
200
+
201
+ ## Important Notes
202
+
203
+ - (anything the agent should always remember)
204
+
208
205
  ---
209
206
 
210
- # General Assistant
211
-
212
- You are a helpful AI assistant capable of handling various tasks including:
213
-
214
- - Code analysis and debugging
215
- - File operations
216
- - Web searches
217
- - General problem solving
218
-
219
- Always be precise, helpful, and follow instructions carefully.
220
- `;
221
-
222
- await writeFile(join(agentsDir, "general-assistant.md"), defaultAgent, "utf8");
223
- }
224
-
225
- /**
226
- * Active les extensions Phi Code
227
- */
228
- async function activateExtensions(): Promise<void> {
229
- const extensionConfig = {
230
- enabled: true,
231
- extensions: [
232
- "phi/memory",
233
- "phi/benchmark",
234
- "phi/smart-router",
235
- "phi/skill-loader",
236
- "phi/web-search",
237
- "phi/orchestrator"
238
- ]
239
- };
240
-
241
- await writeFile(
242
- join(extensionsDir, "config.json"),
243
- JSON.stringify(extensionConfig, null, 2),
244
- "utf8"
245
- );
246
- }
247
-
248
- /**
249
- * Wizard interactif
250
- */
251
- pi.registerCommand("phi-init", {
252
- description: "Initialize Phi Code with interactive wizard",
253
- handler: async (args, ctx) => {
254
- try {
255
- ctx.ui.notify("🚀 Welcome to Phi Code Setup Wizard!", "info");
256
-
257
- // 1. Détection des API keys
258
- ctx.ui.notify("🔍 Detecting available API keys...", "info");
259
- const providers = detectProviders();
260
- const availableProviders = providers.filter(p => p.available);
261
-
262
- if (availableProviders.length === 0) {
263
- ctx.ui.notify("❌ No API keys detected. Please set one of the following environment variables:\n" +
264
- providers.map(p => `- ${p.envVar} (for ${p.name})`).join("\n"), "error");
265
- return;
266
- }
267
-
268
- ctx.ui.notify("✅ Found API keys for:", "info");
269
- for (const provider of availableProviders) {
270
- ctx.ui.notify(` - ${provider.name} (${provider.models.length} models)`, "info");
271
- }
272
-
273
- // 2. Choix du mode de configuration
274
- const mode = await ctx.ui.input("Choose setup mode:\n" +
275
- "1. auto - Use public rankings to assign models (fastest)\n" +
276
- "2. benchmark - Test models with simple exercises (recommended)\n" +
277
- "3. manual - Choose models yourself (most control)\n" +
278
- "\nEnter mode (1-3 or auto/benchmark/manual):");
279
-
280
- const selectedMode = mode.toLowerCase().startsWith("1") || mode.toLowerCase().startsWith("a") ? "auto" :
281
- mode.toLowerCase().startsWith("2") || mode.toLowerCase().startsWith("b") ? "benchmark" :
282
- mode.toLowerCase().startsWith("3") || mode.toLowerCase().startsWith("m") ? "manual" : "auto";
283
-
284
- ctx.ui.notify(`📋 Selected mode: ${selectedMode}`, "info");
285
-
286
- let config: { routing: RoutingConfigData; models: ModelConfig[] };
287
-
288
- if (selectedMode === "auto") {
289
- config = createAutoConfig(availableProviders);
290
- } else {
291
- // Pour benchmark et manual, utilisons auto pour l'instant (à implémenter)
292
- ctx.ui.notify("⚠️ Benchmark and manual modes not yet implemented, using auto mode.", "info");
293
- config = createAutoConfig(availableProviders);
294
- }
295
-
296
- // 3. Créer les répertoires
297
- ctx.ui.notify("📁 Creating configuration directories...", "info");
298
- await ensureDirectories();
299
-
300
- // 4. Écrire les fichiers de configuration
301
- ctx.ui.notify("💾 Writing configuration files...", "info");
302
- await writeFile(routingConfigPath, JSON.stringify(config.routing, null, 2), "utf8");
303
- await writeFile(modelsConfigPath, JSON.stringify({ models: config.models }, null, 2), "utf8");
304
-
305
- // 5. Copier les agents par défaut
306
- ctx.ui.notify("🤖 Setting up default agents...", "info");
307
- await copyDefaultAgents();
308
-
309
- // 6. Activer les extensions
310
- ctx.ui.notify("🔌 Activating extensions...", "info");
311
- await activateExtensions();
312
-
313
- // 7. Confirmation finale
314
- const confirm = await ctx.ui.confirm("Setup complete! Would you like to see the configuration summary?");
315
-
316
- if (confirm) {
317
- ctx.ui.notify("📊 Configuration Summary:\n" +
318
- `- Models configured: ${config.models.length}\n` +
319
- `- Primary provider: ${availableProviders[0].name}\n` +
320
- `- Config location: ${phiAgentDir}\n` +
321
- `- Extensions activated: Yes\n` +
322
- `- Default agents: Created\n\n` +
323
- "🎉 Phi Code is ready to use! Try running 'phi --help' for available commands.", "info");
324
- }
325
-
326
- } catch (error) {
327
- ctx.ui.notify(`❌ Setup failed: ${error}`, "error");
328
- }
329
- }
330
- });
331
- }
207
+ _Edit this file to customize Phi Code's behavior for your project._
208
+ `, "utf-8");
209
+ }
210
+
211
+ // ─── MODE: Auto ──────────────────────────────────────────────────
212
+
213
+ function autoMode(availableModels: string[]): Record<string, { preferred: string; fallback: string }> {
214
+ const assignments: Record<string, { preferred: string; fallback: string }> = {};
215
+
216
+ for (const role of TASK_ROLES) {
217
+ const preferred = availableModels.includes(role.defaultModel) ? role.defaultModel : availableModels[0];
218
+ const fallbackModel = availableModels.includes("qwen3.5-plus") ? "qwen3.5-plus" : availableModels[0];
219
+ assignments[role.key] = { preferred, fallback: fallbackModel };
220
+ }
221
+ assignments["default"] = {
222
+ preferred: availableModels.includes("qwen3.5-plus") ? "qwen3.5-plus" : availableModels[0],
223
+ fallback: availableModels[0],
224
+ };
225
+
226
+ return assignments;
227
+ }
228
+
229
+ // ─── MODE: Benchmark ─────────────────────────────────────────────
230
+
231
+ async function benchmarkMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
232
+ ctx.ui.notify("🧪 Benchmark mode: running tests on available models...", "info");
233
+ ctx.ui.notify("This will test each model with 6 coding tasks. It may take 10-15 minutes.", "info");
234
+ ctx.ui.notify("💡 Tip: You can run `/benchmark all` separately and use `/benchmark results` to see rankings.\n", "info");
235
+
236
+ // Check if benchmark results already exist
237
+ const benchmarkPath = join(phiDir, "benchmark", "results.json");
238
+ let existingResults: any = null;
239
+ try {
240
+ await access(benchmarkPath);
241
+ const content = await (await import("node:fs/promises")).readFile(benchmarkPath, "utf-8");
242
+ existingResults = JSON.parse(content);
243
+ } catch {
244
+ // No existing results
245
+ }
246
+
247
+ if (existingResults?.results?.length > 0) {
248
+ const useExisting = await ctx.ui.confirm(
249
+ `Found ${existingResults.results.length} existing benchmark results. Use them instead of re-running?`
250
+ );
251
+ if (useExisting) {
252
+ return assignFromBenchmark(existingResults.results, availableModels);
253
+ }
254
+ }
255
+
256
+ // Run benchmarks via the /benchmark command
257
+ ctx.ui.notify("Starting benchmarks... (this runs in the background, continue with /phi-init after /benchmark completes)\n", "info");
258
+ ctx.ui.notify("Run: `/benchmark all` then `/phi-init` again with mode=benchmark to use results.\n", "info");
259
+
260
+ // Fall back to auto for now
261
+ return autoMode(availableModels);
262
+ }
263
+
264
+ function assignFromBenchmark(results: any[], availableModels: string[]): Record<string, { preferred: string; fallback: string }> {
265
+ const assignments: Record<string, { preferred: string; fallback: string }> = {};
266
+
267
+ // Sort by total score
268
+ const sorted = [...results].sort((a: any, b: any) => (b.totalScore || 0) - (a.totalScore || 0));
269
+ const bestOverall = sorted[0]?.modelId || availableModels[0];
270
+ const secondBest = sorted[1]?.modelId || bestOverall;
271
+
272
+ // Find best per category
273
+ function bestForCategory(category: string): string {
274
+ let best = { id: bestOverall, score: 0 };
275
+ for (const r of results) {
276
+ const catScore = r.categories?.[category]?.score ?? 0;
277
+ if (catScore > best.score) {
278
+ best = { id: r.modelId, score: catScore };
279
+ }
280
+ }
281
+ return best.id;
282
+ }
283
+
284
+ assignments["code"] = { preferred: bestForCategory("code-gen"), fallback: secondBest };
285
+ assignments["debug"] = { preferred: bestForCategory("debug"), fallback: secondBest };
286
+ assignments["plan"] = { preferred: bestForCategory("planning"), fallback: secondBest };
287
+ assignments["explore"] = { preferred: bestForCategory("speed"), fallback: secondBest };
288
+ assignments["test"] = { preferred: bestForCategory("speed"), fallback: secondBest };
289
+ assignments["review"] = { preferred: bestForCategory("orchestration"), fallback: secondBest };
290
+ assignments["default"] = { preferred: bestOverall, fallback: secondBest };
291
+
292
+ return assignments;
293
+ }
294
+
295
+ // ─── MODE: Manual ────────────────────────────────────────────────
296
+
297
+ async function manualMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
298
+ ctx.ui.notify("🎛️ Manual mode: assign a model to each task category.\n", "info");
299
+
300
+ const modelList = availableModels.map((m, i) => ` ${i + 1}. ${m}`).join("\n");
301
+ const assignments: Record<string, { preferred: string; fallback: string }> = {};
302
+
303
+ for (const role of TASK_ROLES) {
304
+ const input = await ctx.ui.input(
305
+ `**${role.label}** — ${role.desc}\nDefault: ${role.defaultModel}\n\nAvailable models:\n${modelList}\n\nEnter model name or number (Enter for default):`
306
+ );
307
+
308
+ let chosen = role.defaultModel;
309
+ const trimmed = input.trim();
310
+
311
+ if (trimmed) {
312
+ // Try as number
313
+ const num = parseInt(trimmed);
314
+ if (num >= 1 && num <= availableModels.length) {
315
+ chosen = availableModels[num - 1];
316
+ } else {
317
+ // Try as model name (partial match)
318
+ const match = availableModels.find(m => m.toLowerCase().includes(trimmed.toLowerCase()));
319
+ if (match) chosen = match;
320
+ }
321
+ }
322
+
323
+ // Fallback selection
324
+ const fallbackDefault = availableModels.find(m => m !== chosen) || chosen;
325
+ const fallbackInput = await ctx.ui.input(
326
+ `Fallback model for ${role.label}? (Enter for ${fallbackDefault}):`
327
+ );
328
+
329
+ let fallback = fallbackDefault;
330
+ if (fallbackInput.trim()) {
331
+ const num = parseInt(fallbackInput.trim());
332
+ if (num >= 1 && num <= availableModels.length) {
333
+ fallback = availableModels[num - 1];
334
+ } else {
335
+ const match = availableModels.find(m => m.toLowerCase().includes(fallbackInput.trim().toLowerCase()));
336
+ if (match) fallback = match;
337
+ }
338
+ }
339
+
340
+ assignments[role.key] = { preferred: chosen, fallback };
341
+ ctx.ui.notify(` ✅ ${role.label}: ${chosen} (fallback: ${fallback})`, "info");
342
+ }
343
+
344
+ // Default model
345
+ const defaultInput = await ctx.ui.input(
346
+ `Default model for general tasks?\nAvailable:\n${modelList}\n\nEnter model name or number (Enter for ${availableModels[0]}):`
347
+ );
348
+ let defaultModel = availableModels[0];
349
+ if (defaultInput.trim()) {
350
+ const num = parseInt(defaultInput.trim());
351
+ if (num >= 1 && num <= availableModels.length) {
352
+ defaultModel = availableModels[num - 1];
353
+ } else {
354
+ const match = availableModels.find(m => m.toLowerCase().includes(defaultInput.trim().toLowerCase()));
355
+ if (match) defaultModel = match;
356
+ }
357
+ }
358
+ assignments["default"] = { preferred: defaultModel, fallback: availableModels[0] };
359
+
360
+ return assignments;
361
+ }
362
+
363
+ // ─── Command ─────────────────────────────────────────────────────
364
+
365
+ pi.registerCommand("phi-init", {
366
+ description: "Initialize Phi Code — interactive setup wizard (3 modes: auto, benchmark, manual)",
367
+ handler: async (args, ctx) => {
368
+ try {
369
+ ctx.ui.notify("╔══════════════════════════════════════╗", "info");
370
+ ctx.ui.notify("║ Φ Phi Code Setup Wizard ║", "info");
371
+ ctx.ui.notify("╚══════════════════════════════════════╝\n", "info");
372
+
373
+ // 1. Detect API keys
374
+ ctx.ui.notify("🔍 Detecting API keys...", "info");
375
+ const providers = detectProviders();
376
+ const available = providers.filter(p => p.available);
377
+
378
+ if (available.length === 0) {
379
+ ctx.ui.notify("❌ No API keys found. Set at least one:\n" +
380
+ providers.map(p => ` export ${p.envVar}="your-key" # ${p.name}`).join("\n") +
381
+ "\n\n💡 Free option: Get an Alibaba Coding Plan key at https://help.aliyun.com/zh/model-studio/", "error");
382
+ return;
383
+ }
384
+
385
+ ctx.ui.notify(`✅ Found ${available.length} provider(s):`, "info");
386
+ for (const p of available) {
387
+ ctx.ui.notify(` • ${p.name} — ${p.models.length} models`, "info");
388
+ }
389
+
390
+ const allModels = getAllAvailableModels(providers);
391
+ ctx.ui.notify(` Total: ${allModels.length} models available\n`, "info");
392
+
393
+ // 2. Choose mode
394
+ const modeInput = await ctx.ui.input(
395
+ "Choose setup mode:\n\n" +
396
+ " 1. auto — Use optimal defaults from public rankings (instant)\n" +
397
+ " 2. benchmark — Test models with coding tasks, assign by results (10-15 min)\n" +
398
+ " 3. manual — Choose each model assignment yourself\n\n" +
399
+ "Enter 1, 2, or 3:"
400
+ );
401
+
402
+ const mode = modeInput.trim().startsWith("2") || modeInput.trim().toLowerCase().startsWith("b") ? "benchmark"
403
+ : modeInput.trim().startsWith("3") || modeInput.trim().toLowerCase().startsWith("m") ? "manual"
404
+ : "auto";
405
+
406
+ ctx.ui.notify(`\n📋 Mode: **${mode}**\n`, "info");
407
+
408
+ // 3. Get assignments based on mode
409
+ let assignments: Record<string, { preferred: string; fallback: string }>;
410
+
411
+ if (mode === "auto") {
412
+ assignments = autoMode(allModels);
413
+ ctx.ui.notify("⚡ Auto-assigned models based on public rankings and model specializations.", "info");
414
+ } else if (mode === "benchmark") {
415
+ assignments = await benchmarkMode(allModels, ctx);
416
+ } else {
417
+ assignments = await manualMode(allModels, ctx);
418
+ }
419
+
420
+ // 4. Create directory structure
421
+ ctx.ui.notify("\n📁 Creating directories...", "info");
422
+ await ensureDirs();
423
+
424
+ // 5. Write routing config
425
+ ctx.ui.notify("🔀 Writing routing configuration...", "info");
426
+ const routing = createRouting(assignments);
427
+ await writeFile(join(agentDir, "routing.json"), JSON.stringify(routing, null, 2), "utf-8");
428
+
429
+ // 6. Copy bundled agents
430
+ ctx.ui.notify("🤖 Setting up sub-agents...", "info");
431
+ await copyBundledAgents();
432
+
433
+ // 7. Create AGENTS.md template
434
+ ctx.ui.notify("📝 Creating memory template...", "info");
435
+ await createAgentsTemplate();
436
+
437
+ // 8. Summary
438
+ ctx.ui.notify("\n╔══════════════════════════════════════╗", "info");
439
+ ctx.ui.notify("║ ✅ Setup Complete! ║", "info");
440
+ ctx.ui.notify("╚══════════════════════════════════════╝\n", "info");
441
+
442
+ ctx.ui.notify("**Configuration:**", "info");
443
+ ctx.ui.notify(` 📁 Config: ${agentDir}`, "info");
444
+ ctx.ui.notify(` 📁 Memory: ${memoryDir}`, "info");
445
+ ctx.ui.notify(` 🤖 Agents: ${agentsDir}`, "info");
446
+
447
+ ctx.ui.notify("\n**Model Assignments:**", "info");
448
+ for (const role of TASK_ROLES) {
449
+ const a = assignments[role.key];
450
+ ctx.ui.notify(` ${role.label}: \`${a.preferred}\` (fallback: \`${a.fallback}\`)`, "info");
451
+ }
452
+ ctx.ui.notify(` Default: \`${assignments["default"].preferred}\``, "info");
453
+
454
+ ctx.ui.notify("\n**Next steps:**", "info");
455
+ ctx.ui.notify(" • Edit `~/.phi/memory/AGENTS.md` with your project instructions", "info");
456
+ ctx.ui.notify(" • Run `/agents` to see available sub-agents", "info");
457
+ ctx.ui.notify(" • Run `/skills` to see available skills", "info");
458
+ ctx.ui.notify(" • Run `/benchmark all` to test model performance", "info");
459
+ ctx.ui.notify(" • Start coding! 🚀\n", "info");
460
+
461
+ } catch (error) {
462
+ ctx.ui.notify(`❌ Setup failed: ${error}`, "error");
463
+ }
464
+ },
465
+ });
466
+ }