@phi-code-admin/phi-code 0.96.1 → 0.97.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/core/slash-commands.d.ts +13 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +38 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +67 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/extensions/phi/benchmark.ts +12 -56
- package/extensions/phi/init.ts +86 -673
- package/extensions/phi/providers/catalog.ts +133 -0
- package/extensions/phi/setup.ts +180 -280
- package/extensions/phi/smart-router.ts +0 -17
- package/package.json +3 -3
package/extensions/phi/init.ts
CHANGED
|
@@ -1,323 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phi Init Extension -
|
|
2
|
+
* Phi Init Extension - legacy `/phi-init` entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
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 {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
// ─── Extension ───────────────────────────────────────────────────────────
|
|
587
101
|
|
|
102
|
+
export default function initExtension(pi: ExtensionAPI) {
|
|
588
103
|
pi.registerCommand("phi-init", {
|
|
589
|
-
description: "Initialize Phi Code
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
ctx.ui
|
|
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
|
|
716
|
-
//
|
|
717
|
-
//
|
|
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
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
755
|
-
|
|
168
|
+
const { defaultModel, orchestration } = await configureAssignments(ctx.ui, available);
|
|
756
169
|
await ensureDirs();
|
|
757
|
-
const routing =
|
|
758
|
-
|
|
759
|
-
ctx.ui.notify(
|
|
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");
|