@phi-code-admin/phi-code 0.96.0 → 0.97.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 (39) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +43 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/api-key-store.d.ts +20 -4
  7. package/dist/core/api-key-store.d.ts.map +1 -1
  8. package/dist/core/api-key-store.js +37 -7
  9. package/dist/core/api-key-store.js.map +1 -1
  10. package/dist/core/compaction/compaction.d.ts +16 -0
  11. package/dist/core/compaction/compaction.d.ts.map +1 -1
  12. package/dist/core/compaction/compaction.js +43 -19
  13. package/dist/core/compaction/compaction.js.map +1 -1
  14. package/dist/core/json-utils.d.ts +11 -0
  15. package/dist/core/json-utils.d.ts.map +1 -0
  16. package/dist/core/json-utils.js +15 -0
  17. package/dist/core/json-utils.js.map +1 -0
  18. package/dist/core/model-registry.d.ts +6 -1
  19. package/dist/core/model-registry.d.ts.map +1 -1
  20. package/dist/core/model-registry.js +10 -9
  21. package/dist/core/model-registry.js.map +1 -1
  22. package/dist/core/slash-commands.d.ts +13 -0
  23. package/dist/core/slash-commands.d.ts.map +1 -1
  24. package/dist/core/slash-commands.js +38 -0
  25. package/dist/core/slash-commands.js.map +1 -1
  26. package/dist/migrations.d.ts.map +1 -1
  27. package/dist/migrations.js +2 -2
  28. package/dist/migrations.js.map +1 -1
  29. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  30. package/dist/modes/interactive/interactive-mode.js +24 -4
  31. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  32. package/extensions/phi/benchmark.ts +43 -83
  33. package/extensions/phi/init.ts +86 -673
  34. package/extensions/phi/memory.ts +1 -1
  35. package/extensions/phi/models.ts +4 -3
  36. package/extensions/phi/providers/catalog.ts +133 -0
  37. package/extensions/phi/setup.ts +180 -280
  38. package/extensions/phi/smart-router.ts +0 -17
  39. package/package.json +5 -5
@@ -1,323 +1,72 @@
1
1
  /**
2
- * Phi Init Extension - Interactive setup wizard for Phi Code (legacy `/phi-init`).
2
+ * Phi Init Extension - legacy `/phi-init` entry point.
3
3
  *
4
- * Detects providers (API keys + local endpoints), then lets the user manually
5
- * assign models to each agent role (code, debug, plan, explore, test, review).
4
+ * Since 0.97.0 the provider/model wizard lives in ONE place: /setup
5
+ * (setup.ts, backed by the shared providers/catalog.ts). The wizard that used
6
+ * to live here was a drifting near-copy of it, with its own models.json
7
+ * reader that silently wiped a commented config. /phi-init now only:
8
+ * 1. scaffolds the ~/.phi structure (dirs, bundled agents, AGENTS.md template)
9
+ * 2. delegates to the exact same wizard as /setup
6
10
  *
7
- * Creates ~/.phi/agent/ structure with routing, agents, and memory.
8
- *
9
- * Hardening (2026-05-15):
10
- * - Uses the unified live-models registry (every provider is now refreshed
11
- * against its `/v1/models` endpoint, with static fallback when offline).
12
- * - The entire handler runs inside a defensive try/catch that surfaces errors
13
- * as `ctx.ui.notify("error")` instead of letting the host TUI crash. A
14
- * process-level `unhandledRejection` guard is installed once on first run
15
- * so a stray promise rejection during model probing cannot terminate phi-code.
16
- * - The API-key input flow now persists the key BEFORE any model enrichment
17
- * so a network failure on /v1/models cannot lose the user's input.
11
+ * /plan-models (lightweight per-role reassignment without provider probing)
12
+ * stays here, but reuses the shared assignment/routing helpers from setup.ts
13
+ * instead of maintaining a second copy of the picker and routing writer.
18
14
  */
19
15
 
20
16
  import { existsSync } from "node:fs";
21
- import { chmod, copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
17
+ import { copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
22
18
  import { homedir } from "node:os";
23
19
  import { join, resolve } from "node:path";
24
20
  import type { ExtensionAPI } from "phi-code";
25
- import { fetchLiveModels, pingProvider, toPersistedModel } from "./providers/live-models.js";
26
- import { isOpenCodeGoAnthropicModel, OPENCODE_GO_ANTHROPIC_BASE_URL } from "./providers/opencode-go.js";
27
-
28
- // ─── Types ───────────────────────────────────────────────────────────────
29
-
30
- interface DetectedProvider {
31
- id: string;
32
- name: string;
33
- envVar: string;
34
- baseUrl: string;
35
- api: string;
36
- requiresApiKey: boolean;
37
- available: boolean;
38
- models: string[];
39
- /** True for Ollama/LM Studio (models discovered at runtime, no key required). */
40
- local?: boolean;
41
- }
42
-
43
- interface RoutingConfig {
44
- routes: Record<
45
- string,
46
- {
47
- description: string;
48
- keywords: string[];
49
- preferredModel: string;
50
- fallback: string;
51
- agent: string;
52
- }
53
- >;
54
- default: { model: string; agent: string | null };
55
- }
56
-
57
- // ─── One-time global unhandledRejection guard ────────────────────────────
58
-
59
- let unhandledRejectionGuardInstalled = false;
60
- function installUnhandledRejectionGuard(): void {
61
- if (unhandledRejectionGuardInstalled) return;
62
- unhandledRejectionGuardInstalled = true;
63
- process.on("unhandledRejection", (reason) => {
64
- // Swallow async failures from extension wizards so they cannot terminate the TUI.
65
- // The wizard itself reports the failure via ctx.ui.notify in its own try/catch.
66
- const message = reason instanceof Error ? reason.message : String(reason);
67
- try {
68
- // Best effort logging — process.stderr survives TUI shutdown unlike console.
69
- process.stderr.write(`[phi-init] swallowed unhandledRejection: ${message}\n`);
70
- } catch {
71
- // no-op
72
- }
73
- });
74
- }
75
-
76
- // ─── Provider Detection ──────────────────────────────────────────────────
77
-
78
- function detectProviders(): DetectedProvider[] {
79
- const providers: DetectedProvider[] = [
80
- {
81
- id: "alibaba-codingplan",
82
- name: "Alibaba Coding Plan",
83
- envVar: "ALIBABA_CODING_PLAN_KEY",
84
- baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
85
- api: "openai-completions",
86
- requiresApiKey: true,
87
- available: false,
88
- models: [],
89
- },
90
- {
91
- id: "opencode-go",
92
- name: "OpenCode Go",
93
- envVar: "OPENCODE_GO_API_KEY",
94
- baseUrl: "https://opencode.ai/zen/go/v1",
95
- api: "openai-completions",
96
- requiresApiKey: true,
97
- available: false,
98
- models: [],
99
- },
100
- {
101
- id: "openai",
102
- name: "OpenAI",
103
- envVar: "OPENAI_API_KEY",
104
- baseUrl: "https://api.openai.com/v1",
105
- api: "openai-completions",
106
- requiresApiKey: true,
107
- available: false,
108
- models: [],
109
- },
110
- {
111
- id: "anthropic",
112
- name: "Anthropic",
113
- envVar: "ANTHROPIC_API_KEY",
114
- baseUrl: "https://api.anthropic.com/v1",
115
- api: "anthropic-messages",
116
- requiresApiKey: true,
117
- available: false,
118
- models: [],
119
- },
120
- {
121
- id: "google",
122
- name: "Google Gemini",
123
- envVar: "GOOGLE_API_KEY",
124
- baseUrl: "https://generativelanguage.googleapis.com/v1beta",
125
- api: "google",
126
- requiresApiKey: true,
127
- available: false,
128
- models: [],
129
- },
130
- {
131
- id: "openrouter",
132
- name: "OpenRouter",
133
- envVar: "OPENROUTER_API_KEY",
134
- baseUrl: "https://openrouter.ai/api/v1",
135
- api: "openai-completions",
136
- requiresApiKey: true,
137
- available: false,
138
- models: [],
139
- },
140
- {
141
- id: "groq",
142
- name: "Groq",
143
- envVar: "GROQ_API_KEY",
144
- baseUrl: "https://api.groq.com/openai/v1",
145
- api: "openai-completions",
146
- requiresApiKey: true,
147
- available: false,
148
- models: [],
149
- },
150
- {
151
- id: "ollama",
152
- name: "Ollama",
153
- envVar: "OLLAMA",
154
- baseUrl: "http://localhost:11434/v1",
155
- api: "openai-completions",
156
- requiresApiKey: false,
157
- available: false,
158
- models: [],
159
- local: true,
160
- },
161
- {
162
- id: "lm-studio",
163
- name: "LM Studio",
164
- envVar: "LM_STUDIO",
165
- baseUrl: "http://localhost:1234/v1",
166
- api: "openai-completions",
167
- requiresApiKey: false,
168
- available: false,
169
- models: [],
170
- local: true,
171
- },
172
- ];
173
-
174
- for (const p of providers) {
175
- if (!p.local && process.env[p.envVar]) {
176
- p.available = true;
177
- }
178
- }
179
- return providers;
180
- }
181
-
182
- /**
183
- * Probe local providers (Ollama, LM Studio) and live-fetch their model list.
184
- * Failures are silent — local servers are optional.
185
- */
186
- async function detectLocalProviders(providers: DetectedProvider[]): Promise<void> {
187
- await Promise.all(
188
- providers
189
- .filter((p) => p.local)
190
- .map(async (p) => {
191
- const result = await fetchLiveModels(p.id, { forceRefresh: true, timeoutMs: 2_500 });
192
- if (result.models.length > 0 && result.source !== "fallback") {
193
- p.models = result.models.map((m) => m.id);
194
- p.available = true;
195
- }
196
- }),
197
- );
198
- }
199
-
200
- function getAllAvailableModels(providers: DetectedProvider[]): Array<{ ref: string; display: string }> {
201
- const out: Array<{ ref: string; display: string }> = [];
202
- const seen = new Set<string>();
203
- for (const p of providers) {
204
- if (!p.available) continue;
205
- for (const id of p.models) {
206
- // Provider-qualified reference ("provider/id") so the same model id
207
- // offered by several providers stays distinct, and the provider is
208
- // visible at selection. No cross-provider dedup (only exact dupes).
209
- const ref = `${p.id}/${id}`;
210
- if (seen.has(ref)) continue;
211
- seen.add(ref);
212
- out.push({ ref, display: `${id} [${p.id}]` });
213
- }
21
+ import {
22
+ buildRoutingConfig,
23
+ configureAssignments,
24
+ ORCHESTRATION_ROLES,
25
+ runSetupWizard,
26
+ writeRoutingConfig,
27
+ } from "./setup.js";
28
+
29
+ const phiDir = join(homedir(), ".phi");
30
+ const agentDir = join(phiDir, "agent");
31
+ const agentsDir = join(agentDir, "agents");
32
+ const memoryDir = join(phiDir, "memory");
33
+
34
+ async function ensureDirs(): Promise<void> {
35
+ for (const dir of [
36
+ agentDir,
37
+ agentsDir,
38
+ join(agentDir, "skills"),
39
+ join(agentDir, "extensions"),
40
+ memoryDir,
41
+ join(memoryDir, "ontology"),
42
+ ]) {
43
+ await mkdir(dir, { recursive: true });
214
44
  }
215
- return out;
216
45
  }
217
46
 
218
- // ─── Routing Presets ─────────────────────────────────────────────────────
219
-
220
- const TASK_ROLES = [
221
- {
222
- key: "code",
223
- label: "Code Generation",
224
- desc: "Writing and modifying code",
225
- agent: "code",
226
- defaultModel: "default",
227
- },
228
- { key: "debug", label: "Debugging", desc: "Finding and fixing bugs", agent: "code", defaultModel: "default" },
229
- { key: "plan", label: "Planning", desc: "Architecture and design", agent: "plan", defaultModel: "default" },
230
- {
231
- key: "explore",
232
- label: "Exploration",
233
- desc: "Code reading and analysis",
234
- agent: "explore",
235
- defaultModel: "default",
236
- },
237
- { key: "test", label: "Testing", desc: "Running and writing tests", agent: "test", defaultModel: "default" },
238
- {
239
- key: "review",
240
- label: "Code Review",
241
- desc: "Quality and security review",
242
- agent: "review",
243
- defaultModel: "default",
244
- },
245
- ] as const;
246
-
247
- const KEYWORDS: Record<string, string[]> = {
248
- code: ["implement", "create", "build", "refactor", "write", "add", "modify", "update", "generate"],
249
- debug: ["fix", "bug", "error", "debug", "crash", "broken", "failing", "issue", "troubleshoot"],
250
- explore: ["read", "analyze", "explain", "understand", "find", "search", "look", "show", "what", "how"],
251
- plan: ["plan", "design", "architect", "spec", "structure", "organize", "strategy", "approach"],
252
- test: ["test", "verify", "validate", "check", "assert", "coverage"],
253
- review: ["review", "audit", "quality", "security", "improve", "optimize"],
254
- };
255
-
256
- function createRouting(assignments: Record<string, { preferred: string; fallback: string }>): RoutingConfig {
257
- const routes: RoutingConfig["routes"] = {};
258
- for (const role of TASK_ROLES) {
259
- const assignment = assignments[role.key];
260
- routes[role.key] = {
261
- description: role.desc,
262
- keywords: KEYWORDS[role.key] || [],
263
- preferredModel: assignment?.preferred || role.defaultModel,
264
- fallback: assignment?.fallback || role.defaultModel,
265
- agent: role.agent,
266
- };
267
- }
268
- return {
269
- routes,
270
- default: { model: assignments.default?.preferred || "default", agent: null },
271
- };
272
- }
273
-
274
- // ─── Extension ───────────────────────────────────────────────────────────
275
-
276
- export default function initExtension(pi: ExtensionAPI) {
277
- installUnhandledRejectionGuard();
278
-
279
- const phiDir = join(homedir(), ".phi");
280
- const agentDir = join(phiDir, "agent");
281
- const agentsDir = join(agentDir, "agents");
282
- const memoryDir = join(phiDir, "memory");
283
- const modelsJsonPath = join(agentDir, "models.json");
284
-
285
- async function ensureDirs(): Promise<void> {
286
- for (const dir of [
287
- agentDir,
288
- agentsDir,
289
- join(agentDir, "skills"),
290
- join(agentDir, "extensions"),
291
- memoryDir,
292
- join(memoryDir, "ontology"),
293
- ]) {
294
- await mkdir(dir, { recursive: true });
295
- }
296
- }
297
-
298
- async function copyBundledAgents(): Promise<void> {
299
- const bundledDir = resolve(join(__dirname, "..", "..", "..", "agents"));
300
- if (!existsSync(bundledDir)) return;
301
- try {
302
- const files = await readdir(bundledDir);
303
- for (const file of files) {
304
- if (!file.endsWith(".md")) continue;
305
- const dest = join(agentsDir, file);
306
- if (!existsSync(dest)) {
307
- await copyFile(join(bundledDir, file), dest);
308
- }
47
+ async function copyBundledAgents(): Promise<void> {
48
+ const bundledDir = resolve(join(__dirname, "..", "..", "..", "agents"));
49
+ if (!existsSync(bundledDir)) return;
50
+ try {
51
+ const files = await readdir(bundledDir);
52
+ for (const file of files) {
53
+ if (!file.endsWith(".md")) continue;
54
+ const dest = join(agentsDir, file);
55
+ if (!existsSync(dest)) {
56
+ await copyFile(join(bundledDir, file), dest);
309
57
  }
310
- } catch {
311
- // bundled dir not available
312
58
  }
59
+ } catch {
60
+ // bundled dir not available
313
61
  }
62
+ }
314
63
 
315
- async function createAgentsTemplate(): Promise<void> {
316
- const agentsMdPath = join(memoryDir, "AGENTS.md");
317
- if (existsSync(agentsMdPath)) return;
318
- await writeFile(
319
- agentsMdPath,
320
- `# AGENTS.md — Persistent Instructions
64
+ async function createAgentsTemplate(): Promise<void> {
65
+ const agentsMdPath = join(memoryDir, "AGENTS.md");
66
+ if (existsSync(agentsMdPath)) return;
67
+ await writeFile(
68
+ agentsMdPath,
69
+ `# AGENTS.md — Persistent Instructions
321
70
 
322
71
  This file is loaded at the start of every session. Use it to store:
323
72
  - Project conventions and rules
@@ -344,366 +93,27 @@ This file is loaded at the start of every session. Use it to store:
344
93
 
345
94
  _Edit this file to customize Phi Code's behavior for your project._
346
95
  `,
347
- "utf-8",
348
- );
349
- }
350
-
351
- // ─── Persistence helper (single source of truth for models.json writes) ──
352
-
353
- async function readModelsConfig(): Promise<{ providers: Record<string, any> }> {
354
- try {
355
- const raw = await readFile(modelsJsonPath, "utf-8");
356
- const parsed = JSON.parse(raw) as { providers?: Record<string, any> };
357
- return { providers: parsed.providers ?? {} };
358
- } catch {
359
- return { providers: {} };
360
- }
361
- }
362
-
363
- async function writeModelsConfig(config: { providers: Record<string, any> }): Promise<void> {
364
- await mkdir(agentDir, { recursive: true });
365
- await writeFile(modelsJsonPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
366
- // models.json contient des cles API en clair : restreindre l'acces au
367
- // proprietaire (no-op sur Windows, ou les permissions POSIX n'existent pas).
368
- if (process.platform !== "win32") {
369
- try {
370
- await chmod(modelsJsonPath, 0o600);
371
- } catch {
372
- /* best-effort : certains systemes de fichiers ne supportent pas chmod */
373
- }
374
- }
375
- }
376
-
377
- async function persistProviderKey(provider: DetectedProvider, apiKey: string): Promise<void> {
378
- const config = await readModelsConfig();
379
- const existing = config.providers[provider.id] ?? {};
380
- config.providers[provider.id] = {
381
- ...existing,
382
- baseUrl: provider.baseUrl,
383
- api: provider.api,
384
- apiKey,
385
- };
386
- await writeModelsConfig(config);
387
- }
388
-
389
- async function persistProviderModels(
390
- provider: DetectedProvider,
391
- models: ReturnType<typeof toPersistedModel>[],
392
- ): Promise<void> {
393
- const config = await readModelsConfig();
394
- const existing = config.providers[provider.id] ?? {};
395
- config.providers[provider.id] = {
396
- ...existing,
397
- baseUrl: provider.baseUrl,
398
- api: provider.api,
399
- models,
400
- };
401
- await writeModelsConfig(config);
402
- }
403
-
404
- // OpenCode Go's Qwen/MiniMax models must be persisted under a separate
405
- // Anthropic-compatible provider (the OpenAI shim 401s them).
406
- async function persistOpenCodeGoAnthropic(
407
- apiKey: string,
408
- models: ReturnType<typeof toPersistedModel>[],
409
- ): Promise<void> {
410
- const config = await readModelsConfig();
411
- const existing = config.providers["opencode-go-anthropic"] ?? {};
412
- config.providers["opencode-go-anthropic"] = {
413
- ...existing,
414
- baseUrl: OPENCODE_GO_ANTHROPIC_BASE_URL,
415
- api: "anthropic-messages",
416
- apiKey,
417
- models,
418
- };
419
- await writeModelsConfig(config);
420
- }
421
-
422
- // ─── Manual model assignment (one model per orchestration role) ─────
423
- //
424
- // As of 0.75.6, `/phi-init` ONLY configures orchestration role models
425
- // (used by `/plan` and the orchestrator). The chat default model is
426
- // owned exclusively by `/model` and persisted via the settings manager.
427
- // We intentionally do NOT ask "Default model" here — that would override
428
- // the user's `/model` choice on every routing decision.
429
-
430
- async function manualMode(
431
- availableModels: Array<{ ref: string; display: string }>,
432
- ctx: any,
433
- ): Promise<Record<string, { preferred: string; fallback: string }>> {
434
- ctx.ui.notify(
435
- "Assign a model to each orchestration role.\n" +
436
- "These models are used by `/plan` and the orchestrator — NOT by normal chat.\n" +
437
- "The chat default model is controlled via `/model` (and stays sticky across prompts).\n" +
438
- "Each option shows its provider as `model-id [provider]`, so the same model from\n" +
439
- "different providers stays distinct.\n",
440
- "info",
441
- );
442
- // Display strings carry the provider badge; map them back to the canonical
443
- // "provider/id" reference that gets persisted into routing.json.
444
- const DEFAULT_OPTION = "default (use current chat model)";
445
- const refByDisplay = new Map<string, string>([[DEFAULT_OPTION, "default"]]);
446
- for (const m of availableModels) refByDisplay.set(m.display, m.ref);
447
- const modelOptions = [DEFAULT_OPTION, ...availableModels.map((m) => m.display)];
448
- const assignments: Record<string, { preferred: string; fallback: string }> = {};
449
-
450
- for (const role of TASK_ROLES) {
451
- const chosen = await ctx.ui.select(`${role.label} — ${role.desc}`, modelOptions);
452
- const preferredModel = refByDisplay.get(chosen) ?? "default";
453
-
454
- const fallbackOptions = modelOptions.filter((m) => m !== chosen);
455
- const fallbackChoice = await ctx.ui.select(`Fallback for ${role.label}`, fallbackOptions);
456
- const fallback = refByDisplay.get(fallbackChoice) ?? "default";
457
-
458
- assignments[role.key] = { preferred: preferredModel, fallback };
459
- ctx.ui.notify(` ${role.label}: ${preferredModel} (fallback: ${fallback})`, "info");
460
- }
461
-
462
- // Orchestrator fallback (used only when a specific route has no model).
463
- // This is NOT the chat default — `/model` controls that.
464
- assignments.default = {
465
- preferred: "default",
466
- fallback: availableModels[0]?.ref || "default",
467
- };
468
- return assignments;
469
- }
470
-
471
- // ─── Per-provider configuration step ─────────────────────────────────
472
-
473
- async function configureProvider(provider: DetectedProvider, ctx: any): Promise<void> {
474
- if (provider.local) {
475
- const port = provider.id === "ollama" ? 11434 : 1234;
476
- const result = await fetchLiveModels(provider.id, { forceRefresh: true, timeoutMs: 2_500 });
477
- if (result.source === "live" && result.models.length > 0) {
478
- provider.models = result.models.map((m) => m.id);
479
- provider.available = true;
480
- ctx.ui.notify(`${provider.name} is running with ${provider.models.length} model(s).\n`, "info");
481
- } else {
482
- ctx.ui.notify(
483
- `${provider.name} not reachable on port ${port}. Start it and re-run \`/phi-init\`.\n`,
484
- "warning",
485
- );
486
- }
487
- return;
488
- }
489
-
490
- // Cloud provider — API key required.
491
- ctx.ui.notify(
492
- `\n${provider.name}\nNote: the key you type will be visible on screen. Stored in ${modelsJsonPath} (chmod 0600 on Unix).`,
493
- "info",
494
- );
495
-
496
- const apiKey = await ctx.ui.input(`Enter your ${provider.name} API key`, "Paste your key here");
497
-
498
- if (apiKey === undefined) {
499
- ctx.ui.notify("Cancelled. No key saved.", "warning");
500
- return;
501
- }
502
- const trimmed = apiKey.trim();
503
- if (trimmed.length < 5) {
504
- ctx.ui.notify("Invalid API key (too short). Skipped.\n", "error");
505
- return;
506
- }
507
-
508
- // Persist the key FIRST. Any subsequent failure during live-fetch must
509
- // not cause the user to lose what they just typed.
510
- try {
511
- await persistProviderKey(provider, trimmed);
512
- // Do not mirror the key into process.env: getKey() reads models.json
513
- // first, so the store is already the source of truth. Setting the env
514
- // var would leak the plaintext key to every child process (bash tool,
515
- // pi.exec, npm postinstall, etc.).
516
- } catch (err) {
517
- ctx.ui.notify(
518
- `Failed to write ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
519
- "error",
520
- );
521
- return;
522
- }
523
-
524
- // Optional ping — informational only, never fatal.
525
- ctx.ui.setStatus?.("phi-init-ping", `Pinging ${provider.name}...`);
526
- const ping = await pingProvider(provider.id, trimmed, 5_000).catch((err) => ({
527
- ok: false,
528
- error: err instanceof Error ? err.message : String(err),
529
- }));
530
- ctx.ui.setStatus?.("phi-init-ping", undefined);
531
- if (ping.ok) {
532
- ctx.ui.notify(`${provider.name} ping OK (200).`, "info");
533
- } else {
534
- ctx.ui.notify(
535
- `${provider.name} ping failed: ${ping.error ?? "unknown"}. Key saved anyway — you can retry with \`/keys test ${provider.id}\`.`,
536
- "warning",
537
- );
538
- }
539
-
540
- // Fetch live model list (with fallback) and persist it.
541
- ctx.ui.setStatus?.("phi-init-fetch", `Fetching ${provider.name} models...`);
542
- const live = await fetchLiveModels(provider.id, {
543
- apiKey: trimmed,
544
- forceRefresh: true,
545
- timeoutMs: 6_000,
546
- });
547
- ctx.ui.setStatus?.("phi-init-fetch", undefined);
548
-
549
- const persistedModels = live.models.map(toPersistedModel);
550
- try {
551
- if (provider.id === "opencode-go") {
552
- // Qwen/MiniMax are only served via the Anthropic-compatible endpoint;
553
- // GLM/Kimi/DeepSeek/Mimo/Hy3 use the OpenAI shim. Persist them as two
554
- // providers so neither family hits a "not supported for format" 401.
555
- const openaiModels = persistedModels.filter((m) => !isOpenCodeGoAnthropicModel(m.id));
556
- const anthropicModels = persistedModels.filter((m) => isOpenCodeGoAnthropicModel(m.id));
557
- provider.models = openaiModels.map((m) => m.id);
558
- await persistProviderModels(provider, openaiModels);
559
- if (anthropicModels.length > 0) {
560
- await persistOpenCodeGoAnthropic(trimmed, anthropicModels);
561
- ctx.ui.notify(
562
- `OpenCode Go: ${anthropicModels.length} Qwen/MiniMax model(s) routed via the Anthropic-compatible provider \`opencode-go-anthropic\` (assign them with \`/plan-models\` or \`/model\`).`,
563
- "info",
564
- );
565
- }
566
- } else {
567
- await persistProviderModels(provider, persistedModels);
568
- }
569
- } catch (err) {
570
- ctx.ui.notify(
571
- `Failed to write provider models to ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
572
- "error",
573
- );
574
- return;
575
- }
576
-
577
- provider.models = persistedModels.map((m) => m.id);
578
- provider.available = true;
579
- const masked = `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
580
- ctx.ui.notify(
581
- `${provider.name} configured (${masked}) — ${persistedModels.length} models (source: ${live.source}${live.error ? `, ${live.error}` : ""}).\n`,
582
- "info",
583
- );
584
- }
96
+ "utf-8",
97
+ );
98
+ }
585
99
 
586
- // ─── Command ─────────────────────────────────────────────────────
100
+ // ─── Extension ───────────────────────────────────────────────────────────
587
101
 
102
+ export default function initExtension(pi: ExtensionAPI) {
588
103
  pi.registerCommand("phi-init", {
589
- description: "Initialize Phi Code (legacy alias prefer /setup for the refined wizard)",
104
+ description: "Initialize Phi Code: scaffold ~/.phi, then run the /setup wizard (legacy alias)",
590
105
  handler: async (_args, ctx) => {
591
106
  try {
592
- ctx.ui.notify(
593
- "`/phi-init` configures **orchestration** roles only (Code / Debug / Plan / Explore / " +
594
- "Test / Review — used by `/plan` and the orchestrator).\n\n" +
595
- "The **chat default model** is owned exclusively by `/model` and stays sticky across " +
596
- "prompts. This wizard will NOT change it.",
597
- "info",
598
- );
599
-
600
- ctx.ui.notify(" Phi Code Setup Wizard (orchestration roles)", "info");
601
-
602
- // 1. Detect providers (env vars + local servers + previously saved keys)
603
- ctx.ui.notify("Detecting providers...\n", "info");
604
- const providers = detectProviders();
605
-
606
- // Merge in any previously saved providers from models.json.
607
- const savedConfig = await readModelsConfig();
608
- for (const [id, config] of Object.entries(savedConfig.providers)) {
609
- const match = providers.find((p) => p.id === id);
610
- if (!match) continue;
611
- if (config?.apiKey) {
612
- match.available = true;
613
- if (Array.isArray(config.models) && config.models.length > 0) {
614
- match.models = config.models.map((m: any) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
615
- }
616
- }
617
- }
618
-
619
- // Probe local providers (Ollama, LM Studio)
620
- await detectLocalProviders(providers);
621
-
622
- ctx.ui.notify("Provider Status:", "info");
623
- for (const p of providers) {
624
- const status = p.available ? "[ok]" : "[--]";
625
- const tag = p.local ? " (local)" : "";
626
- const modelCount = p.available ? ` — ${p.models.length} model(s)` : "";
627
- ctx.ui.notify(` ${status} ${p.name}${tag}${modelCount}`, "info");
628
- }
629
-
630
- // Provider configuration loop
631
- let addingProviders = true;
632
- while (addingProviders) {
633
- const providerOptions = [
634
- "Done — continue with current providers",
635
- ...providers.map((p) => {
636
- const status = p.available ? "[ok]" : "[--]";
637
- const tag = p.local ? " (local)" : "";
638
- const modelCount = p.available ? ` (${p.models.length} models)` : "";
639
- return `${status} ${p.name}${tag}${modelCount}`;
640
- }),
641
- ];
642
- const addProvider = await ctx.ui.select("Configure a provider (add multiple!):", providerOptions);
643
-
644
- const choiceIdx = providerOptions.indexOf(addProvider ?? "");
645
- if (choiceIdx <= 0) {
646
- addingProviders = false;
647
- break;
648
- }
649
-
650
- const chosen = providers[choiceIdx - 1];
651
- try {
652
- await configureProvider(chosen, ctx);
653
- } catch (err) {
654
- // Never bubble up — keep the wizard alive.
655
- ctx.ui.notify(
656
- `Provider configuration failed: ${err instanceof Error ? err.message : String(err)}`,
657
- "error",
658
- );
659
- }
660
- }
661
-
662
- const available = providers.filter((p) => p.available);
663
- if (available.length === 0) {
664
- ctx.ui.notify("No providers available. Run `/phi-init` again after setting up a provider.", "error");
665
- return;
666
- }
667
-
668
- const allModels = getAllAvailableModels(providers);
669
- ctx.ui.notify(`\n${allModels.length} models available from ${available.length} provider(s).\n`, "info");
670
-
671
- // 2. Assign models to agents
672
- ctx.ui.notify("Assign a model to each agent role:\n", "info");
673
- const assignments = await manualMode(allModels, ctx);
674
-
675
- // 3. Persist everything
676
- ctx.ui.notify("Creating directories...", "info");
107
+ ctx.ui.notify("Scaffolding ~/.phi (directories, bundled agents, AGENTS.md template)...", "info");
677
108
  await ensureDirs();
678
-
679
- ctx.ui.notify("Writing routing configuration...", "info");
680
- const routing = createRouting(assignments);
681
- await writeFile(join(agentDir, "routing.json"), JSON.stringify(routing, null, 2), "utf-8");
682
-
683
- ctx.ui.notify("Setting up sub-agents...", "info");
684
109
  await copyBundledAgents();
685
-
686
- ctx.ui.notify("Creating memory template...", "info");
687
110
  await createAgentsTemplate();
688
-
689
- ctx.ui.notify("\n Setup Complete!\n", "info");
690
- ctx.ui.notify("Configuration:", "info");
691
- ctx.ui.notify(` Config: ${agentDir}`, "info");
692
- ctx.ui.notify(` Memory: ${memoryDir}`, "info");
693
- ctx.ui.notify(` Agents: ${agentsDir}`, "info");
694
- ctx.ui.notify("\nOrchestration role assignments (used by `/plan`):", "info");
695
- for (const role of TASK_ROLES) {
696
- const a = assignments[role.key];
697
- ctx.ui.notify(` ${role.label}: \`${a.preferred}\` (fallback: \`${a.fallback}\`)`, "info");
698
- }
699
- ctx.ui.notify("\nChat default model: use `/model` (this wizard does NOT change the chat default).", "info");
700
- ctx.ui.notify("\nNext steps:", "info");
701
- ctx.ui.notify(" - `/model` to pick the chat default model (sticky across prompts)", "info");
702
- ctx.ui.notify(" - `/plan <description>` to run the orchestrator with the roles above", "info");
703
- ctx.ui.notify(" - `/routing` to inspect the route table (auto-switch is OFF by default)", "info");
704
- ctx.ui.notify(" - `/models refresh` to re-fetch the live model catalog", "info");
705
- ctx.ui.notify(" - Edit `~/.phi/memory/AGENTS.md` with your project instructions", "info");
706
- ctx.ui.notify(" - Start coding!\n", "info");
111
+ ctx.ui.notify(
112
+ "`/phi-init` delegates to the `/setup` wizard (one wizard, no drift). " +
113
+ "Use `/plan-models` afterwards to reassign per-role models without re-probing providers.",
114
+ "info",
115
+ );
116
+ await runSetupWizard(ctx.ui);
707
117
  } catch (error) {
708
118
  const message = error instanceof Error ? error.message : String(error);
709
119
  ctx.ui.notify(`Setup failed: ${message}`, "error");
@@ -712,10 +122,9 @@ _Edit this file to customize Phi Code's behavior for your project._
712
122
  });
713
123
 
714
124
  // ─── /plan-models : lightweight per-role model reconfiguration ─────
715
- // Standalone alternative to re-running the full /phi-init wizard. Sources the
716
- // model list from the already-loaded model registry (no provider probing),
717
- // shows the current routing, and lets you reassign each /plan role with the
718
- // same provider-qualified picker (id [provider]).
125
+ // Standalone alternative to re-running the full wizard. Sources the model
126
+ // list from the already-loaded model registry (no provider probing), shows
127
+ // the current routing, and reuses the shared picker + routing writer.
719
128
  pi.registerCommand("plan-models", {
720
129
  description: "Reconfigure the per-role models used by /plan (provider-qualified, cross-provider)",
721
130
  handler: async (_args, ctx) => {
@@ -725,16 +134,15 @@ _Edit this file to customize Phi Code's behavior for your project._
725
134
  const seen = new Set<string>();
726
135
  for (const m of registryModels) {
727
136
  if (!m?.provider || !m?.id) continue;
137
+ // Provider-qualified reference ("provider/id") so the same model id
138
+ // offered by several providers stays distinct at selection time.
728
139
  const ref = `${m.provider}/${m.id}`;
729
140
  if (seen.has(ref)) continue;
730
141
  seen.add(ref);
731
142
  available.push({ ref, display: `${m.id} [${m.provider}]` });
732
143
  }
733
144
  if (available.length === 0) {
734
- ctx.ui.notify(
735
- "No configured models found. Add a provider via `/phi-init` or `/setup` first.",
736
- "warning",
737
- );
145
+ ctx.ui.notify("No configured models found. Add a provider via `/setup` first.", "warning");
738
146
  return;
739
147
  }
740
148
 
@@ -745,18 +153,23 @@ _Edit this file to customize Phi Code's behavior for your project._
745
153
  } catch {
746
154
  /* no routing config yet */
747
155
  }
748
- const currentLines = TASK_ROLES.map((r) => {
749
- const route = current.routes?.[r.key];
750
- return ` ${r.label}: ${route?.preferredModel || "default"} (fallback: ${route?.fallback || "default"})`;
751
- }).join("\n");
156
+ const roleKeys = [
157
+ ...ORCHESTRATION_ROLES.map((r) => ({ key: r.key, label: r.label })),
158
+ { key: "debug", label: "Debug" },
159
+ ];
160
+ const currentLines = roleKeys
161
+ .map(({ key, label }) => {
162
+ const route = current.routes?.[key];
163
+ return ` ${label}: ${route?.preferredModel || "default"} (fallback: ${route?.fallback || "default"})`;
164
+ })
165
+ .join("\n");
752
166
  ctx.ui.notify(`Current /plan models:\n${currentLines}\n`, "info");
753
167
 
754
- const assignments = await manualMode(available, ctx);
755
-
168
+ const { defaultModel, orchestration } = await configureAssignments(ctx.ui, available);
756
169
  await ensureDirs();
757
- const routing = createRouting(assignments);
758
- await writeFile(join(agentDir, "routing.json"), JSON.stringify(routing, null, 2), "utf-8");
759
- ctx.ui.notify("Updated `~/.phi/agent/routing.json`. `/plan` will use these per-role models.", "info");
170
+ const routing = buildRoutingConfig(defaultModel, orchestration);
171
+ const routingPath = await writeRoutingConfig(routing);
172
+ ctx.ui.notify(`Updated \`${routingPath}\`. \`/plan\` will use these per-role models.`, "info");
760
173
  } catch (error) {
761
174
  const message = error instanceof Error ? error.message : String(error);
762
175
  ctx.ui.notify(`/plan-models failed: ${message}`, "error");