@phi-code-admin/phi-code 0.84.2 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +95 -126
- package/agents/code.md +1 -0
- package/agents/explore.md +11 -5
- package/agents/plan.md +18 -3
- package/agents/review.md +18 -7
- package/agents/test.md +1 -0
- package/config/routing.example.json +129 -0
- package/config/routing.schema.json +58 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -6
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +32 -30
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +9 -8
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +4 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/dist/utils/version-check.d.ts +5 -5
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +13 -8
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +11 -11
- package/docs/custom-provider.md +4 -4
- package/docs/development.md +2 -2
- package/docs/extensions.md +47 -47
- package/docs/fork-policy.md +63 -0
- package/docs/index.md +7 -7
- package/docs/json.md +3 -3
- package/docs/keybindings.md +5 -5
- package/docs/models.md +6 -6
- package/docs/packages.md +37 -37
- package/docs/prompt-templates.md +4 -4
- package/docs/providers.md +9 -9
- package/docs/quickstart.md +25 -25
- package/docs/rpc.md +3 -3
- package/docs/sdk.md +34 -34
- package/docs/session-format.md +4 -4
- package/docs/sessions.md +11 -11
- package/docs/settings.md +11 -11
- package/docs/shell-aliases.md +2 -2
- package/docs/skills.md +9 -9
- package/docs/terminal-setup.md +7 -7
- package/docs/termux.md +6 -6
- package/docs/themes.md +9 -9
- package/docs/tmux.md +3 -3
- package/docs/tui.md +8 -8
- package/docs/usage.md +40 -40
- package/docs/windows.md +2 -2
- package/examples/sdk/12-full-control.ts +1 -1
- package/extensions/phi/agents.ts +5 -113
- package/extensions/phi/models.ts +170 -49
- package/extensions/phi/orchestrator.ts +44 -37
- package/extensions/phi/providers/agent-def.ts +128 -0
- package/extensions/phi/providers/live-models.ts +31 -1
- package/extensions/phi/providers/opencode-go.ts +7 -3
- package/extensions/phi/setup.ts +9 -0
- package/extensions/phi/skill-loader.ts +46 -25
- package/package.json +2 -1
package/extensions/phi/models.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { ApiKeyStore, type ConfigWatcher, type ExtensionAPI, getApiKeyStore, getConfigWatcher } from "phi-code";
|
|
19
|
+
import { getModels } from "phi-code-ai";
|
|
19
20
|
import {
|
|
20
21
|
buildOpenCodeGoAnthropicProviderConfig,
|
|
21
22
|
buildOpenCodeGoProviderConfig,
|
|
@@ -25,7 +26,9 @@ import { formatWindow, inferContextWindow, parseContextWindow } from "./provider
|
|
|
25
26
|
import { fetchLiveModels, peekCache, resetLiveModelsCache, toPersistedModel } from "./providers/live-models.js";
|
|
26
27
|
|
|
27
28
|
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
29
|
+
opencode: "OpenCode Zen",
|
|
28
30
|
"opencode-go": "OpenCode Go",
|
|
31
|
+
"opencode-go-anthropic": "OpenCode Go (Anthropic-compat)",
|
|
29
32
|
"alibaba-codingplan": "Alibaba Coding Plan (OpenAI-compat)",
|
|
30
33
|
"alibaba-codingplan-anthropic": "Alibaba Coding Plan (Anthropic-compat)",
|
|
31
34
|
openai: "OpenAI",
|
|
@@ -37,10 +40,62 @@ const PROVIDER_DISPLAY: Record<string, string> = {
|
|
|
37
40
|
"lm-studio": "LM Studio (local)",
|
|
38
41
|
};
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Providers the live-models dispatcher can actually re-fetch (see
|
|
45
|
+
* live-models.ts dispatchFetch + refreshOpenCodeGo). Used to extend the
|
|
46
|
+
* startup/manual refresh to providers that are authenticated via auth.json or
|
|
47
|
+
* env vars but have no models.json entry yet — without it, a user who set a
|
|
48
|
+
* key with /auth (and never ran /setup) would never see new upstream models.
|
|
49
|
+
*/
|
|
50
|
+
const REFRESHABLE_PROVIDERS: ReadonlySet<string> = new Set([
|
|
51
|
+
"opencode",
|
|
52
|
+
"opencode-go",
|
|
53
|
+
"opencode-go-anthropic",
|
|
54
|
+
"alibaba-codingplan",
|
|
55
|
+
"alibaba-codingplan-anthropic",
|
|
56
|
+
"openai",
|
|
57
|
+
"anthropic",
|
|
58
|
+
"google",
|
|
59
|
+
"openrouter",
|
|
60
|
+
"groq",
|
|
61
|
+
"ollama",
|
|
62
|
+
"lm-studio",
|
|
63
|
+
]);
|
|
64
|
+
|
|
40
65
|
function displayName(id: string): string {
|
|
41
66
|
return PROVIDER_DISPLAY[id] ?? id;
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
/** Default discovery base URLs for providers whose models.json entry is created by a refresh. */
|
|
70
|
+
const DEFAULT_BASE_URLS: Record<string, string> = {
|
|
71
|
+
opencode: "https://opencode.ai/zen/v1",
|
|
72
|
+
"opencode-go": "https://opencode.ai/zen/go/v1",
|
|
73
|
+
"alibaba-codingplan": "https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
74
|
+
"alibaba-codingplan-anthropic": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
|
|
75
|
+
openai: "https://api.openai.com/v1",
|
|
76
|
+
anthropic: "https://api.anthropic.com/v1",
|
|
77
|
+
google: "https://generativelanguage.googleapis.com/v1beta",
|
|
78
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
79
|
+
groq: "https://api.groq.com/openai/v1",
|
|
80
|
+
ollama: "http://localhost:11434/v1",
|
|
81
|
+
"lm-studio": "http://localhost:1234/v1",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Built-in (models.generated.ts) model ids for a provider. Persisting only the
|
|
86
|
+
* models NOT in this set keeps the rich built-in definitions (costs, image
|
|
87
|
+
* input, thinking-level maps) authoritative — models.json carries just the
|
|
88
|
+
* delta the static catalog does not know about yet.
|
|
89
|
+
*/
|
|
90
|
+
function builtinModelIds(providerId: string): Set<string> {
|
|
91
|
+
try {
|
|
92
|
+
const models = getModels(providerId as Parameters<typeof getModels>[0]) as Array<{ id: string }>;
|
|
93
|
+
return new Set(models.map((m) => m.id));
|
|
94
|
+
} catch {
|
|
95
|
+
return new Set();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
44
99
|
interface RefreshOutcome {
|
|
45
100
|
provider: string;
|
|
46
101
|
source: "live" | "cache" | "fallback" | "unsupported" | "skipped";
|
|
@@ -68,7 +123,21 @@ async function refreshOpenCodeGo(
|
|
|
68
123
|
? buildOpenCodeGoAnthropicProviderConfig(keyForBuild, models)
|
|
69
124
|
: buildOpenCodeGoProviderConfig(keyForBuild, models);
|
|
70
125
|
|
|
71
|
-
|
|
126
|
+
// Persist only models the built-in catalog does not know yet; built-ins stay
|
|
127
|
+
// authoritative (costs, image input) and models.json carries the delta.
|
|
128
|
+
const builtin = builtinModelIds(providerId);
|
|
129
|
+
const newModels = config.models.filter((m) => !builtin.has(m.id));
|
|
130
|
+
|
|
131
|
+
if (newModels.length === 0) {
|
|
132
|
+
if (stored && Array.isArray(stored.models) && stored.models.length > 0) {
|
|
133
|
+
// Clean up previously persisted models that have since become built-in.
|
|
134
|
+
watcher.muteForWrite("models_json_changed");
|
|
135
|
+
store.setKey(providerId, stored.apiKey ?? apiKey ?? "local", {
|
|
136
|
+
baseUrl: stored.baseUrl ?? config.baseUrl,
|
|
137
|
+
api: stored.api ?? config.api,
|
|
138
|
+
models: [],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
72
141
|
return { provider: providerId, source: source === "fallback" ? "fallback" : "skipped", count: 0 };
|
|
73
142
|
}
|
|
74
143
|
|
|
@@ -76,22 +145,26 @@ async function refreshOpenCodeGo(
|
|
|
76
145
|
store.setKey(providerId, stored?.apiKey ?? apiKey ?? "local", {
|
|
77
146
|
baseUrl: stored?.baseUrl ?? config.baseUrl,
|
|
78
147
|
api: stored?.api ?? config.api,
|
|
79
|
-
models:
|
|
148
|
+
models: newModels,
|
|
80
149
|
});
|
|
81
150
|
|
|
82
151
|
const outcomeSource = source === "live" ? "live" : source === "cache" ? "cache" : "fallback";
|
|
83
|
-
return { provider: providerId, source: outcomeSource, count:
|
|
152
|
+
return { provider: providerId, source: outcomeSource, count: newModels.length };
|
|
84
153
|
}
|
|
85
154
|
|
|
86
155
|
async function refreshOne(
|
|
87
156
|
store: ApiKeyStore,
|
|
88
157
|
watcher: ConfigWatcher,
|
|
89
158
|
providerId: string,
|
|
159
|
+
resolvedApiKey?: string,
|
|
90
160
|
): Promise<RefreshOutcome> {
|
|
91
161
|
const stored = store.getProvider(providerId);
|
|
92
|
-
const
|
|
162
|
+
const storedKey = stored?.apiKey && !stored.apiKey.startsWith("$") && stored.apiKey !== "local"
|
|
93
163
|
? stored.apiKey
|
|
94
164
|
: undefined;
|
|
165
|
+
// Prefer the key stored in models.json, else the one resolved from
|
|
166
|
+
// auth.json/env by the model registry (providers set up via /auth only).
|
|
167
|
+
const apiKey = storedKey ?? resolvedApiKey;
|
|
95
168
|
|
|
96
169
|
// OpenCode Go is a provider pair the generic fetchLiveModels path can't express
|
|
97
170
|
// (and never handled the Anthropic side), so refresh it from the shared catalog.
|
|
@@ -110,40 +183,30 @@ async function refreshOne(
|
|
|
110
183
|
return { provider: providerId, source: "skipped", count: 0, error: result.error };
|
|
111
184
|
}
|
|
112
185
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
186
|
+
// Persist only the delta the built-in catalog does not know yet (see
|
|
187
|
+
// builtinModelIds). Built-in definitions keep their costs/capabilities.
|
|
188
|
+
const builtin = builtinModelIds(providerId);
|
|
189
|
+
const persisted = result.models.map(toPersistedModel).filter((m) => !builtin.has(m.id));
|
|
117
190
|
|
|
118
191
|
// Preserve baseUrl/api/apiKey/headers from existing config; only models change.
|
|
119
|
-
const baseUrl =
|
|
120
|
-
stored?.baseUrl ??
|
|
121
|
-
(providerId === "opencode-go"
|
|
122
|
-
? "https://opencode.ai/zen/go/v1"
|
|
123
|
-
: providerId === "alibaba-codingplan"
|
|
124
|
-
? "https://coding-intl.dashscope.aliyuncs.com/v1"
|
|
125
|
-
: providerId === "alibaba-codingplan-anthropic"
|
|
126
|
-
? "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
|
127
|
-
: providerId === "openai"
|
|
128
|
-
? "https://api.openai.com/v1"
|
|
129
|
-
: providerId === "anthropic"
|
|
130
|
-
? "https://api.anthropic.com/v1"
|
|
131
|
-
: providerId === "google"
|
|
132
|
-
? "https://generativelanguage.googleapis.com/v1beta"
|
|
133
|
-
: providerId === "openrouter"
|
|
134
|
-
? "https://openrouter.ai/api/v1"
|
|
135
|
-
: providerId === "groq"
|
|
136
|
-
? "https://api.groq.com/openai/v1"
|
|
137
|
-
: providerId === "ollama"
|
|
138
|
-
? "http://localhost:11434/v1"
|
|
139
|
-
: providerId === "lm-studio"
|
|
140
|
-
? "http://localhost:1234/v1"
|
|
141
|
-
: undefined);
|
|
142
|
-
|
|
192
|
+
const baseUrl = stored?.baseUrl ?? DEFAULT_BASE_URLS[providerId];
|
|
143
193
|
if (!baseUrl) {
|
|
144
194
|
return { provider: providerId, source: "skipped", count: 0, error: "unknown baseUrl" };
|
|
145
195
|
}
|
|
146
196
|
|
|
197
|
+
if (persisted.length === 0) {
|
|
198
|
+
if (stored && Array.isArray(stored.models) && stored.models.length > 0) {
|
|
199
|
+
// Clean up previously persisted models that have since become built-in.
|
|
200
|
+
watcher.muteForWrite("models_json_changed");
|
|
201
|
+
store.setKey(providerId, stored.apiKey ?? "local", {
|
|
202
|
+
baseUrl,
|
|
203
|
+
api: stored.api,
|
|
204
|
+
models: [],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return { provider: providerId, source: result.source, count: 0, error: result.error };
|
|
208
|
+
}
|
|
209
|
+
|
|
147
210
|
// Mute the config watcher so it does not echo this programmatic write back
|
|
148
211
|
// as a models_json_changed event (which would trigger a spurious reload +
|
|
149
212
|
// "Keys reloaded" notification). Mute per-write because refresh loops can
|
|
@@ -311,29 +374,73 @@ export default function modelsExtension(pi: ExtensionAPI) {
|
|
|
311
374
|
ctx.ui.notify(out, "info");
|
|
312
375
|
}
|
|
313
376
|
|
|
377
|
+
interface RefreshTarget {
|
|
378
|
+
id: string;
|
|
379
|
+
resolvedApiKey?: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Providers to refresh: every provider persisted in models.json, plus every
|
|
384
|
+
* refreshable provider that is authenticated (auth.json / env vars) but has
|
|
385
|
+
* no models.json entry yet. API keys are resolved through the registry so
|
|
386
|
+
* providers configured via /auth alone still get authenticated listings.
|
|
387
|
+
*/
|
|
388
|
+
async function resolveRefreshTargets(registry: {
|
|
389
|
+
getAvailable(): Array<{ provider: string }>;
|
|
390
|
+
getApiKeyForProvider(provider: string): Promise<string | undefined>;
|
|
391
|
+
}): Promise<RefreshTarget[]> {
|
|
392
|
+
const targets = new Map<string, RefreshTarget>();
|
|
393
|
+
for (const id of store.listProviders()) {
|
|
394
|
+
targets.set(id, { id });
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
for (const model of registry.getAvailable()) {
|
|
398
|
+
const id = model.provider;
|
|
399
|
+
if (!targets.has(id) && REFRESHABLE_PROVIDERS.has(id)) {
|
|
400
|
+
targets.set(id, { id });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
// registry unavailable — fall back to models.json providers only
|
|
405
|
+
}
|
|
406
|
+
for (const target of targets.values()) {
|
|
407
|
+
try {
|
|
408
|
+
target.resolvedApiKey = await registry.getApiKeyForProvider(target.id);
|
|
409
|
+
} catch {
|
|
410
|
+
// no resolvable key — refreshOne will try the stored/keyless path
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return [...targets.values()];
|
|
414
|
+
}
|
|
415
|
+
|
|
314
416
|
// Background refresh on session_start so every new Phi Code session reflects
|
|
315
417
|
// the latest provider catalogs without the user typing `/models refresh`.
|
|
316
418
|
// Failures are silent — startup must never be blocked by upstream API hiccups.
|
|
317
419
|
pi.on("session_start", async (_event, ctx) => {
|
|
420
|
+
if (process.env.PI_OFFLINE) return;
|
|
318
421
|
try {
|
|
319
422
|
store.load();
|
|
320
423
|
} catch {
|
|
321
424
|
// no models.json yet
|
|
322
425
|
}
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
426
|
+
const targets = await resolveRefreshTargets(ctx.modelRegistry);
|
|
427
|
+
if (targets.length === 0) return;
|
|
325
428
|
|
|
326
429
|
// Fire-and-forget. Hot-reload via models_json_changed event surfaces results.
|
|
327
430
|
void (async () => {
|
|
328
|
-
let
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
431
|
+
let discovered = 0;
|
|
432
|
+
let changedProviders = 0;
|
|
433
|
+
for (const target of targets) {
|
|
434
|
+
const outcome = await refreshOne(store, watcher, target.id, target.resolvedApiKey).catch(() => undefined);
|
|
435
|
+
if (outcome && outcome.source === "live" && outcome.count > 0) {
|
|
436
|
+
changedProviders++;
|
|
437
|
+
discovered += outcome.count;
|
|
438
|
+
}
|
|
332
439
|
}
|
|
333
|
-
if (
|
|
440
|
+
if (changedProviders > 0) {
|
|
334
441
|
try {
|
|
335
442
|
ctx.ui.notify(
|
|
336
|
-
`
|
|
443
|
+
`Discovered ${discovered} new model(s) across ${changedProviders} provider(s). See /model.`,
|
|
337
444
|
"info",
|
|
338
445
|
);
|
|
339
446
|
} catch {
|
|
@@ -346,20 +453,33 @@ export default function modelsExtension(pi: ExtensionAPI) {
|
|
|
346
453
|
|
|
347
454
|
async function refreshCommand(
|
|
348
455
|
target: string | undefined,
|
|
349
|
-
ctx: {
|
|
456
|
+
ctx: {
|
|
457
|
+
ui: { notify: (m: string, t?: "info" | "warning" | "error") => void; setStatus?: (k: string, v?: string) => void };
|
|
458
|
+
modelRegistry: {
|
|
459
|
+
getAvailable(): Array<{ provider: string }>;
|
|
460
|
+
getApiKeyForProvider(provider: string): Promise<string | undefined>;
|
|
461
|
+
};
|
|
462
|
+
},
|
|
350
463
|
): Promise<void> {
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
464
|
+
const targets = target ? [{ id: target } as RefreshTarget] : await resolveRefreshTargets(ctx.modelRegistry);
|
|
465
|
+
if (target) {
|
|
466
|
+
try {
|
|
467
|
+
targets[0].resolvedApiKey = await ctx.modelRegistry.getApiKeyForProvider(target);
|
|
468
|
+
} catch {
|
|
469
|
+
// keep undefined
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (targets.length === 0) {
|
|
353
473
|
ctx.ui.notify("No providers configured.", "warning");
|
|
354
474
|
return;
|
|
355
475
|
}
|
|
356
|
-
ctx.ui.notify(`Refreshing ${
|
|
476
|
+
ctx.ui.notify(`Refreshing ${targets.length} provider(s)...`, "info");
|
|
357
477
|
ctx.ui.setStatus?.("models-refresh", "Fetching live model catalogs...");
|
|
358
478
|
|
|
359
479
|
const outcomes: RefreshOutcome[] = [];
|
|
360
|
-
for (const
|
|
361
|
-
const outcome = await refreshOne(store, watcher, id).catch((err) => ({
|
|
362
|
-
provider: id,
|
|
480
|
+
for (const t of targets) {
|
|
481
|
+
const outcome = await refreshOne(store, watcher, t.id, t.resolvedApiKey).catch((err) => ({
|
|
482
|
+
provider: t.id,
|
|
363
483
|
source: "skipped" as const,
|
|
364
484
|
count: 0,
|
|
365
485
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -371,9 +491,10 @@ export default function modelsExtension(pi: ExtensionAPI) {
|
|
|
371
491
|
let out = "**Refresh report:**\n";
|
|
372
492
|
for (const o of outcomes) {
|
|
373
493
|
const icon = o.source === "live" ? "[ok]" : o.source === "fallback" ? "[fb]" : o.source === "cache" ? "[c]" : "[--]";
|
|
374
|
-
out += ` ${icon} ${displayName(o.provider)} \`${o.provider}\` — ${o.count} model(s) (${o.source}${o.error ? `, ${o.error}` : ""})\n`;
|
|
494
|
+
out += ` ${icon} ${displayName(o.provider)} \`${o.provider}\` — ${o.count} new model(s) (${o.source}${o.error ? `, ${o.error}` : ""})\n`;
|
|
375
495
|
}
|
|
376
|
-
out += `\
|
|
496
|
+
out += `\nOnly models missing from the built-in catalog are persisted to \`${store.configPath}\`;\n`;
|
|
497
|
+
out += `built-in models stay available either way. \`/model\` picker reflects the merged catalog.`;
|
|
377
498
|
ctx.ui.notify(out, "info");
|
|
378
499
|
pi.events.emit("models_json_changed", { source: "models-refresh" });
|
|
379
500
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
parsePhaseVerdict,
|
|
30
30
|
} from "./providers/orchestrator-helpers.js";
|
|
31
31
|
import { defaultExplorerSpecs, READONLY_EXPLORER_TOOLS, runExploreFanout } from "./providers/explore-fanout.js";
|
|
32
|
+
import { type AgentDef, loadAgentDef } from "./providers/agent-def.js";
|
|
32
33
|
|
|
33
34
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
34
35
|
|
|
@@ -212,12 +213,6 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
212
213
|
|
|
213
214
|
// ─── Orchestration State ─────────────────────────────────────────
|
|
214
215
|
|
|
215
|
-
interface AgentDef {
|
|
216
|
-
name: string;
|
|
217
|
-
tools: string[];
|
|
218
|
-
systemPrompt: string;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
216
|
interface OrchestratorPhase {
|
|
222
217
|
key: string;
|
|
223
218
|
label: string;
|
|
@@ -331,30 +326,25 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
331
326
|
- **Internal orchestration data:** notes injected as "Previous phase summary" or budget/handoff reminders are for YOUR use only. Do not repeat them back to the user.`;
|
|
332
327
|
|
|
333
328
|
/**
|
|
334
|
-
*
|
|
329
|
+
* Load a skill body (SKILL.md) so a phase instruction can embed it verbatim.
|
|
330
|
+
* Search order mirrors agent-def.ts: project .phi/skills, then the global
|
|
331
|
+
* ~/.phi/agent/skills (postinstall copies bundled skills there), then the
|
|
332
|
+
* bundled <package>/skills for the repo layout. YAML frontmatter is
|
|
333
|
+
* stripped. Returns null when the skill is not installed.
|
|
335
334
|
*/
|
|
336
|
-
function
|
|
335
|
+
function loadSkillContent(name: string): string | null {
|
|
337
336
|
const dirs = [
|
|
338
|
-
join(process.cwd(), ".phi", "
|
|
339
|
-
join(homedir(), ".phi", "agent", "
|
|
337
|
+
join(process.cwd(), ".phi", "skills"),
|
|
338
|
+
join(homedir(), ".phi", "agent", "skills"),
|
|
339
|
+
join(__dirname, "..", "..", "skills"),
|
|
340
340
|
];
|
|
341
341
|
for (const dir of dirs) {
|
|
342
|
-
const filePath = join(dir, `${name}.md`);
|
|
343
342
|
try {
|
|
344
|
-
const content = readFileSync(
|
|
345
|
-
const fmMatch = content.match(/^---\s*\n
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
350
|
-
if (m) fields[m[1]] = m[2].trim();
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
name: fields.name || name,
|
|
354
|
-
tools: (fields.tools || "").split(",").map(t => t.trim()).filter(Boolean),
|
|
355
|
-
systemPrompt: fmMatch[2].trim(),
|
|
356
|
-
};
|
|
357
|
-
} catch { continue; }
|
|
343
|
+
const content = readFileSync(join(dir, name, "SKILL.md"), "utf-8");
|
|
344
|
+
const fmMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
|
|
345
|
+
const body = (fmMatch ? fmMatch[1] : content).trim();
|
|
346
|
+
if (body) return body;
|
|
347
|
+
} catch { /* try next dir */ }
|
|
358
348
|
}
|
|
359
349
|
return null;
|
|
360
350
|
}
|
|
@@ -392,6 +382,15 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
392
382
|
: '';
|
|
393
383
|
const runtimeInfo = `\n\nRuntime: ${process.platform} (${process.arch})${shellNote}`;
|
|
394
384
|
|
|
385
|
+
// Embed the prompt-architect skill so the PLAN phase applies it to every
|
|
386
|
+
// task it writes. The skill ships bundled and is copied to
|
|
387
|
+
// ~/.phi/agent/skills by postinstall; when missing, the inline task format
|
|
388
|
+
// still enforces the [CONTEXT]/[TASK]/[FORMAT]/[CONSTRAINTS] structure.
|
|
389
|
+
const promptArchitectSkill = loadSkillContent("prompt-architect");
|
|
390
|
+
const promptArchitectSection = promptArchitectSkill
|
|
391
|
+
? `\n**Prompt-architect skill (apply it to every task you write):**\n\n${promptArchitectSkill}\n`
|
|
392
|
+
: "";
|
|
393
|
+
|
|
395
394
|
const phases: OrchestratorPhase[] = [
|
|
396
395
|
{
|
|
397
396
|
key: "explore", label: "🔍 Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
|
|
@@ -466,29 +465,37 @@ After your analysis, use \`ontology_add\` to save key project entities AND their
|
|
|
466
465
|
**Step 1:** Read \`.phi/plans/brief-*.md\` (created by the explore phase)
|
|
467
466
|
**Step 2:** Read \`.phi/plans/explore-*.md\` to understand the codebase analysis
|
|
468
467
|
**Step 3:** Design the architecture based on findings
|
|
469
|
-
**Step 4:** Create a DETAILED TODO LIST in \`.phi/plans/todo-${ts}.md
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
-
|
|
473
|
-
-
|
|
474
|
-
-
|
|
475
|
-
-
|
|
468
|
+
**Step 4:** Create a DETAILED TODO LIST in \`.phi/plans/todo-${ts}.md\`.
|
|
469
|
+
Every task is a PROMPT for the CODE agent, which starts with ZERO memory of
|
|
470
|
+
this conversation. Write each task with the prompt-architect structure:
|
|
471
|
+
- **[CONTEXT]** what exists and why this task (real paths from the explore phase)
|
|
472
|
+
- **[TASK]** exactly what to do — files to create/modify with full paths
|
|
473
|
+
- **[FORMAT]** expected deliverable — exports, signatures, style, tests
|
|
474
|
+
- **[CONSTRAINTS]** what NOT to break, patterns to respect
|
|
475
|
+
Plus: task number + title, agent assignment (code/test), dependencies.
|
|
476
476
|
|
|
477
477
|
**Format for the todo list:**
|
|
478
478
|
\`\`\`markdown
|
|
479
479
|
# TODO: Project Tasks
|
|
480
480
|
|
|
481
481
|
## Task 1: [Task Title] [agent-type]
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
- [ ]
|
|
482
|
+
**[CONTEXT]** [What exists now, why this task, real paths (src/x.ts:42)]
|
|
483
|
+
**[TASK]**
|
|
484
|
+
- [ ] Create path/to/file.ext with [specific behavior]
|
|
485
|
+
- [ ] Modify path/to/other.ext: [exact change]
|
|
486
|
+
**[FORMAT]** [Expected exports/signatures/tests]
|
|
487
|
+
**[CONSTRAINTS]** [What not to break, patterns to follow]
|
|
485
488
|
- Dependencies: None
|
|
486
489
|
|
|
487
490
|
## Task 2: [Task Title] [agent-type]
|
|
488
|
-
|
|
491
|
+
**[CONTEXT]** ...
|
|
492
|
+
**[TASK]**
|
|
493
|
+
- [ ] ...
|
|
494
|
+
**[FORMAT]** ...
|
|
495
|
+
**[CONSTRAINTS]** ...
|
|
489
496
|
- Dependencies: Task 1
|
|
490
497
|
\`\`\`
|
|
491
|
-
|
|
498
|
+
${promptArchitectSection}
|
|
492
499
|
Before finishing, use \`memory_write\` to save your plan summary with relevant tags for future reference.` + runtimeInfo + COMMON_PHASE_RULES,
|
|
493
500
|
},
|
|
494
501
|
{
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent definitions (.md with YAML frontmatter) — single source of truth for
|
|
3
|
+
* parsing and discovery, shared by the /plan orchestrator (loadAgentDef) and
|
|
4
|
+
* the /agents command (discoverAgents). Before this module existed the two
|
|
5
|
+
* call sites each had their own parser and they had already drifted.
|
|
6
|
+
*
|
|
7
|
+
* Search order (first match wins):
|
|
8
|
+
* 1. <cwd>/.phi/agents/ (project)
|
|
9
|
+
* 2. ~/.phi/agent/agents/ (global — postinstall copies bundled here)
|
|
10
|
+
* 3. <package>/agents/ (bundled, repo layout)
|
|
11
|
+
*
|
|
12
|
+
* Layout note: this file lives in extensions/phi/providers/. Three hops up is
|
|
13
|
+
* the package root in the repo layout (packages/coding-agent) AND the agent
|
|
14
|
+
* dir in the installed layout (~/.phi/agent), so the "bundled" candidate
|
|
15
|
+
* resolves to a real agents/ directory in both.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { basename, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
export type AgentSource = "project" | "global" | "bundled";
|
|
23
|
+
|
|
24
|
+
export interface AgentDef {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
tools: string[];
|
|
28
|
+
model: string;
|
|
29
|
+
systemPrompt: string;
|
|
30
|
+
filePath: string;
|
|
31
|
+
source: AgentSource;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse an agent .md file: YAML frontmatter (name, description, tools, model)
|
|
36
|
+
* followed by the system prompt body. `tools` is a comma-separated list.
|
|
37
|
+
* Returns null when the file has no frontmatter block.
|
|
38
|
+
*/
|
|
39
|
+
export function parseAgentMarkdown(content: string, filePath: string, source: AgentSource): AgentDef | null {
|
|
40
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
41
|
+
if (!fmMatch) return null;
|
|
42
|
+
|
|
43
|
+
const fields: Record<string, string> = {};
|
|
44
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
45
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
46
|
+
if (m) fields[m[1]] = m[2].trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const name = fields.name || basename(filePath).replace(/\.md$/, "");
|
|
50
|
+
if (!name) return null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
description: fields.description || "No description",
|
|
55
|
+
tools: (fields.tools || "")
|
|
56
|
+
.split(",")
|
|
57
|
+
.map((t) => t.trim())
|
|
58
|
+
.filter(Boolean),
|
|
59
|
+
model: fields.model || "default",
|
|
60
|
+
systemPrompt: fmMatch[2].trim(),
|
|
61
|
+
filePath,
|
|
62
|
+
source,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readAgentFile(filePath: string, source: AgentSource): AgentDef | null {
|
|
67
|
+
try {
|
|
68
|
+
return parseAgentMarkdown(readFileSync(filePath, "utf-8"), filePath, source);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Agent directories in precedence order (project > global > bundled). */
|
|
75
|
+
export function agentSearchDirs(cwd: string = process.cwd()): Array<{ dir: string; source: AgentSource }> {
|
|
76
|
+
return [
|
|
77
|
+
{ dir: join(cwd, ".phi", "agents"), source: "project" },
|
|
78
|
+
{ dir: join(homedir(), ".phi", "agent", "agents"), source: "global" },
|
|
79
|
+
{ dir: join(__dirname, "..", "..", "..", "agents"), source: "bundled" },
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load a single agent definition by name. Used by the /plan orchestrator to
|
|
85
|
+
* activate a phase persona.
|
|
86
|
+
*/
|
|
87
|
+
export function loadAgentDef(name: string, cwd: string = process.cwd()): AgentDef | null {
|
|
88
|
+
for (const { dir, source } of agentSearchDirs(cwd)) {
|
|
89
|
+
const def = readAgentFile(join(dir, `${name}.md`), source);
|
|
90
|
+
if (def) return def;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Discover every agent definition across all sources, first match per name
|
|
97
|
+
* wins. Used by the /agents command.
|
|
98
|
+
*/
|
|
99
|
+
export function discoverAgents(cwd: string = process.cwd()): AgentDef[] {
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
const agents: AgentDef[] = [];
|
|
102
|
+
|
|
103
|
+
for (const { dir, source } of agentSearchDirs(cwd)) {
|
|
104
|
+
if (!existsSync(dir)) continue;
|
|
105
|
+
let entries: string[];
|
|
106
|
+
try {
|
|
107
|
+
entries = readdirSync(dir);
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (!entry.endsWith(".md")) continue;
|
|
113
|
+
const filePath = join(dir, entry);
|
|
114
|
+
try {
|
|
115
|
+
if (!statSync(filePath).isFile()) continue;
|
|
116
|
+
} catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const def = readAgentFile(filePath, source);
|
|
120
|
+
if (def && !seen.has(def.name)) {
|
|
121
|
+
seen.add(def.name);
|
|
122
|
+
agents.push(def);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return agents;
|
|
128
|
+
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { ALIBABA_MODELS, ALIBABA_PROVIDERS, pingAlibaba } from "./alibaba.js";
|
|
28
28
|
import { inferContextWindow } from "./context-window.js";
|
|
29
29
|
|
|
30
|
-
export const LAST_VERIFIED = "2026-
|
|
30
|
+
export const LAST_VERIFIED = "2026-07-10";
|
|
31
31
|
|
|
32
32
|
export interface LiveModel {
|
|
33
33
|
id: string;
|
|
@@ -203,8 +203,27 @@ const STATIC_OPENROUTER: LiveModel[] = [
|
|
|
203
203
|
{ id: "minimax/MiniMax-M2.7", name: "MiniMax M2.7", reasoning: true },
|
|
204
204
|
];
|
|
205
205
|
|
|
206
|
+
// OpenCode Zen (https://opencode.ai/zen) — the non-Go catalog. The models
|
|
207
|
+
// endpoint is reachable without a key (like OpenRouter); inference needs one.
|
|
208
|
+
const STATIC_OPENCODE_ZEN: LiveModel[] = [
|
|
209
|
+
{ id: "claude-fable-5", name: "Claude Fable 5", contextWindow: 200_000, reasoning: true },
|
|
210
|
+
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", contextWindow: 200_000, reasoning: true },
|
|
211
|
+
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", contextWindow: 200_000, reasoning: true },
|
|
212
|
+
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5", contextWindow: 200_000, reasoning: true },
|
|
213
|
+
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", contextWindow: 1_000_000, reasoning: true },
|
|
214
|
+
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", contextWindow: 1_000_000, reasoning: true },
|
|
215
|
+
{ id: "gpt-5.5", name: "GPT-5.5", contextWindow: 400_000, reasoning: true },
|
|
216
|
+
{ id: "gpt-5.4", name: "GPT-5.4", contextWindow: 400_000, reasoning: true },
|
|
217
|
+
{ id: "glm-5.2", name: "GLM 5.2", contextWindow: 200_000, reasoning: true },
|
|
218
|
+
{ id: "glm-5.1", name: "GLM 5.1", contextWindow: 200_000, reasoning: true },
|
|
219
|
+
{ id: "kimi-k2.6", name: "Kimi K2.6", contextWindow: 256_000, reasoning: true },
|
|
220
|
+
{ id: "minimax-m2.7", name: "MiniMax M2.7", contextWindow: 1_000_000, reasoning: true },
|
|
221
|
+
];
|
|
222
|
+
|
|
206
223
|
function staticFallbackFor(providerId: string): LiveModel[] {
|
|
207
224
|
switch (providerId) {
|
|
225
|
+
case "opencode":
|
|
226
|
+
return STATIC_OPENCODE_ZEN;
|
|
208
227
|
case "opencode-go":
|
|
209
228
|
return OPENCODE_GO_FALLBACK_MODELS.map((m) => ({
|
|
210
229
|
id: m.id,
|
|
@@ -274,6 +293,15 @@ async function fetchOpenRouter(apiKey: string | undefined, timeoutMs: number): P
|
|
|
274
293
|
return mapOpenAIModels(raw);
|
|
275
294
|
}
|
|
276
295
|
|
|
296
|
+
async function fetchOpenCodeZen(apiKey: string | undefined, timeoutMs: number): Promise<LiveModel[]> {
|
|
297
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
298
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
299
|
+
const raw = (await fetchJson("https://opencode.ai/zen/v1/models", headers, timeoutMs)) as OpenAIModelsResponse;
|
|
300
|
+
// The Zen endpoint reports neither context windows nor reasoning support;
|
|
301
|
+
// frontier catalog models all expose reasoning, so default to true.
|
|
302
|
+
return mapOpenAIModels(raw).map((m) => ({ ...m, reasoning: true }));
|
|
303
|
+
}
|
|
304
|
+
|
|
277
305
|
async function fetchGroq(apiKey: string, timeoutMs: number): Promise<LiveModel[]> {
|
|
278
306
|
const raw = (await fetchJson(
|
|
279
307
|
"https://api.groq.com/openai/v1/models",
|
|
@@ -351,6 +379,8 @@ async function dispatchFetch(providerId: string, options: FetchOptions): Promise
|
|
|
351
379
|
return await fetchGoogle(apiKey, timeoutMs);
|
|
352
380
|
case "openrouter":
|
|
353
381
|
return await fetchOpenRouter(apiKey, timeoutMs);
|
|
382
|
+
case "opencode":
|
|
383
|
+
return await fetchOpenCodeZen(apiKey, timeoutMs);
|
|
354
384
|
case "groq":
|
|
355
385
|
if (!apiKey) throw new Error("Groq requires an API key for live listing");
|
|
356
386
|
return await fetchGroq(apiKey, timeoutMs);
|