@phi-code-admin/phi-code 0.75.3 → 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/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +19 -1
- package/dist/core/system-prompt.js.map +1 -1
- 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/memory.ts +43 -0
- 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/memory.ts
CHANGED
|
@@ -411,6 +411,49 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
411
411
|
},
|
|
412
412
|
});
|
|
413
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Per-turn reminder injection (Combo strategy D).
|
|
416
|
+
*
|
|
417
|
+
* Pi LLMs (Claude, Kimi, GLM, MiniMax) consistently ignore "MANDATORY"
|
|
418
|
+
* guidelines buried in long system prompts. To enforce the
|
|
419
|
+
* memory_search/memory_write rules, we prepend a fresh <system-reminder>
|
|
420
|
+
* block to the system prompt at EVERY turn. This pattern is what Claude
|
|
421
|
+
* Code and Cursor use successfully in production for high-priority rules.
|
|
422
|
+
*
|
|
423
|
+
* The reminder is short and concrete (includes the user's current prompt
|
|
424
|
+
* truncated) so it grabs the LLM's attention even mid-conversation when
|
|
425
|
+
* the original critical_rule has receded due to context pressure.
|
|
426
|
+
*/
|
|
427
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
428
|
+
// Skip during /plan orchestration: the orchestrator manages its own
|
|
429
|
+
// systemPrompt per phase via the same hook. Conflicting overrides would
|
|
430
|
+
// override the orchestrator's agent personas.
|
|
431
|
+
if ((globalThis as any).__phiOrchestrationActive) {
|
|
432
|
+
return {};
|
|
433
|
+
}
|
|
434
|
+
const userPrompt = (event.prompt ?? "").trim();
|
|
435
|
+
if (userPrompt.length === 0) {
|
|
436
|
+
return {};
|
|
437
|
+
}
|
|
438
|
+
const truncated = userPrompt.length > 200 ? `${userPrompt.slice(0, 200)}...` : userPrompt;
|
|
439
|
+
const reminder = `<system-reminder>
|
|
440
|
+
You are about to respond to a new user message:
|
|
441
|
+
"${truncated.replace(/"/g, '\\"')}"
|
|
442
|
+
|
|
443
|
+
REMINDER (project rule, applies every turn):
|
|
444
|
+
1. Call \`memory_search\` FIRST with keywords from the user's intent. Recent
|
|
445
|
+
project context, prior decisions, and saved learnings are accessible
|
|
446
|
+
ONLY via this tool.
|
|
447
|
+
2. AFTER completing significant work, call \`memory_write\` to save what
|
|
448
|
+
you did and learned.
|
|
449
|
+
|
|
450
|
+
These two tool calls are not optional. Skipping them violates project rules.
|
|
451
|
+
</system-reminder>
|
|
452
|
+
|
|
453
|
+
`;
|
|
454
|
+
return { systemPrompt: reminder + event.systemPrompt };
|
|
455
|
+
});
|
|
456
|
+
|
|
414
457
|
/**
|
|
415
458
|
* Auto-load AGENTS.md on session start
|
|
416
459
|
* Checks both project directory and ~/.phi/memory/
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Models Extension - `/models` command for live model catalog management.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* /models : list models grouped by provider (uses cached/live data)
|
|
6
|
+
* /models list <id> : list models for a specific provider
|
|
7
|
+
* /models refresh : re-fetch the model catalog for every configured provider
|
|
8
|
+
* (writes the result into ~/.phi/agent/models.json,
|
|
9
|
+
* triggering ApiKeyStore hot-reload + ModelRegistry refresh).
|
|
10
|
+
* /models refresh <id>: refresh a single provider
|
|
11
|
+
*
|
|
12
|
+
* This is what keeps the model picker (/model) and the wizards (/setup,
|
|
13
|
+
* /phi-init) in sync with each provider's upstream catalog — for instance
|
|
14
|
+
* when OpenCode Go publishes a new model, a single `/models refresh` makes
|
|
15
|
+
* it appear everywhere without restarting Phi Code.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ApiKeyStore, type ExtensionAPI, getApiKeyStore } from "phi-code";
|
|
19
|
+
import { fetchLiveModels, peekCache, resetLiveModelsCache, toPersistedModel } from "./providers/live-models.js";
|
|
20
|
+
|
|
21
|
+
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
22
|
+
"opencode-go": "OpenCode Go",
|
|
23
|
+
"alibaba-codingplan": "Alibaba Coding Plan (OpenAI-compat)",
|
|
24
|
+
"alibaba-codingplan-anthropic": "Alibaba Coding Plan (Anthropic-compat)",
|
|
25
|
+
openai: "OpenAI",
|
|
26
|
+
anthropic: "Anthropic",
|
|
27
|
+
google: "Google Gemini",
|
|
28
|
+
openrouter: "OpenRouter",
|
|
29
|
+
groq: "Groq",
|
|
30
|
+
ollama: "Ollama (local)",
|
|
31
|
+
"lm-studio": "LM Studio (local)",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function displayName(id: string): string {
|
|
35
|
+
return PROVIDER_DISPLAY[id] ?? id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RefreshOutcome {
|
|
39
|
+
provider: string;
|
|
40
|
+
source: "live" | "cache" | "fallback" | "unsupported" | "skipped";
|
|
41
|
+
count: number;
|
|
42
|
+
error?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function refreshOne(
|
|
46
|
+
store: ApiKeyStore,
|
|
47
|
+
providerId: string,
|
|
48
|
+
): Promise<RefreshOutcome> {
|
|
49
|
+
const stored = store.getProvider(providerId);
|
|
50
|
+
const apiKey = stored?.apiKey && !stored.apiKey.startsWith("$") && stored.apiKey !== "local"
|
|
51
|
+
? stored.apiKey
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
resetLiveModelsCache(providerId);
|
|
55
|
+
const result = await fetchLiveModels(providerId, {
|
|
56
|
+
apiKey,
|
|
57
|
+
forceRefresh: true,
|
|
58
|
+
timeoutMs: 8_000,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (result.source === "unsupported") {
|
|
62
|
+
return { provider: providerId, source: "skipped", count: 0, error: result.error };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const persisted = result.models.map(toPersistedModel);
|
|
66
|
+
if (persisted.length === 0) {
|
|
67
|
+
return { provider: providerId, source: result.source, count: 0, error: result.error };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Preserve baseUrl/api/apiKey/headers from existing config; only models change.
|
|
71
|
+
const baseUrl =
|
|
72
|
+
stored?.baseUrl ??
|
|
73
|
+
(providerId === "opencode-go"
|
|
74
|
+
? "https://opencode.ai/zen/go/v1"
|
|
75
|
+
: providerId === "alibaba-codingplan"
|
|
76
|
+
? "https://coding-intl.dashscope.aliyuncs.com/v1"
|
|
77
|
+
: providerId === "alibaba-codingplan-anthropic"
|
|
78
|
+
? "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic"
|
|
79
|
+
: providerId === "openai"
|
|
80
|
+
? "https://api.openai.com/v1"
|
|
81
|
+
: providerId === "anthropic"
|
|
82
|
+
? "https://api.anthropic.com/v1"
|
|
83
|
+
: providerId === "google"
|
|
84
|
+
? "https://generativelanguage.googleapis.com/v1beta"
|
|
85
|
+
: providerId === "openrouter"
|
|
86
|
+
? "https://openrouter.ai/api/v1"
|
|
87
|
+
: providerId === "groq"
|
|
88
|
+
? "https://api.groq.com/openai/v1"
|
|
89
|
+
: providerId === "ollama"
|
|
90
|
+
? "http://localhost:11434/v1"
|
|
91
|
+
: providerId === "lm-studio"
|
|
92
|
+
? "http://localhost:1234/v1"
|
|
93
|
+
: undefined);
|
|
94
|
+
|
|
95
|
+
if (!baseUrl) {
|
|
96
|
+
return { provider: providerId, source: "skipped", count: 0, error: "unknown baseUrl" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
store.setKey(providerId, stored?.apiKey ?? "local", {
|
|
100
|
+
baseUrl,
|
|
101
|
+
api: stored?.api,
|
|
102
|
+
models: persisted,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { provider: providerId, source: result.source, count: persisted.length, error: result.error };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function modelsExtension(pi: ExtensionAPI) {
|
|
109
|
+
const store = getApiKeyStore();
|
|
110
|
+
|
|
111
|
+
pi.registerCommand("models", {
|
|
112
|
+
description: "List or refresh the live model catalog (use `/models refresh` after a provider adds a new model)",
|
|
113
|
+
handler: async (args, ctx) => {
|
|
114
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
115
|
+
const sub = tokens[0]?.toLowerCase() ?? "";
|
|
116
|
+
const target = tokens[1];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (sub === "" || sub === "list") {
|
|
120
|
+
await listCommand(target, ctx);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (sub === "refresh") {
|
|
124
|
+
await refreshCommand(target, ctx);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
ctx.ui.notify(
|
|
128
|
+
"Unknown subcommand. Use: `/models [list|refresh] [provider-id]`",
|
|
129
|
+
"warning",
|
|
130
|
+
);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
`/models error: ${err instanceof Error ? err.message : String(err)}`,
|
|
134
|
+
"error",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
async function listCommand(target: string | undefined, ctx: { ui: { notify: (m: string, t?: "info" | "warning" | "error") => void } }): Promise<void> {
|
|
141
|
+
const providers = target ? [target] : store.listProviders();
|
|
142
|
+
if (providers.length === 0) {
|
|
143
|
+
ctx.ui.notify(
|
|
144
|
+
"No providers configured. Run `/setup` or `/phi-init` to add one.",
|
|
145
|
+
"info",
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let out = `**Model catalog (${providers.length} provider(s))**\n\n`;
|
|
151
|
+
for (const id of providers) {
|
|
152
|
+
const stored = store.getProvider(id);
|
|
153
|
+
const cached = peekCache(id);
|
|
154
|
+
const models = (Array.isArray(stored?.models) ? stored?.models : []) as Array<{ id?: string; name?: string }>;
|
|
155
|
+
const ageMin = cached ? Math.round(cached.ageMs / 60_000) : undefined;
|
|
156
|
+
|
|
157
|
+
out += ` **${displayName(id)}** \`${id}\``;
|
|
158
|
+
out += ` — ${models.length} model(s) persisted`;
|
|
159
|
+
if (ageMin !== undefined) out += ` (cache age: ${ageMin}m)`;
|
|
160
|
+
out += "\n";
|
|
161
|
+
if (models.length > 0) {
|
|
162
|
+
const ids = models.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
|
163
|
+
out += ` ${ids.join(", ")}\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
out += `\nUse \`/models refresh\` to re-fetch from each provider's API.`;
|
|
167
|
+
ctx.ui.notify(out, "info");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Background refresh on session_start so every new Phi Code session reflects
|
|
171
|
+
// the latest provider catalogs without the user typing `/models refresh`.
|
|
172
|
+
// Failures are silent — startup must never be blocked by upstream API hiccups.
|
|
173
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
174
|
+
try {
|
|
175
|
+
store.load();
|
|
176
|
+
} catch {
|
|
177
|
+
// no models.json yet
|
|
178
|
+
}
|
|
179
|
+
const providers = store.listProviders();
|
|
180
|
+
if (providers.length === 0) return;
|
|
181
|
+
|
|
182
|
+
// Fire-and-forget. Hot-reload via models_json_changed event surfaces results.
|
|
183
|
+
void (async () => {
|
|
184
|
+
let changed = 0;
|
|
185
|
+
for (const id of providers) {
|
|
186
|
+
const outcome = await refreshOne(store, id).catch(() => undefined);
|
|
187
|
+
if (outcome && outcome.source === "live" && outcome.count > 0) changed++;
|
|
188
|
+
}
|
|
189
|
+
if (changed > 0) {
|
|
190
|
+
try {
|
|
191
|
+
ctx.ui.notify(
|
|
192
|
+
`Refreshed ${changed}/${providers.length} provider catalog(s) in the background.`,
|
|
193
|
+
"info",
|
|
194
|
+
);
|
|
195
|
+
} catch {
|
|
196
|
+
// notify may fail if the TUI is mid-shutdown — ignore
|
|
197
|
+
}
|
|
198
|
+
pi.events.emit("models_json_changed", { source: "session-start-refresh" });
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
async function refreshCommand(
|
|
204
|
+
target: string | undefined,
|
|
205
|
+
ctx: { ui: { notify: (m: string, t?: "info" | "warning" | "error") => void; setStatus?: (k: string, v?: string) => void } },
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const providers = target ? [target] : store.listProviders();
|
|
208
|
+
if (providers.length === 0) {
|
|
209
|
+
ctx.ui.notify("No providers configured.", "warning");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
ctx.ui.notify(`Refreshing ${providers.length} provider(s)...`, "info");
|
|
213
|
+
ctx.ui.setStatus?.("models-refresh", "Fetching live model catalogs...");
|
|
214
|
+
|
|
215
|
+
const outcomes: RefreshOutcome[] = [];
|
|
216
|
+
for (const id of providers) {
|
|
217
|
+
const outcome = await refreshOne(store, id).catch((err) => ({
|
|
218
|
+
provider: id,
|
|
219
|
+
source: "skipped" as const,
|
|
220
|
+
count: 0,
|
|
221
|
+
error: err instanceof Error ? err.message : String(err),
|
|
222
|
+
}));
|
|
223
|
+
outcomes.push(outcome);
|
|
224
|
+
}
|
|
225
|
+
ctx.ui.setStatus?.("models-refresh", undefined);
|
|
226
|
+
|
|
227
|
+
let out = "**Refresh report:**\n";
|
|
228
|
+
for (const o of outcomes) {
|
|
229
|
+
const icon = o.source === "live" ? "[ok]" : o.source === "fallback" ? "[fb]" : o.source === "cache" ? "[c]" : "[--]";
|
|
230
|
+
out += ` ${icon} ${displayName(o.provider)} \`${o.provider}\` — ${o.count} model(s) (${o.source}${o.error ? `, ${o.error}` : ""})\n`;
|
|
231
|
+
}
|
|
232
|
+
out += `\nModels persisted to \`${store.configPath}\`. \`/model\` picker now reflects this catalog.`;
|
|
233
|
+
ctx.ui.notify(out, "info");
|
|
234
|
+
pi.events.emit("models_json_changed", { source: "models-refresh" });
|
|
235
|
+
}
|
|
236
|
+
}
|