@phi-code-admin/phi-code 0.75.4 → 0.75.6
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 +388 -376
- 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 +102 -40
- package/extensions/phi/smart-router.ts +63 -19
- 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,342 @@ 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
|
+
}
|
|
380
356
|
|
|
381
|
-
async function
|
|
382
|
-
|
|
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
|
+
}
|
|
383
371
|
|
|
384
|
-
|
|
372
|
+
// ─── Manual model assignment (one model per orchestration role) ─────
|
|
373
|
+
//
|
|
374
|
+
// As of 0.75.6, `/phi-init` ONLY configures orchestration role models
|
|
375
|
+
// (used by `/plan` and the orchestrator). The chat default model is
|
|
376
|
+
// owned exclusively by `/model` and persisted via the settings manager.
|
|
377
|
+
// We intentionally do NOT ask "Default model" here — that would override
|
|
378
|
+
// the user's `/model` choice on every routing decision.
|
|
379
|
+
|
|
380
|
+
async function manualMode(
|
|
381
|
+
availableModels: string[],
|
|
382
|
+
ctx: any,
|
|
383
|
+
): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
384
|
+
ctx.ui.notify(
|
|
385
|
+
"Assign a model to each orchestration role.\n" +
|
|
386
|
+
"These models are used by `/plan` and the orchestrator — NOT by normal chat.\n" +
|
|
387
|
+
"The chat default model is controlled via `/model` (and stays sticky across prompts).\n",
|
|
388
|
+
"info",
|
|
389
|
+
);
|
|
390
|
+
const modelOptions = ["default (use current chat model)", ...availableModels];
|
|
385
391
|
const assignments: Record<string, { preferred: string; fallback: string }> = {};
|
|
386
392
|
|
|
387
393
|
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";
|
|
394
|
+
const chosen = await ctx.ui.select(`${role.label} — ${role.desc}`, modelOptions);
|
|
395
|
+
const preferredModel = chosen && chosen !== modelOptions[0] ? chosen : "default";
|
|
394
396
|
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
`Fallback for ${role.label}`,
|
|
399
|
-
fallbackOptions,
|
|
400
|
-
);
|
|
401
|
-
const fallback = (fallbackChoice && fallbackChoice !== modelOptions[0]) ? fallbackChoice : "default";
|
|
397
|
+
const fallbackOptions = modelOptions.filter((m) => m !== chosen);
|
|
398
|
+
const fallbackChoice = await ctx.ui.select(`Fallback for ${role.label}`, fallbackOptions);
|
|
399
|
+
const fallback = fallbackChoice && fallbackChoice !== modelOptions[0] ? fallbackChoice : "default";
|
|
402
400
|
|
|
403
401
|
assignments[role.key] = { preferred: preferredModel, fallback };
|
|
404
|
-
ctx.ui.notify(`
|
|
402
|
+
ctx.ui.notify(` ${role.label}: ${preferredModel} (fallback: ${fallback})`, "info");
|
|
405
403
|
}
|
|
406
404
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
405
|
+
// Orchestrator fallback (used only when a specific route has no model).
|
|
406
|
+
// This is NOT the chat default — `/model` controls that.
|
|
407
|
+
assignments["default"] = {
|
|
408
|
+
preferred: "default",
|
|
409
|
+
fallback: availableModels[0] || "default",
|
|
410
|
+
};
|
|
412
411
|
return assignments;
|
|
413
412
|
}
|
|
414
413
|
|
|
415
|
-
// ───
|
|
414
|
+
// ─── Per-provider configuration step ─────────────────────────────────
|
|
415
|
+
|
|
416
|
+
async function configureProvider(provider: DetectedProvider, ctx: any): Promise<void> {
|
|
417
|
+
if (provider.local) {
|
|
418
|
+
const port = provider.id === "ollama" ? 11434 : 1234;
|
|
419
|
+
const result = await fetchLiveModels(provider.id, { forceRefresh: true, timeoutMs: 2_500 });
|
|
420
|
+
if (result.source === "live" && result.models.length > 0) {
|
|
421
|
+
provider.models = result.models.map((m) => m.id);
|
|
422
|
+
provider.available = true;
|
|
423
|
+
ctx.ui.notify(
|
|
424
|
+
`${provider.name} is running with ${provider.models.length} model(s).\n`,
|
|
425
|
+
"info",
|
|
426
|
+
);
|
|
427
|
+
} else {
|
|
428
|
+
ctx.ui.notify(
|
|
429
|
+
`${provider.name} not reachable on port ${port}. Start it and re-run \`/phi-init\`.\n`,
|
|
430
|
+
"warning",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
416
435
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
436
|
+
// Cloud provider — API key required.
|
|
437
|
+
ctx.ui.notify(
|
|
438
|
+
`\n${provider.name}\nNote: the key you type will be visible on screen. Stored in ${modelsJsonPath} (chmod 0600 on Unix).`,
|
|
439
|
+
"info",
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const apiKey = await ctx.ui.input(
|
|
443
|
+
`Enter your ${provider.name} API key`,
|
|
444
|
+
"Paste your key here",
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
if (apiKey === undefined) {
|
|
448
|
+
ctx.ui.notify("Cancelled. No key saved.", "warning");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const trimmed = apiKey.trim();
|
|
452
|
+
if (trimmed.length < 5) {
|
|
453
|
+
ctx.ui.notify("Invalid API key (too short). Skipped.\n", "error");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Persist the key FIRST. Any subsequent failure during live-fetch must
|
|
458
|
+
// not cause the user to lose what they just typed.
|
|
459
|
+
try {
|
|
460
|
+
await persistProviderKey(provider, trimmed);
|
|
461
|
+
process.env[provider.envVar] = trimmed;
|
|
462
|
+
} catch (err) {
|
|
420
463
|
ctx.ui.notify(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"separate chat/orchestration assignments, hot-reload integration). " +
|
|
424
|
-
"This legacy command still works for backwards compatibility.",
|
|
425
|
-
"info",
|
|
464
|
+
`Failed to write ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
465
|
+
"error",
|
|
426
466
|
);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
ctx.ui.notify("║ φ Phi Code Setup Wizard ║", "info");
|
|
430
|
-
ctx.ui.notify("╚══════════════════════════════════════╝\n", "info");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
431
469
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
470
|
+
// Optional ping — informational only, never fatal.
|
|
471
|
+
ctx.ui.setStatus?.("phi-init-ping", `Pinging ${provider.name}...`);
|
|
472
|
+
const ping = await pingProvider(provider.id, trimmed, 5_000).catch((err) => ({
|
|
473
|
+
ok: false,
|
|
474
|
+
error: err instanceof Error ? err.message : String(err),
|
|
475
|
+
}));
|
|
476
|
+
ctx.ui.setStatus?.("phi-init-ping", undefined);
|
|
477
|
+
if (ping.ok) {
|
|
478
|
+
ctx.ui.notify(`${provider.name} ping OK (200).`, "info");
|
|
479
|
+
} else {
|
|
480
|
+
ctx.ui.notify(
|
|
481
|
+
`${provider.name} ping failed: ${ping.error ?? "unknown"}. Key saved anyway — you can retry with \`/keys test ${provider.id}\`.`,
|
|
482
|
+
"warning",
|
|
483
|
+
);
|
|
484
|
+
}
|
|
435
485
|
|
|
436
|
-
|
|
437
|
-
|
|
486
|
+
// Fetch live model list (with fallback) and persist it.
|
|
487
|
+
ctx.ui.setStatus?.("phi-init-fetch", `Fetching ${provider.name} models...`);
|
|
488
|
+
const live = await fetchLiveModels(provider.id, {
|
|
489
|
+
apiKey: trimmed,
|
|
490
|
+
forceRefresh: true,
|
|
491
|
+
timeoutMs: 6_000,
|
|
492
|
+
});
|
|
493
|
+
ctx.ui.setStatus?.("phi-init-fetch", undefined);
|
|
494
|
+
|
|
495
|
+
const persistedModels = live.models.map(toPersistedModel);
|
|
496
|
+
try {
|
|
497
|
+
await persistProviderModels(provider, persistedModels);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
ctx.ui.notify(
|
|
500
|
+
`Failed to write provider models to ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
501
|
+
"error",
|
|
502
|
+
);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
provider.models = persistedModels.map((m) => m.id);
|
|
507
|
+
provider.available = true;
|
|
508
|
+
const masked = `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
|
|
509
|
+
ctx.ui.notify(
|
|
510
|
+
`${provider.name} configured (${masked}) — ${persistedModels.length} models (source: ${live.source}${live.error ? `, ${live.error}` : ""}).\n`,
|
|
511
|
+
"info",
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─── Command ─────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
pi.registerCommand("phi-init", {
|
|
518
|
+
description: "Initialize Phi Code (legacy alias — prefer /setup for the refined wizard)",
|
|
519
|
+
handler: async (_args, ctx) => {
|
|
520
|
+
try {
|
|
521
|
+
ctx.ui.notify(
|
|
522
|
+
"`/phi-init` configures **orchestration** roles only (Code / Debug / Plan / Explore / " +
|
|
523
|
+
"Test / Review — used by `/plan` and the orchestrator).\n\n" +
|
|
524
|
+
"The **chat default model** is owned exclusively by `/model` and stays sticky across " +
|
|
525
|
+
"prompts. This wizard will NOT change it.",
|
|
526
|
+
"info",
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
ctx.ui.notify(" Phi Code Setup Wizard (orchestration roles)", "info");
|
|
530
|
+
|
|
531
|
+
// 1. Detect providers (env vars + local servers + previously saved keys)
|
|
532
|
+
ctx.ui.notify("Detecting providers...\n", "info");
|
|
438
533
|
const providers = detectProviders();
|
|
439
534
|
|
|
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
|
-
}
|
|
535
|
+
// Merge in any previously saved providers from models.json.
|
|
536
|
+
const savedConfig = await readModelsConfig();
|
|
537
|
+
for (const [id, config] of Object.entries(savedConfig.providers)) {
|
|
538
|
+
const match = providers.find((p) => p.id === id);
|
|
539
|
+
if (!match) continue;
|
|
540
|
+
if (config?.apiKey) {
|
|
541
|
+
match.available = true;
|
|
542
|
+
if (Array.isArray(config.models) && config.models.length > 0) {
|
|
543
|
+
match.models = config.models.map((m: any) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
|
460
544
|
}
|
|
461
545
|
}
|
|
462
|
-
}
|
|
546
|
+
}
|
|
463
547
|
|
|
464
548
|
// Probe local providers (Ollama, LM Studio)
|
|
465
549
|
await detectLocalProviders(providers);
|
|
466
550
|
|
|
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");
|
|
551
|
+
ctx.ui.notify("Provider Status:", "info");
|
|
472
552
|
for (const p of providers) {
|
|
473
|
-
const status = p.available ? "
|
|
553
|
+
const status = p.available ? "[ok]" : "[--]";
|
|
474
554
|
const tag = p.local ? " (local)" : "";
|
|
475
555
|
const modelCount = p.available ? ` — ${p.models.length} model(s)` : "";
|
|
476
556
|
ctx.ui.notify(` ${status} ${p.name}${tag}${modelCount}`, "info");
|
|
477
557
|
}
|
|
478
558
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
// Provider configuration loop — add as many providers as needed
|
|
559
|
+
// Provider configuration loop
|
|
482
560
|
let addingProviders = true;
|
|
483
561
|
while (addingProviders) {
|
|
484
562
|
const providerOptions = [
|
|
485
563
|
"Done — continue with current providers",
|
|
486
|
-
...providers.map(p => {
|
|
487
|
-
const status = p.available ? "
|
|
564
|
+
...providers.map((p) => {
|
|
565
|
+
const status = p.available ? "[ok]" : "[--]";
|
|
488
566
|
const tag = p.local ? " (local)" : "";
|
|
489
567
|
const modelCount = p.available ? ` (${p.models.length} models)` : "";
|
|
490
568
|
return `${status} ${p.name}${tag}${modelCount}`;
|
|
491
569
|
}),
|
|
492
570
|
];
|
|
493
|
-
const addProvider = await ctx.ui.select(
|
|
571
|
+
const addProvider = await ctx.ui.select(
|
|
572
|
+
"Configure a provider (add multiple!):",
|
|
573
|
+
providerOptions,
|
|
574
|
+
);
|
|
494
575
|
|
|
495
576
|
const choiceIdx = providerOptions.indexOf(addProvider ?? "");
|
|
496
|
-
if (choiceIdx <= 0) {
|
|
577
|
+
if (choiceIdx <= 0) {
|
|
497
578
|
addingProviders = false;
|
|
498
579
|
break;
|
|
499
580
|
}
|
|
500
581
|
|
|
501
582
|
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]
|
|
583
|
+
try {
|
|
584
|
+
await configureProvider(chosen, ctx);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
// Never bubble up — keep the wizard alive.
|
|
587
|
+
ctx.ui.notify(
|
|
588
|
+
`Provider configuration failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
589
|
+
"error",
|
|
515
590
|
);
|
|
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
591
|
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Re-check available after potential additions
|
|
584
|
-
available = providers.filter(p => p.available);
|
|
592
|
+
}
|
|
585
593
|
|
|
594
|
+
const available = providers.filter((p) => p.available);
|
|
586
595
|
if (available.length === 0) {
|
|
587
|
-
ctx.ui.notify(
|
|
596
|
+
ctx.ui.notify(
|
|
597
|
+
"No providers available. Run `/phi-init` again after setting up a provider.",
|
|
598
|
+
"error",
|
|
599
|
+
);
|
|
588
600
|
return;
|
|
589
601
|
}
|
|
590
602
|
|
|
591
603
|
const allModels = getAllAvailableModels(providers);
|
|
592
|
-
ctx.ui.notify(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
604
|
+
ctx.ui.notify(
|
|
605
|
+
`\n${allModels.length} models available from ${available.length} provider(s).\n`,
|
|
606
|
+
"info",
|
|
607
|
+
);
|
|
596
608
|
|
|
609
|
+
// 2. Assign models to agents
|
|
610
|
+
ctx.ui.notify("Assign a model to each agent role:\n", "info");
|
|
597
611
|
const assignments = await manualMode(allModels, ctx);
|
|
598
612
|
|
|
599
|
-
//
|
|
600
|
-
ctx.ui.notify("
|
|
613
|
+
// 3. Persist everything
|
|
614
|
+
ctx.ui.notify("Creating directories...", "info");
|
|
601
615
|
await ensureDirs();
|
|
602
616
|
|
|
603
|
-
|
|
604
|
-
ctx.ui.notify("🔀 Writing routing configuration...", "info");
|
|
617
|
+
ctx.ui.notify("Writing routing configuration...", "info");
|
|
605
618
|
const routing = createRouting(assignments);
|
|
606
|
-
await writeFile(
|
|
619
|
+
await writeFile(
|
|
620
|
+
join(agentDir, "routing.json"),
|
|
621
|
+
JSON.stringify(routing, null, 2),
|
|
622
|
+
"utf-8",
|
|
623
|
+
);
|
|
607
624
|
|
|
608
|
-
|
|
609
|
-
ctx.ui.notify("🤖 Setting up sub-agents...", "info");
|
|
625
|
+
ctx.ui.notify("Setting up sub-agents...", "info");
|
|
610
626
|
await copyBundledAgents();
|
|
611
627
|
|
|
612
|
-
|
|
613
|
-
ctx.ui.notify("📝 Creating memory template...", "info");
|
|
628
|
+
ctx.ui.notify("Creating memory template...", "info");
|
|
614
629
|
await createAgentsTemplate();
|
|
615
630
|
|
|
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");
|
|
631
|
+
ctx.ui.notify("\n Setup Complete!\n", "info");
|
|
632
|
+
ctx.ui.notify("Configuration:", "info");
|
|
633
|
+
ctx.ui.notify(` Config: ${agentDir}`, "info");
|
|
634
|
+
ctx.ui.notify(` Memory: ${memoryDir}`, "info");
|
|
635
|
+
ctx.ui.notify(` Agents: ${agentsDir}`, "info");
|
|
636
|
+
ctx.ui.notify("\nOrchestration role assignments (used by `/plan`):", "info");
|
|
627
637
|
for (const role of TASK_ROLES) {
|
|
628
638
|
const a = assignments[role.key];
|
|
629
639
|
ctx.ui.notify(` ${role.label}: \`${a.preferred}\` (fallback: \`${a.fallback}\`)`, "info");
|
|
630
640
|
}
|
|
631
|
-
ctx.ui.notify(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
ctx.ui.notify("
|
|
636
|
-
ctx.ui.notify("
|
|
637
|
-
ctx.ui.notify("
|
|
638
|
-
ctx.ui.notify("
|
|
639
|
-
|
|
641
|
+
ctx.ui.notify(
|
|
642
|
+
"\nChat default model: use `/model` (this wizard does NOT change the chat default).",
|
|
643
|
+
"info",
|
|
644
|
+
);
|
|
645
|
+
ctx.ui.notify("\nNext steps:", "info");
|
|
646
|
+
ctx.ui.notify(" - `/model` to pick the chat default model (sticky across prompts)", "info");
|
|
647
|
+
ctx.ui.notify(" - `/plan <description>` to run the orchestrator with the roles above", "info");
|
|
648
|
+
ctx.ui.notify(" - `/routing` to inspect the route table (auto-switch is OFF by default)", "info");
|
|
649
|
+
ctx.ui.notify(" - `/models refresh` to re-fetch the live model catalog", "info");
|
|
650
|
+
ctx.ui.notify(" - Edit `~/.phi/memory/AGENTS.md` with your project instructions", "info");
|
|
651
|
+
ctx.ui.notify(" - Start coding!\n", "info");
|
|
640
652
|
} catch (error) {
|
|
641
|
-
|
|
653
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
654
|
+
ctx.ui.notify(`Setup failed: ${message}`, "error");
|
|
642
655
|
}
|
|
643
656
|
},
|
|
644
657
|
});
|
|
645
|
-
|
|
646
658
|
}
|