@phi-code-admin/phi-code 0.75.4 → 0.75.5
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/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +3 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/extensions/phi/README.md +21 -1
- package/extensions/phi/init.ts +366 -372
- package/extensions/phi/models.ts +236 -0
- package/extensions/phi/providers/live-models.ts +493 -0
- package/extensions/phi/providers/opencode-go.ts +15 -10
- package/extensions/phi/setup.ts +84 -32
- package/package.json +1 -1
package/extensions/phi/init.ts
CHANGED
|
@@ -1,144 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phi Init Extension - Interactive setup wizard for Phi Code
|
|
2
|
+
* Phi Init Extension - Interactive setup wizard for Phi Code (legacy `/phi-init`).
|
|
3
3
|
*
|
|
4
|
-
* Detects providers (API keys + local endpoints), then lets the user
|
|
5
|
-
*
|
|
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).
|
|
6
6
|
*
|
|
7
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.
|
|
8
18
|
*/
|
|
9
19
|
|
|
10
20
|
import type { ExtensionAPI } from "phi-code";
|
|
11
|
-
import { writeFile, mkdir, copyFile, readdir,
|
|
21
|
+
import { writeFile, mkdir, copyFile, readdir, readFile } from "node:fs/promises";
|
|
12
22
|
import { join, resolve } from "node:path";
|
|
13
23
|
import { homedir } from "node:os";
|
|
14
24
|
import { existsSync } from "node:fs";
|
|
25
|
+
import { fetchLiveModels, pingProvider, toPersistedModel } from "./providers/live-models.js";
|
|
15
26
|
|
|
16
27
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
17
28
|
|
|
18
29
|
interface DetectedProvider {
|
|
30
|
+
id: string;
|
|
19
31
|
name: string;
|
|
20
32
|
envVar: string;
|
|
21
33
|
baseUrl: string;
|
|
22
|
-
|
|
34
|
+
api: string;
|
|
35
|
+
requiresApiKey: boolean;
|
|
23
36
|
available: boolean;
|
|
24
|
-
|
|
37
|
+
models: string[];
|
|
38
|
+
/** True for Ollama/LM Studio (models discovered at runtime, no key required). */
|
|
39
|
+
local?: boolean;
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
interface RoutingConfig {
|
|
28
|
-
routes: Record<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ─── Dynamic Model Specs via OpenRouter ──────────────────────────────────
|
|
39
|
-
|
|
40
|
-
interface ModelSpec {
|
|
41
|
-
contextWindow: number;
|
|
42
|
-
maxTokens: number;
|
|
43
|
-
reasoning: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Cache for OpenRouter model data (fetched once per session)
|
|
47
|
-
let openRouterCache: Map<string, ModelSpec> | null = null;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Fetch model specs from OpenRouter's free API (no key needed).
|
|
51
|
-
* Returns a map of model base name → specs.
|
|
52
|
-
* Falls back to conservative defaults if unreachable.
|
|
53
|
-
*/
|
|
54
|
-
async function fetchModelSpecs(): Promise<Map<string, ModelSpec>> {
|
|
55
|
-
if (openRouterCache) return openRouterCache;
|
|
56
|
-
|
|
57
|
-
const cache = new Map<string, ModelSpec>();
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const controller = new AbortController();
|
|
61
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
62
|
-
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
63
|
-
signal: controller.signal,
|
|
64
|
-
});
|
|
65
|
-
clearTimeout(timeout);
|
|
66
|
-
|
|
67
|
-
if (res.ok) {
|
|
68
|
-
const data = await res.json() as any;
|
|
69
|
-
for (const m of (data.data || [])) {
|
|
70
|
-
const contextLength = m.context_length || 128000;
|
|
71
|
-
const maxOutput = m.top_provider?.max_completion_tokens || Math.min(contextLength, 16384);
|
|
72
|
-
const hasReasoning = (m.supported_parameters || []).includes("reasoning");
|
|
73
|
-
|
|
74
|
-
// Store by full ID and by base name (for fuzzy matching)
|
|
75
|
-
const spec: ModelSpec = {
|
|
76
|
-
contextWindow: contextLength,
|
|
77
|
-
maxTokens: typeof maxOutput === "number" ? maxOutput : 16384,
|
|
78
|
-
reasoning: hasReasoning,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
cache.set(m.id, spec);
|
|
82
|
-
// Also store by short name (e.g., "qwen3.5-plus" from "qwen/qwen3.5-plus-02-15")
|
|
83
|
-
const parts = m.id.split("/");
|
|
84
|
-
if (parts.length > 1) {
|
|
85
|
-
cache.set(parts[1], spec);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
43
|
+
routes: Record<
|
|
44
|
+
string,
|
|
45
|
+
{
|
|
46
|
+
description: string;
|
|
47
|
+
keywords: string[];
|
|
48
|
+
preferredModel: string;
|
|
49
|
+
fallback: string;
|
|
50
|
+
agent: string;
|
|
88
51
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
openRouterCache = cache;
|
|
94
|
-
return cache;
|
|
52
|
+
>;
|
|
53
|
+
default: { model: string; agent: string | null };
|
|
95
54
|
}
|
|
96
55
|
|
|
97
|
-
|
|
98
|
-
* Get model spec by ID. Tries OpenRouter cache first with fuzzy matching,
|
|
99
|
-
* then falls back to conservative defaults.
|
|
100
|
-
*/
|
|
101
|
-
async function getModelSpec(id: string): Promise<ModelSpec> {
|
|
102
|
-
const cache = await fetchModelSpecs();
|
|
103
|
-
|
|
104
|
-
// Exact match
|
|
105
|
-
if (cache.has(id)) return cache.get(id)!;
|
|
56
|
+
// ─── One-time global unhandledRejection guard ────────────────────────────
|
|
106
57
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
58
|
+
let unhandledRejectionGuardInstalled = false;
|
|
59
|
+
function installUnhandledRejectionGuard(): void {
|
|
60
|
+
if (unhandledRejectionGuardInstalled) return;
|
|
61
|
+
unhandledRejectionGuardInstalled = true;
|
|
62
|
+
process.on("unhandledRejection", (reason) => {
|
|
63
|
+
// Swallow async failures from extension wizards so they cannot terminate the TUI.
|
|
64
|
+
// The wizard itself reports the failure via ctx.ui.notify in its own try/catch.
|
|
65
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
66
|
+
try {
|
|
67
|
+
// Best effort logging — process.stderr survives TUI shutdown unlike console.
|
|
68
|
+
process.stderr.write(`[phi-init] swallowed unhandledRejection: ${message}\n`);
|
|
69
|
+
} catch {
|
|
70
|
+
// no-op
|
|
120
71
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Conservative fallback
|
|
124
|
-
return { contextWindow: 128000, maxTokens: 16384, reasoning: true };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Synchronous fallback for non-async contexts.
|
|
129
|
-
* Uses cached data if available, otherwise returns defaults.
|
|
130
|
-
*/
|
|
131
|
-
function getModelSpecSync(id: string): ModelSpec {
|
|
132
|
-
if (!openRouterCache) return { contextWindow: 128000, maxTokens: 16384, reasoning: true };
|
|
133
|
-
|
|
134
|
-
if (openRouterCache.has(id)) return openRouterCache.get(id)!;
|
|
135
|
-
|
|
136
|
-
const prefixes = ["qwen/", "moonshotai/", "z-ai/", "minimax/", "openai/", "anthropic/", "google/"];
|
|
137
|
-
for (const prefix of prefixes) {
|
|
138
|
-
if (openRouterCache.has(prefix + id)) return openRouterCache.get(prefix + id)!;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { contextWindow: 128000, maxTokens: 16384, reasoning: true };
|
|
72
|
+
});
|
|
142
73
|
}
|
|
143
74
|
|
|
144
75
|
// ─── Provider Detection ──────────────────────────────────────────────────
|
|
@@ -146,115 +77,132 @@ function getModelSpecSync(id: string): ModelSpec {
|
|
|
146
77
|
function detectProviders(): DetectedProvider[] {
|
|
147
78
|
const providers: DetectedProvider[] = [
|
|
148
79
|
{
|
|
80
|
+
id: "alibaba-codingplan",
|
|
149
81
|
name: "Alibaba Coding Plan",
|
|
150
82
|
envVar: "ALIBABA_CODING_PLAN_KEY",
|
|
151
83
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
152
|
-
|
|
84
|
+
api: "openai-completions",
|
|
85
|
+
requiresApiKey: true,
|
|
153
86
|
available: false,
|
|
87
|
+
models: [],
|
|
154
88
|
},
|
|
155
89
|
{
|
|
90
|
+
id: "opencode-go",
|
|
156
91
|
name: "OpenCode Go",
|
|
157
92
|
envVar: "OPENCODE_GO_API_KEY",
|
|
158
93
|
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
159
|
-
|
|
94
|
+
api: "openai-completions",
|
|
95
|
+
requiresApiKey: true,
|
|
160
96
|
available: false,
|
|
97
|
+
models: [],
|
|
161
98
|
},
|
|
162
99
|
{
|
|
100
|
+
id: "openai",
|
|
163
101
|
name: "OpenAI",
|
|
164
102
|
envVar: "OPENAI_API_KEY",
|
|
165
103
|
baseUrl: "https://api.openai.com/v1",
|
|
166
|
-
|
|
104
|
+
api: "openai-completions",
|
|
105
|
+
requiresApiKey: true,
|
|
167
106
|
available: false,
|
|
107
|
+
models: [],
|
|
168
108
|
},
|
|
169
109
|
{
|
|
110
|
+
id: "anthropic",
|
|
170
111
|
name: "Anthropic",
|
|
171
112
|
envVar: "ANTHROPIC_API_KEY",
|
|
172
113
|
baseUrl: "https://api.anthropic.com/v1",
|
|
173
|
-
|
|
114
|
+
api: "anthropic-messages",
|
|
115
|
+
requiresApiKey: true,
|
|
174
116
|
available: false,
|
|
117
|
+
models: [],
|
|
175
118
|
},
|
|
176
119
|
{
|
|
177
|
-
|
|
120
|
+
id: "google",
|
|
121
|
+
name: "Google Gemini",
|
|
178
122
|
envVar: "GOOGLE_API_KEY",
|
|
179
123
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
180
|
-
|
|
124
|
+
api: "google",
|
|
125
|
+
requiresApiKey: true,
|
|
181
126
|
available: false,
|
|
127
|
+
models: [],
|
|
182
128
|
},
|
|
183
129
|
{
|
|
130
|
+
id: "openrouter",
|
|
184
131
|
name: "OpenRouter",
|
|
185
132
|
envVar: "OPENROUTER_API_KEY",
|
|
186
133
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
187
|
-
|
|
134
|
+
api: "openai-completions",
|
|
135
|
+
requiresApiKey: true,
|
|
188
136
|
available: false,
|
|
137
|
+
models: [],
|
|
189
138
|
},
|
|
190
139
|
{
|
|
140
|
+
id: "groq",
|
|
191
141
|
name: "Groq",
|
|
192
142
|
envVar: "GROQ_API_KEY",
|
|
193
143
|
baseUrl: "https://api.groq.com/openai/v1",
|
|
194
|
-
|
|
144
|
+
api: "openai-completions",
|
|
145
|
+
requiresApiKey: true,
|
|
195
146
|
available: false,
|
|
147
|
+
models: [],
|
|
196
148
|
},
|
|
197
149
|
{
|
|
150
|
+
id: "ollama",
|
|
198
151
|
name: "Ollama",
|
|
199
152
|
envVar: "OLLAMA",
|
|
200
153
|
baseUrl: "http://localhost:11434/v1",
|
|
201
|
-
|
|
154
|
+
api: "openai-completions",
|
|
155
|
+
requiresApiKey: false,
|
|
202
156
|
available: false,
|
|
157
|
+
models: [],
|
|
203
158
|
local: true,
|
|
204
159
|
},
|
|
205
160
|
{
|
|
161
|
+
id: "lm-studio",
|
|
206
162
|
name: "LM Studio",
|
|
207
163
|
envVar: "LM_STUDIO",
|
|
208
164
|
baseUrl: "http://localhost:1234/v1",
|
|
209
|
-
|
|
165
|
+
api: "openai-completions",
|
|
166
|
+
requiresApiKey: false,
|
|
210
167
|
available: false,
|
|
168
|
+
models: [],
|
|
211
169
|
local: true,
|
|
212
170
|
},
|
|
213
171
|
];
|
|
214
172
|
|
|
215
173
|
for (const p of providers) {
|
|
216
|
-
if (p.local) {
|
|
217
|
-
|
|
218
|
-
p.available = false; // Will be checked async in detectLocalProviders()
|
|
219
|
-
} else {
|
|
220
|
-
p.available = !!process.env[p.envVar];
|
|
174
|
+
if (!p.local && process.env[p.envVar]) {
|
|
175
|
+
p.available = true;
|
|
221
176
|
}
|
|
222
177
|
}
|
|
223
|
-
|
|
224
178
|
return providers;
|
|
225
179
|
}
|
|
226
180
|
|
|
227
181
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
182
|
+
* Probe local providers (Ollama, LM Studio) and live-fetch their model list.
|
|
183
|
+
* Failures are silent — local servers are optional.
|
|
230
184
|
*/
|
|
231
185
|
async function detectLocalProviders(providers: DetectedProvider[]): Promise<void> {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
headers: { Authorization: `Bearer ${p.envVar === "OLLAMA" ? "ollama" : "lm-studio"}` },
|
|
240
|
-
});
|
|
241
|
-
clearTimeout(timeout);
|
|
242
|
-
if (res.ok) {
|
|
243
|
-
const data = await res.json() as any;
|
|
244
|
-
const models = (data.data || []).map((m: any) => m.id).filter(Boolean);
|
|
245
|
-
if (models.length > 0) {
|
|
246
|
-
p.models = models;
|
|
186
|
+
await Promise.all(
|
|
187
|
+
providers
|
|
188
|
+
.filter((p) => p.local)
|
|
189
|
+
.map(async (p) => {
|
|
190
|
+
const result = await fetchLiveModels(p.id, { forceRefresh: true, timeoutMs: 2_500 });
|
|
191
|
+
if (result.models.length > 0 && result.source !== "fallback") {
|
|
192
|
+
p.models = result.models.map((m) => m.id);
|
|
247
193
|
p.available = true;
|
|
248
194
|
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Server not running — that's fine
|
|
252
|
-
}
|
|
253
|
-
}
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
254
197
|
}
|
|
255
198
|
|
|
256
199
|
function getAllAvailableModels(providers: DetectedProvider[]): string[] {
|
|
257
|
-
|
|
200
|
+
const ids = new Set<string>();
|
|
201
|
+
for (const p of providers) {
|
|
202
|
+
if (!p.available) continue;
|
|
203
|
+
for (const id of p.models) ids.add(id);
|
|
204
|
+
}
|
|
205
|
+
return [...ids];
|
|
258
206
|
}
|
|
259
207
|
|
|
260
208
|
// ─── Routing Presets ─────────────────────────────────────────────────────
|
|
@@ -277,7 +225,9 @@ const KEYWORDS: Record<string, string[]> = {
|
|
|
277
225
|
review: ["review", "audit", "quality", "security", "improve", "optimize"],
|
|
278
226
|
};
|
|
279
227
|
|
|
280
|
-
function createRouting(
|
|
228
|
+
function createRouting(
|
|
229
|
+
assignments: Record<string, { preferred: string; fallback: string }>,
|
|
230
|
+
): RoutingConfig {
|
|
281
231
|
const routes: RoutingConfig["routes"] = {};
|
|
282
232
|
for (const role of TASK_ROLES) {
|
|
283
233
|
const assignment = assignments[role.key];
|
|
@@ -298,35 +248,37 @@ function createRouting(assignments: Record<string, { preferred: string; fallback
|
|
|
298
248
|
// ─── Extension ───────────────────────────────────────────────────────────
|
|
299
249
|
|
|
300
250
|
export default function initExtension(pi: ExtensionAPI) {
|
|
251
|
+
installUnhandledRejectionGuard();
|
|
252
|
+
|
|
301
253
|
const phiDir = join(homedir(), ".phi");
|
|
302
254
|
const agentDir = join(phiDir, "agent");
|
|
303
255
|
const agentsDir = join(agentDir, "agents");
|
|
304
256
|
const memoryDir = join(phiDir, "memory");
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
257
|
+
const modelsJsonPath = join(agentDir, "models.json");
|
|
258
|
+
|
|
259
|
+
async function ensureDirs(): Promise<void> {
|
|
260
|
+
for (const dir of [
|
|
261
|
+
agentDir,
|
|
262
|
+
agentsDir,
|
|
263
|
+
join(agentDir, "skills"),
|
|
264
|
+
join(agentDir, "extensions"),
|
|
265
|
+
memoryDir,
|
|
266
|
+
join(memoryDir, "ontology"),
|
|
267
|
+
]) {
|
|
311
268
|
await mkdir(dir, { recursive: true });
|
|
312
269
|
}
|
|
313
270
|
}
|
|
314
271
|
|
|
315
|
-
|
|
316
|
-
* Copy bundled agent definitions to user directory
|
|
317
|
-
*/
|
|
318
|
-
async function copyBundledAgents() {
|
|
272
|
+
async function copyBundledAgents(): Promise<void> {
|
|
319
273
|
const bundledDir = resolve(join(__dirname, "..", "..", "..", "agents"));
|
|
320
274
|
if (!existsSync(bundledDir)) return;
|
|
321
|
-
|
|
322
275
|
try {
|
|
323
276
|
const files = await readdir(bundledDir);
|
|
324
277
|
for (const file of files) {
|
|
325
|
-
if (file.endsWith(".md"))
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
278
|
+
if (!file.endsWith(".md")) continue;
|
|
279
|
+
const dest = join(agentsDir, file);
|
|
280
|
+
if (!existsSync(dest)) {
|
|
281
|
+
await copyFile(join(bundledDir, file), dest);
|
|
330
282
|
}
|
|
331
283
|
}
|
|
332
284
|
} catch {
|
|
@@ -334,14 +286,12 @@ export default function initExtension(pi: ExtensionAPI) {
|
|
|
334
286
|
}
|
|
335
287
|
}
|
|
336
288
|
|
|
337
|
-
|
|
338
|
-
* Create AGENTS.md template
|
|
339
|
-
*/
|
|
340
|
-
async function createAgentsTemplate() {
|
|
289
|
+
async function createAgentsTemplate(): Promise<void> {
|
|
341
290
|
const agentsMdPath = join(memoryDir, "AGENTS.md");
|
|
342
|
-
if (existsSync(agentsMdPath)) return;
|
|
343
|
-
|
|
344
|
-
|
|
291
|
+
if (existsSync(agentsMdPath)) return;
|
|
292
|
+
await writeFile(
|
|
293
|
+
agentsMdPath,
|
|
294
|
+
`# AGENTS.md — Persistent Instructions
|
|
345
295
|
|
|
346
296
|
This file is loaded at the start of every session. Use it to store:
|
|
347
297
|
- Project conventions and rules
|
|
@@ -367,280 +317,324 @@ This file is loaded at the start of every session. Use it to store:
|
|
|
367
317
|
---
|
|
368
318
|
|
|
369
319
|
_Edit this file to customize Phi Code's behavior for your project._
|
|
370
|
-
`,
|
|
320
|
+
`,
|
|
321
|
+
"utf-8",
|
|
322
|
+
);
|
|
371
323
|
}
|
|
372
324
|
|
|
373
|
-
// ───
|
|
325
|
+
// ─── Persistence helper (single source of truth for models.json writes) ──
|
|
374
326
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
327
|
+
async function readModelsConfig(): Promise<{ providers: Record<string, any> }> {
|
|
328
|
+
try {
|
|
329
|
+
const raw = await readFile(modelsJsonPath, "utf-8");
|
|
330
|
+
const parsed = JSON.parse(raw) as { providers?: Record<string, any> };
|
|
331
|
+
return { providers: parsed.providers ?? {} };
|
|
332
|
+
} catch {
|
|
333
|
+
return { providers: {} };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function writeModelsConfig(config: { providers: Record<string, any> }): Promise<void> {
|
|
338
|
+
await mkdir(agentDir, { recursive: true });
|
|
339
|
+
await writeFile(modelsJsonPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function persistProviderKey(
|
|
343
|
+
provider: DetectedProvider,
|
|
344
|
+
apiKey: string,
|
|
345
|
+
): Promise<void> {
|
|
346
|
+
const config = await readModelsConfig();
|
|
347
|
+
const existing = config.providers[provider.id] ?? {};
|
|
348
|
+
config.providers[provider.id] = {
|
|
349
|
+
...existing,
|
|
350
|
+
baseUrl: provider.baseUrl,
|
|
351
|
+
api: provider.api,
|
|
352
|
+
apiKey,
|
|
353
|
+
};
|
|
354
|
+
await writeModelsConfig(config);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function persistProviderModels(
|
|
358
|
+
provider: DetectedProvider,
|
|
359
|
+
models: ReturnType<typeof toPersistedModel>[],
|
|
360
|
+
): Promise<void> {
|
|
361
|
+
const config = await readModelsConfig();
|
|
362
|
+
const existing = config.providers[provider.id] ?? {};
|
|
363
|
+
config.providers[provider.id] = {
|
|
364
|
+
...existing,
|
|
365
|
+
baseUrl: provider.baseUrl,
|
|
366
|
+
api: provider.api,
|
|
367
|
+
models,
|
|
368
|
+
};
|
|
369
|
+
await writeModelsConfig(config);
|
|
370
|
+
}
|
|
380
371
|
|
|
381
|
-
|
|
382
|
-
ctx.ui.notify("🎛️ Manual mode: assign a model to each task category.\n", "info");
|
|
372
|
+
// ─── Manual model assignment (one model per agent role) ──────────────
|
|
383
373
|
|
|
374
|
+
async function manualMode(
|
|
375
|
+
availableModels: string[],
|
|
376
|
+
ctx: any,
|
|
377
|
+
): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
378
|
+
ctx.ui.notify("Manual mode: assign a model to each task category.\n", "info");
|
|
384
379
|
const modelOptions = ["default (use current model)", ...availableModels];
|
|
385
380
|
const assignments: Record<string, { preferred: string; fallback: string }> = {};
|
|
386
381
|
|
|
387
382
|
for (const role of TASK_ROLES) {
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
`${role.label} — ${role.desc}`,
|
|
391
|
-
modelOptions,
|
|
392
|
-
);
|
|
393
|
-
const preferredModel = (chosen && chosen !== modelOptions[0]) ? chosen : "default";
|
|
383
|
+
const chosen = await ctx.ui.select(`${role.label} — ${role.desc}`, modelOptions);
|
|
384
|
+
const preferredModel = chosen && chosen !== modelOptions[0] ? chosen : "default";
|
|
394
385
|
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
`Fallback for ${role.label}`,
|
|
399
|
-
fallbackOptions,
|
|
400
|
-
);
|
|
401
|
-
const fallback = (fallbackChoice && fallbackChoice !== modelOptions[0]) ? fallbackChoice : "default";
|
|
386
|
+
const fallbackOptions = modelOptions.filter((m) => m !== chosen);
|
|
387
|
+
const fallbackChoice = await ctx.ui.select(`Fallback for ${role.label}`, fallbackOptions);
|
|
388
|
+
const fallback = fallbackChoice && fallbackChoice !== modelOptions[0] ? fallbackChoice : "default";
|
|
402
389
|
|
|
403
390
|
assignments[role.key] = { preferred: preferredModel, fallback };
|
|
404
|
-
ctx.ui.notify(`
|
|
391
|
+
ctx.ui.notify(` ${role.label}: ${preferredModel} (fallback: ${fallback})`, "info");
|
|
405
392
|
}
|
|
406
393
|
|
|
407
|
-
// Default model
|
|
408
394
|
const defaultChoice = await ctx.ui.select("Default model (for general tasks)", modelOptions);
|
|
409
|
-
|
|
395
|
+
const defaultModel = defaultChoice && defaultChoice !== modelOptions[0] ? defaultChoice : "default";
|
|
410
396
|
assignments["default"] = { preferred: defaultModel, fallback: availableModels[0] || "default" };
|
|
411
|
-
|
|
412
397
|
return assignments;
|
|
413
398
|
}
|
|
414
399
|
|
|
415
|
-
// ───
|
|
400
|
+
// ─── Per-provider configuration step ─────────────────────────────────
|
|
401
|
+
|
|
402
|
+
async function configureProvider(provider: DetectedProvider, ctx: any): Promise<void> {
|
|
403
|
+
if (provider.local) {
|
|
404
|
+
const port = provider.id === "ollama" ? 11434 : 1234;
|
|
405
|
+
const result = await fetchLiveModels(provider.id, { forceRefresh: true, timeoutMs: 2_500 });
|
|
406
|
+
if (result.source === "live" && result.models.length > 0) {
|
|
407
|
+
provider.models = result.models.map((m) => m.id);
|
|
408
|
+
provider.available = true;
|
|
409
|
+
ctx.ui.notify(
|
|
410
|
+
`${provider.name} is running with ${provider.models.length} model(s).\n`,
|
|
411
|
+
"info",
|
|
412
|
+
);
|
|
413
|
+
} else {
|
|
414
|
+
ctx.ui.notify(
|
|
415
|
+
`${provider.name} not reachable on port ${port}. Start it and re-run \`/phi-init\`.\n`,
|
|
416
|
+
"warning",
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
416
421
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
422
|
+
// Cloud provider — API key required.
|
|
423
|
+
ctx.ui.notify(
|
|
424
|
+
`\n${provider.name}\nNote: the key you type will be visible on screen. Stored in ${modelsJsonPath} (chmod 0600 on Unix).`,
|
|
425
|
+
"info",
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const apiKey = await ctx.ui.input(
|
|
429
|
+
`Enter your ${provider.name} API key`,
|
|
430
|
+
"Paste your key here",
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (apiKey === undefined) {
|
|
434
|
+
ctx.ui.notify("Cancelled. No key saved.", "warning");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const trimmed = apiKey.trim();
|
|
438
|
+
if (trimmed.length < 5) {
|
|
439
|
+
ctx.ui.notify("Invalid API key (too short). Skipped.\n", "error");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Persist the key FIRST. Any subsequent failure during live-fetch must
|
|
444
|
+
// not cause the user to lose what they just typed.
|
|
445
|
+
try {
|
|
446
|
+
await persistProviderKey(provider, trimmed);
|
|
447
|
+
process.env[provider.envVar] = trimmed;
|
|
448
|
+
} catch (err) {
|
|
420
449
|
ctx.ui.notify(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"separate chat/orchestration assignments, hot-reload integration). " +
|
|
424
|
-
"This legacy command still works for backwards compatibility.",
|
|
425
|
-
"info",
|
|
450
|
+
`Failed to write ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
451
|
+
"error",
|
|
426
452
|
);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
ctx.ui.notify("║ φ Phi Code Setup Wizard ║", "info");
|
|
430
|
-
ctx.ui.notify("╚══════════════════════════════════════╝\n", "info");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
431
455
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
456
|
+
// Optional ping — informational only, never fatal.
|
|
457
|
+
ctx.ui.setStatus?.("phi-init-ping", `Pinging ${provider.name}...`);
|
|
458
|
+
const ping = await pingProvider(provider.id, trimmed, 5_000).catch((err) => ({
|
|
459
|
+
ok: false,
|
|
460
|
+
error: err instanceof Error ? err.message : String(err),
|
|
461
|
+
}));
|
|
462
|
+
ctx.ui.setStatus?.("phi-init-ping", undefined);
|
|
463
|
+
if (ping.ok) {
|
|
464
|
+
ctx.ui.notify(`${provider.name} ping OK (200).`, "info");
|
|
465
|
+
} else {
|
|
466
|
+
ctx.ui.notify(
|
|
467
|
+
`${provider.name} ping failed: ${ping.error ?? "unknown"}. Key saved anyway — you can retry with \`/keys test ${provider.id}\`.`,
|
|
468
|
+
"warning",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
435
471
|
|
|
436
|
-
|
|
437
|
-
|
|
472
|
+
// Fetch live model list (with fallback) and persist it.
|
|
473
|
+
ctx.ui.setStatus?.("phi-init-fetch", `Fetching ${provider.name} models...`);
|
|
474
|
+
const live = await fetchLiveModels(provider.id, {
|
|
475
|
+
apiKey: trimmed,
|
|
476
|
+
forceRefresh: true,
|
|
477
|
+
timeoutMs: 6_000,
|
|
478
|
+
});
|
|
479
|
+
ctx.ui.setStatus?.("phi-init-fetch", undefined);
|
|
480
|
+
|
|
481
|
+
const persistedModels = live.models.map(toPersistedModel);
|
|
482
|
+
try {
|
|
483
|
+
await persistProviderModels(provider, persistedModels);
|
|
484
|
+
} catch (err) {
|
|
485
|
+
ctx.ui.notify(
|
|
486
|
+
`Failed to write provider models to ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
487
|
+
"error",
|
|
488
|
+
);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
provider.models = persistedModels.map((m) => m.id);
|
|
493
|
+
provider.available = true;
|
|
494
|
+
const masked = `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
|
|
495
|
+
ctx.ui.notify(
|
|
496
|
+
`${provider.name} configured (${masked}) — ${persistedModels.length} models (source: ${live.source}${live.error ? `, ${live.error}` : ""}).\n`,
|
|
497
|
+
"info",
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── Command ─────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
pi.registerCommand("phi-init", {
|
|
504
|
+
description: "Initialize Phi Code (legacy alias — prefer /setup for the refined wizard)",
|
|
505
|
+
handler: async (_args, ctx) => {
|
|
506
|
+
try {
|
|
507
|
+
ctx.ui.notify(
|
|
508
|
+
"NOTE: `/phi-init` is the legacy wizard. The refined replacement is `/setup` " +
|
|
509
|
+
"(richer flow: Alibaba dual-endpoint, OpenCode Go auto-fetch, ping validation, " +
|
|
510
|
+
"separate chat/orchestration assignments, hot-reload integration). " +
|
|
511
|
+
"This legacy command still works for backwards compatibility.",
|
|
512
|
+
"info",
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
ctx.ui.notify(" Phi Code Setup Wizard", "info");
|
|
516
|
+
|
|
517
|
+
// 1. Detect providers (env vars + local servers + previously saved keys)
|
|
518
|
+
ctx.ui.notify("Detecting providers...\n", "info");
|
|
438
519
|
const providers = detectProviders();
|
|
439
520
|
|
|
440
|
-
//
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const match = providers.find(p =>
|
|
450
|
-
id.includes(p.name.toLowerCase().split(" ")[0]) ||
|
|
451
|
-
p.name.toLowerCase().replace(/\s+/g, "-") === id
|
|
452
|
-
);
|
|
453
|
-
if (match) {
|
|
454
|
-
match.available = true;
|
|
455
|
-
if (config.models?.length > 0) {
|
|
456
|
-
match.models = config.models.map((m: any) => m.id || m);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
521
|
+
// Merge in any previously saved providers from models.json.
|
|
522
|
+
const savedConfig = await readModelsConfig();
|
|
523
|
+
for (const [id, config] of Object.entries(savedConfig.providers)) {
|
|
524
|
+
const match = providers.find((p) => p.id === id);
|
|
525
|
+
if (!match) continue;
|
|
526
|
+
if (config?.apiKey) {
|
|
527
|
+
match.available = true;
|
|
528
|
+
if (Array.isArray(config.models) && config.models.length > 0) {
|
|
529
|
+
match.models = config.models.map((m: any) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
|
460
530
|
}
|
|
461
531
|
}
|
|
462
|
-
}
|
|
532
|
+
}
|
|
463
533
|
|
|
464
534
|
// Probe local providers (Ollama, LM Studio)
|
|
465
535
|
await detectLocalProviders(providers);
|
|
466
536
|
|
|
467
|
-
|
|
468
|
-
const cloudConfigured = available.filter(p => !p.local);
|
|
469
|
-
|
|
470
|
-
// Always show provider status and offer to add/change
|
|
471
|
-
ctx.ui.notify("**Provider Status:**", "info");
|
|
537
|
+
ctx.ui.notify("Provider Status:", "info");
|
|
472
538
|
for (const p of providers) {
|
|
473
|
-
const status = p.available ? "
|
|
539
|
+
const status = p.available ? "[ok]" : "[--]";
|
|
474
540
|
const tag = p.local ? " (local)" : "";
|
|
475
541
|
const modelCount = p.available ? ` — ${p.models.length} model(s)` : "";
|
|
476
542
|
ctx.ui.notify(` ${status} ${p.name}${tag}${modelCount}`, "info");
|
|
477
543
|
}
|
|
478
544
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
// Provider configuration loop — add as many providers as needed
|
|
545
|
+
// Provider configuration loop
|
|
482
546
|
let addingProviders = true;
|
|
483
547
|
while (addingProviders) {
|
|
484
548
|
const providerOptions = [
|
|
485
549
|
"Done — continue with current providers",
|
|
486
|
-
...providers.map(p => {
|
|
487
|
-
const status = p.available ? "
|
|
550
|
+
...providers.map((p) => {
|
|
551
|
+
const status = p.available ? "[ok]" : "[--]";
|
|
488
552
|
const tag = p.local ? " (local)" : "";
|
|
489
553
|
const modelCount = p.available ? ` (${p.models.length} models)` : "";
|
|
490
554
|
return `${status} ${p.name}${tag}${modelCount}`;
|
|
491
555
|
}),
|
|
492
556
|
];
|
|
493
|
-
const addProvider = await ctx.ui.select(
|
|
557
|
+
const addProvider = await ctx.ui.select(
|
|
558
|
+
"Configure a provider (add multiple!):",
|
|
559
|
+
providerOptions,
|
|
560
|
+
);
|
|
494
561
|
|
|
495
562
|
const choiceIdx = providerOptions.indexOf(addProvider ?? "");
|
|
496
|
-
if (choiceIdx <= 0) {
|
|
563
|
+
if (choiceIdx <= 0) {
|
|
497
564
|
addingProviders = false;
|
|
498
565
|
break;
|
|
499
566
|
}
|
|
500
567
|
|
|
501
568
|
const chosen = providers[choiceIdx - 1];
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
ctx.ui.notify(`\n✅ **${chosen.name}** is running with ${chosen.models.length} model(s).\n`, "info");
|
|
510
|
-
}
|
|
511
|
-
} else {
|
|
512
|
-
// Cloud provider — choose auth method
|
|
513
|
-
const supportsOAuth = ["openai", "anthropic", "google"].includes(
|
|
514
|
-
chosen.name.toLowerCase().split(" ")[0]
|
|
569
|
+
try {
|
|
570
|
+
await configureProvider(chosen, ctx);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
// Never bubble up — keep the wizard alive.
|
|
573
|
+
ctx.ui.notify(
|
|
574
|
+
`Provider configuration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
575
|
+
"error",
|
|
515
576
|
);
|
|
516
|
-
|
|
517
|
-
let authMethod = "api-key";
|
|
518
|
-
if (supportsOAuth) {
|
|
519
|
-
const authChoice = await ctx.ui.select(
|
|
520
|
-
`How to authenticate with ${chosen.name}?`,
|
|
521
|
-
["API Key (paste your key)", "OAuth (browser login via /login)"]
|
|
522
|
-
);
|
|
523
|
-
if (authChoice?.includes("OAuth")) {
|
|
524
|
-
authMethod = "oauth";
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (authMethod === "oauth") {
|
|
529
|
-
ctx.ui.notify(`\n🔐 **${chosen.name}** — Use \`/login\` after setup to authenticate via OAuth.`, "info");
|
|
530
|
-
ctx.ui.notify("OAuth opens a browser window for secure login.\n", "info");
|
|
531
|
-
// Mark as available for model assignment (auth will be done via /login)
|
|
532
|
-
chosen.available = true;
|
|
533
|
-
} else {
|
|
534
|
-
// API Key method
|
|
535
|
-
ctx.ui.notify(`\n🔑 **${chosen.name}**`, "info");
|
|
536
|
-
|
|
537
|
-
const apiKey = await ctx.ui.input(
|
|
538
|
-
`Enter your ${chosen.name} API key`,
|
|
539
|
-
"Paste your key here"
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
if (!apiKey || apiKey.trim().length < 5) {
|
|
543
|
-
ctx.ui.notify("❌ Invalid API key. Skipped.\n", "error");
|
|
544
|
-
} else {
|
|
545
|
-
// Save to models.json (merges with existing)
|
|
546
|
-
let modelsConfig: any = { providers: {} };
|
|
547
|
-
try {
|
|
548
|
-
const existing = await readFile(modelsJsonPath, "utf-8");
|
|
549
|
-
modelsConfig = JSON.parse(existing);
|
|
550
|
-
if (!modelsConfig.providers) modelsConfig.providers = {};
|
|
551
|
-
} catch { /* new file */ }
|
|
552
|
-
|
|
553
|
-
const providerId = chosen.name.toLowerCase().replace(/\s+/g, "-");
|
|
554
|
-
modelsConfig.providers[providerId] = {
|
|
555
|
-
baseUrl: chosen.baseUrl,
|
|
556
|
-
api: "openai-completions",
|
|
557
|
-
apiKey: apiKey.trim(),
|
|
558
|
-
models: await Promise.all(chosen.models.map(async (id: string) => {
|
|
559
|
-
const spec = await getModelSpec(id);
|
|
560
|
-
return {
|
|
561
|
-
id,
|
|
562
|
-
name: id,
|
|
563
|
-
reasoning: spec.reasoning,
|
|
564
|
-
input: ["text"],
|
|
565
|
-
contextWindow: spec.contextWindow,
|
|
566
|
-
maxTokens: spec.maxTokens,
|
|
567
|
-
};
|
|
568
|
-
})),
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
await writeFile(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
|
|
572
|
-
process.env[chosen.envVar] = apiKey.trim();
|
|
573
|
-
chosen.available = true;
|
|
574
|
-
|
|
575
|
-
const masked = apiKey.trim().substring(0, 6) + "..." + apiKey.trim().slice(-4);
|
|
576
|
-
ctx.ui.notify(`✅ **${chosen.name}** configured (${masked})`, "info");
|
|
577
|
-
ctx.ui.notify(` ${chosen.models.length} models added to \`models.json\`\n`, "info");
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
577
|
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Re-check available after potential additions
|
|
584
|
-
available = providers.filter(p => p.available);
|
|
578
|
+
}
|
|
585
579
|
|
|
580
|
+
const available = providers.filter((p) => p.available);
|
|
586
581
|
if (available.length === 0) {
|
|
587
|
-
ctx.ui.notify(
|
|
582
|
+
ctx.ui.notify(
|
|
583
|
+
"No providers available. Run `/phi-init` again after setting up a provider.",
|
|
584
|
+
"error",
|
|
585
|
+
);
|
|
588
586
|
return;
|
|
589
587
|
}
|
|
590
588
|
|
|
591
589
|
const allModels = getAllAvailableModels(providers);
|
|
592
|
-
ctx.ui.notify(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
590
|
+
ctx.ui.notify(
|
|
591
|
+
`\n${allModels.length} models available from ${available.length} provider(s).\n`,
|
|
592
|
+
"info",
|
|
593
|
+
);
|
|
596
594
|
|
|
595
|
+
// 2. Assign models to agents
|
|
596
|
+
ctx.ui.notify("Assign a model to each agent role:\n", "info");
|
|
597
597
|
const assignments = await manualMode(allModels, ctx);
|
|
598
598
|
|
|
599
|
-
//
|
|
600
|
-
ctx.ui.notify("
|
|
599
|
+
// 3. Persist everything
|
|
600
|
+
ctx.ui.notify("Creating directories...", "info");
|
|
601
601
|
await ensureDirs();
|
|
602
602
|
|
|
603
|
-
|
|
604
|
-
ctx.ui.notify("🔀 Writing routing configuration...", "info");
|
|
603
|
+
ctx.ui.notify("Writing routing configuration...", "info");
|
|
605
604
|
const routing = createRouting(assignments);
|
|
606
|
-
await writeFile(
|
|
605
|
+
await writeFile(
|
|
606
|
+
join(agentDir, "routing.json"),
|
|
607
|
+
JSON.stringify(routing, null, 2),
|
|
608
|
+
"utf-8",
|
|
609
|
+
);
|
|
607
610
|
|
|
608
|
-
|
|
609
|
-
ctx.ui.notify("🤖 Setting up sub-agents...", "info");
|
|
611
|
+
ctx.ui.notify("Setting up sub-agents...", "info");
|
|
610
612
|
await copyBundledAgents();
|
|
611
613
|
|
|
612
|
-
|
|
613
|
-
ctx.ui.notify("📝 Creating memory template...", "info");
|
|
614
|
+
ctx.ui.notify("Creating memory template...", "info");
|
|
614
615
|
await createAgentsTemplate();
|
|
615
616
|
|
|
616
|
-
|
|
617
|
-
ctx.ui.notify("
|
|
618
|
-
ctx.ui.notify(
|
|
619
|
-
ctx.ui.notify(
|
|
620
|
-
|
|
621
|
-
ctx.ui.notify("
|
|
622
|
-
ctx.ui.notify(` 📁 Config: ${agentDir}`, "info");
|
|
623
|
-
ctx.ui.notify(` 📁 Memory: ${memoryDir}`, "info");
|
|
624
|
-
ctx.ui.notify(` 🤖 Agents: ${agentsDir}`, "info");
|
|
625
|
-
|
|
626
|
-
ctx.ui.notify("\n**Model Assignments:**", "info");
|
|
617
|
+
ctx.ui.notify("\n Setup Complete!\n", "info");
|
|
618
|
+
ctx.ui.notify("Configuration:", "info");
|
|
619
|
+
ctx.ui.notify(` Config: ${agentDir}`, "info");
|
|
620
|
+
ctx.ui.notify(` Memory: ${memoryDir}`, "info");
|
|
621
|
+
ctx.ui.notify(` Agents: ${agentsDir}`, "info");
|
|
622
|
+
ctx.ui.notify("\nModel Assignments:", "info");
|
|
627
623
|
for (const role of TASK_ROLES) {
|
|
628
624
|
const a = assignments[role.key];
|
|
629
625
|
ctx.ui.notify(` ${role.label}: \`${a.preferred}\` (fallback: \`${a.fallback}\`)`, "info");
|
|
630
626
|
}
|
|
631
627
|
ctx.ui.notify(` Default: \`${assignments["default"].preferred}\``, "info");
|
|
632
|
-
|
|
633
|
-
ctx.ui.notify("
|
|
634
|
-
ctx.ui.notify("
|
|
635
|
-
ctx.ui.notify("
|
|
636
|
-
ctx.ui.notify("
|
|
637
|
-
ctx.ui.notify("
|
|
638
|
-
ctx.ui.notify(" • Start coding! 🚀\n", "info");
|
|
639
|
-
|
|
628
|
+
ctx.ui.notify("\nNext steps:", "info");
|
|
629
|
+
ctx.ui.notify(" - Edit `~/.phi/memory/AGENTS.md` with your project instructions", "info");
|
|
630
|
+
ctx.ui.notify(" - Run `/agents` to see available sub-agents", "info");
|
|
631
|
+
ctx.ui.notify(" - Run `/skills` to see available skills", "info");
|
|
632
|
+
ctx.ui.notify(" - Run `/models refresh` to re-fetch the model catalog", "info");
|
|
633
|
+
ctx.ui.notify(" - Start coding!\n", "info");
|
|
640
634
|
} catch (error) {
|
|
641
|
-
|
|
635
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
636
|
+
ctx.ui.notify(`Setup failed: ${message}`, "error");
|
|
642
637
|
}
|
|
643
638
|
},
|
|
644
639
|
});
|
|
645
|
-
|
|
646
640
|
}
|