@phi-code-admin/phi-code 0.82.3 → 0.84.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/docs/usage.md +1 -1
  3. package/extensions/phi/btw/LICENSE +21 -0
  4. package/extensions/phi/btw/btw-ui.ts +238 -0
  5. package/extensions/phi/btw/btw.ts +363 -0
  6. package/extensions/phi/btw/index.ts +17 -0
  7. package/extensions/phi/btw/prompts/btw-system.txt +9 -0
  8. package/extensions/phi/chrome/LICENSE +21 -0
  9. package/extensions/phi/chrome/browser-extension/manifest.json +26 -0
  10. package/extensions/phi/chrome/browser-extension/service_worker.js +2388 -0
  11. package/extensions/phi/chrome/browser-extension/snapshot_injected.js +677 -0
  12. package/extensions/phi/chrome/index.ts +1799 -0
  13. package/extensions/phi/goal/LICENSE +21 -0
  14. package/extensions/phi/goal/index.ts +784 -0
  15. package/extensions/phi/mcp/LICENSE +21 -0
  16. package/extensions/phi/mcp/callback-server.ts +263 -0
  17. package/extensions/phi/mcp/config.ts +195 -0
  18. package/extensions/phi/mcp/errors.ts +35 -0
  19. package/extensions/phi/mcp/index.ts +376 -0
  20. package/extensions/phi/mcp/oauth-provider.ts +367 -0
  21. package/extensions/phi/mcp/server-manager.ts +464 -0
  22. package/extensions/phi/mcp/tool-bridge.ts +494 -0
  23. package/extensions/phi/todo/LICENSE +21 -0
  24. package/extensions/phi/todo/config.ts +14 -0
  25. package/extensions/phi/todo/index.ts +113 -0
  26. package/extensions/phi/todo/locales/de.json +17 -0
  27. package/extensions/phi/todo/locales/en.json +15 -0
  28. package/extensions/phi/todo/locales/es.json +17 -0
  29. package/extensions/phi/todo/locales/fr.json +17 -0
  30. package/extensions/phi/todo/locales/pt-BR.json +17 -0
  31. package/extensions/phi/todo/locales/pt.json +17 -0
  32. package/extensions/phi/todo/locales/ru.json +17 -0
  33. package/extensions/phi/todo/locales/uk.json +17 -0
  34. package/extensions/phi/todo/rpiv-config/config.ts +223 -0
  35. package/extensions/phi/todo/rpiv-config/index.ts +12 -0
  36. package/extensions/phi/todo/state/i18n-bridge.ts +65 -0
  37. package/extensions/phi/todo/state/invariants.ts +20 -0
  38. package/extensions/phi/todo/state/replay.ts +38 -0
  39. package/extensions/phi/todo/state/selectors.ts +107 -0
  40. package/extensions/phi/todo/state/state-reducer.ts +187 -0
  41. package/extensions/phi/todo/state/state.ts +18 -0
  42. package/extensions/phi/todo/state/store.ts +54 -0
  43. package/extensions/phi/todo/state/task-graph.ts +57 -0
  44. package/extensions/phi/todo/todo-overlay.ts +194 -0
  45. package/extensions/phi/todo/todo.ts +146 -0
  46. package/extensions/phi/todo/tool/response-envelope.ts +94 -0
  47. package/extensions/phi/todo/tool/types.ts +128 -0
  48. package/extensions/phi/todo/view/format.ts +162 -0
  49. package/package.json +4 -2
@@ -0,0 +1,376 @@
1
+ /**
2
+ * pi-mcp — MCP client extension for the Pi coding agent.
3
+ *
4
+ * Entry point registered in package.json under "pi.extensions".
5
+ * Pi loads this file via jiti (TypeScript executed directly, no build step).
6
+ *
7
+ * Wires together: config → server manager → tool bridge → Pi API.
8
+ *
9
+ * Vendored into phi-code from the MIT-licensed `pi-mcp-extension` by irahardianto
10
+ * (https://github.com/irahardianto/pi-mcp-extension). See ./LICENSE. Adapted for
11
+ * phi: type imports resolve via "phi-code"; config lives in ~/.phi/agent/mcp.json
12
+ * and <cwd>/.phi/mcp.json (phi configDir), and it ships bundled (no install).
13
+ */
14
+
15
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "phi-code";
16
+ import { loadConfig } from "./config.js";
17
+ import { ServerManager } from "./server-manager.js";
18
+ import type { TransportAuthCallbacks } from "./server-manager.js";
19
+ import { ToolBridge } from "./tool-bridge.js";
20
+ import { McpError } from "./errors.js";
21
+ import { exec } from "node:child_process";
22
+
23
+ // OAuth imports
24
+ import {
25
+ ensureCallbackServer,
26
+ waitForCallback,
27
+ cancelCallback,
28
+ stopCallbackServer,
29
+ } from "./callback-server.js";
30
+ import { setCallbackPort, McpOAuthProvider } from "./oauth-provider.js";
31
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
32
+ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
33
+
34
+ /**
35
+ * Open a URL in the user's default browser.
36
+ * Works on macOS, Linux, and Windows.
37
+ */
38
+ function openBrowser(url: string): void {
39
+ const cmd = process.platform === "darwin"
40
+ ? `open "${url}"`
41
+ : process.platform === "win32"
42
+ ? `start "" "${url}"`
43
+ : `xdg-open "${url}"`;
44
+
45
+ exec(cmd, (err, stdout, stderr) => {
46
+ if (err) {
47
+ const errorMsg = `[pi-mcp] Failed to open browser: ${err.message}`;
48
+ console.error(errorMsg);
49
+ if (stderr) {
50
+ console.error(`[pi-mcp] Browser error output: ${stderr}`);
51
+ }
52
+ }
53
+ });
54
+ }
55
+
56
+ export default async function (pi: ExtensionAPI): Promise<void> {
57
+ // ── 1. Load and validate config ──────────────────────────────────────────
58
+ // cwd is available on the ExtensionContext passed to event handlers.
59
+ // We load config lazily on session_start to get the correct per-session cwd.
60
+ // For the initial load we use process.cwd() as a bootstrap path to detect
61
+ // whether any config exists at all.
62
+ let config;
63
+ try {
64
+ config = await loadConfig(process.cwd());
65
+ } catch (err) {
66
+ // Can't notify yet (no ctx), so log to stderr. The session_start handler
67
+ // will re-try with the real cwd and surface errors properly.
68
+ console.error(`[pi-mcp] Config error: ${err instanceof McpError ? err.message : String(err)}`);
69
+ return;
70
+ }
71
+
72
+ if (Object.keys(config.mcpServers).length === 0) {
73
+ // No servers configured — silently exit. Users can create mcp.json later.
74
+ return;
75
+ }
76
+
77
+ // ── 2. Initialize bridge components ──────────────────────────────────────
78
+ // Auth callbacks — opens browser and notifies user when OAuth is needed
79
+ const authCallbacks: TransportAuthCallbacks = {
80
+ onAuthRequired: (serverName: string, authorizationUrl: URL): void => {
81
+ console.error(
82
+ `[pi-mcp] OAuth required for "${serverName}". Opening browser for authorization...`,
83
+ );
84
+ openBrowser(authorizationUrl.toString());
85
+ },
86
+ };
87
+
88
+ const manager = new ServerManager(config, authCallbacks);
89
+ const bridge = new ToolBridge(config.settings, pi);
90
+
91
+ // Connect tool refresh callback: called on connect and on list_changed
92
+ manager.setToolRefreshCallback(async (serverName, client) => {
93
+ await bridge.refreshTools(serverName, client);
94
+ });
95
+
96
+ // ── 3. Session lifecycle ──────────────────────────────────────────────────
97
+ pi.on("session_start", async (_event, ctx: ExtensionContext) => {
98
+ // Reload config with the real session cwd (project config may differ)
99
+ let sessionConfig = config;
100
+ try {
101
+ sessionConfig = await loadConfig(ctx.cwd);
102
+ } catch (err) {
103
+ const msg = err instanceof McpError ? err.userMessage : String(err);
104
+ ctx.ui.notify(`pi-mcp: Config error — ${msg}`, "error");
105
+ return;
106
+ }
107
+
108
+ // If config changed (different cwd with project-level overrides),
109
+ // shut down old servers and rebuild the manager's server list
110
+ if (JSON.stringify(sessionConfig) !== JSON.stringify(config)) {
111
+ // Deactivate and remove all tools from old config
112
+ for (const server of manager.getAllServers()) {
113
+ bridge.removeServer(server.name);
114
+ }
115
+ // Shut down all running servers
116
+ await manager.shutdownAll();
117
+ // Rebuild server entries from new config
118
+ manager.rebuildServers(sessionConfig);
119
+ }
120
+
121
+ const eagerServers = Object.entries(sessionConfig.mcpServers).filter(
122
+ ([, cfg]) => cfg.lifecycle === "eager",
123
+ );
124
+
125
+ // Start all eager servers concurrently
126
+ await Promise.allSettled(
127
+ eagerServers.map(async ([name]) => {
128
+ try {
129
+ await manager.startServer(name, ctx.cwd);
130
+ } catch (err) {
131
+ const msg = err instanceof McpError ? err.userMessage : String(err);
132
+ ctx.ui.notify(`pi-mcp: Failed to start ${name} — ${msg}`, "error");
133
+ }
134
+ }),
135
+ );
136
+ });
137
+
138
+ pi.on("session_shutdown", async (_event, _ctx: ExtensionContext) => {
139
+ // Stop the callback server
140
+ await stopCallbackServer().catch(() => {});
141
+
142
+ // Deactivate all tools before shutting down servers
143
+ for (const server of manager.getAllServers()) {
144
+ bridge.deactivateServer(server.name);
145
+ }
146
+ await manager.shutdownAll();
147
+ });
148
+
149
+ // ── 4. /mcp — show server status ─────────────────────────────────────────
150
+ pi.registerCommand("mcp", {
151
+ description:
152
+ "Show MCP server status. Usage: /mcp [server-name] for detail.",
153
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
154
+ const serverName = args.trim();
155
+ if (serverName) {
156
+ // Detailed view: status + recent stderr
157
+ const server = manager.getServer(serverName);
158
+ if (!server) {
159
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
160
+ return;
161
+ }
162
+ const logs = manager.getServerLogs(serverName);
163
+ const detail = [
164
+ `Server: ${serverName}`,
165
+ `State: ${server.state}`,
166
+ `Retries: ${server.retryCount}`,
167
+ server.lastError ? `Last error: ${server.lastError.message}` : null,
168
+ "",
169
+ "Recent output:",
170
+ logs,
171
+ ]
172
+ .filter(Boolean)
173
+ .join("\n");
174
+ ctx.ui.notify(detail, "info");
175
+ } else {
176
+ // Summary view: all servers
177
+ ctx.ui.notify(manager.getStatusSummary(), "info");
178
+ }
179
+ },
180
+ });
181
+
182
+ // ── 5. /mcp:stop — stop a server ─────────────────────────────────────────
183
+ pi.registerCommand("mcp:stop", {
184
+ description: "Stop an MCP server. Usage: /mcp:stop <server-name>",
185
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
186
+ const serverName = args.trim();
187
+ if (!serverName) {
188
+ ctx.ui.notify("Usage: /mcp:stop <server-name>", "error");
189
+ return;
190
+ }
191
+ if (!manager.getServer(serverName)) {
192
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
193
+ return;
194
+ }
195
+ bridge.deactivateServer(serverName);
196
+ await manager.stopServer(serverName);
197
+ ctx.ui.notify(`pi-mcp: Stopped ${serverName}`, "info");
198
+ },
199
+ });
200
+
201
+ // ── 6. /mcp:start — manually start a lazy server ─────────────────────────
202
+ pi.registerCommand("mcp:start", {
203
+ description: "Start an MCP server. Usage: /mcp:start <server-name>",
204
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
205
+ const serverName = args.trim();
206
+ if (!serverName) {
207
+ ctx.ui.notify("Usage: /mcp:start <server-name>", "error");
208
+ return;
209
+ }
210
+ if (!manager.getServer(serverName)) {
211
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
212
+ return;
213
+ }
214
+ try {
215
+ await manager.startServer(serverName, ctx.cwd);
216
+ ctx.ui.notify(`pi-mcp: Started ${serverName}`, "info");
217
+ } catch (err) {
218
+ const msg = err instanceof McpError ? err.userMessage : String(err);
219
+ ctx.ui.notify(`pi-mcp: Failed to start ${serverName} — ${msg}`, "error");
220
+ }
221
+ },
222
+ });
223
+
224
+ // ── 7. /mcp:auth — trigger OAuth authentication for a server ────────────────
225
+ pi.registerCommand("mcp:auth", {
226
+ description:
227
+ "Trigger OAuth authentication for a server. Resets credentials and opens browser for re-authorization. Usage: /mcp:auth <server-name>",
228
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
229
+ const serverName = args.trim();
230
+ if (!serverName) {
231
+ // List servers with auth config
232
+ const authServers = manager.getAllServers().filter((s) => s.config.auth);
233
+ if (authServers.length === 0) {
234
+ ctx.ui.notify(
235
+ "pi-mcp: No servers with OAuth configured. Add `auth: { type: \"oauth\" }` to a server in mcp.json.",
236
+ "error",
237
+ );
238
+ return;
239
+ }
240
+ const lines = authServers.map(async (s) => {
241
+ const status = await manager.getServerAuthStatus(s.name);
242
+ const authIcon = status?.hasTokens ? "\u2705 authenticated" : "\u274C not authenticated";
243
+ const savedInfo = status?.savedAt ? ` (since ${status.savedAt})` : "";
244
+ return ` ${s.name}: ${authIcon}${savedInfo}`;
245
+ });
246
+ const statusLines = await Promise.all(lines);
247
+ ctx.ui.notify(
248
+ [
249
+ "Usage: /mcp:auth <server-name>",
250
+ "",
251
+ "OAuth-enabled servers:",
252
+ ...statusLines,
253
+ ].join("\n"),
254
+ "info",
255
+ );
256
+ return;
257
+ }
258
+ const server = manager.getServer(serverName);
259
+ if (!server) {
260
+ ctx.ui.notify(`pi-mcp: No server named "${serverName}"`, "error");
261
+ return;
262
+ }
263
+ if (!server.config.auth) {
264
+ ctx.ui.notify(
265
+ `pi-mcp: Server "${serverName}" does not have OAuth configured. Add \`auth: { type: "oauth" }\` to its config in mcp.json.`,
266
+ "error",
267
+ );
268
+ return;
269
+ }
270
+
271
+ const config = server.config;
272
+ let oauthState: string | undefined;
273
+
274
+ try {
275
+ // Stop the server if running
276
+ if (server.state !== "stopped") {
277
+ bridge.deactivateServer(serverName);
278
+ await manager.stopServer(serverName);
279
+ }
280
+
281
+ // Validate that we have a server URL (required for OAuth)
282
+ if (!config.url) {
283
+ throw new McpError(
284
+ `Server "${serverName}" has OAuth configured but no URL. OAuth requires a URL-based server transport.`,
285
+ serverName,
286
+ "config",
287
+ );
288
+ }
289
+
290
+ // Reset all OAuth credentials (tokens, client info, PKCE, discovery)
291
+ await manager.resetServerAuth(serverName);
292
+
293
+ ctx.ui.notify(
294
+ `pi-mcp: Starting OAuth flow for ${serverName}...`,
295
+ "info",
296
+ );
297
+
298
+ // 1. Start the callback server
299
+ const port = await ensureCallbackServer();
300
+ setCallbackPort(port);
301
+
302
+ // 2. Generate a cryptographically secure state parameter for CSRF protection
303
+ oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
304
+ .map((b: number) => b.toString(16).padStart(2, "0"))
305
+ .join("");
306
+
307
+ // 3. Register the callback promise BEFORE opening the browser
308
+ const callbackPromise = waitForCallback(oauthState);
309
+
310
+ // 4. Create auth provider and transport
311
+ const authProvider = new McpOAuthProvider(
312
+ serverName,
313
+ config.auth || { type: "oauth" },
314
+ (url: URL) => {
315
+ console.error(`[pi-mcp] Opening browser for ${serverName}...`);
316
+ openBrowser(url.toString());
317
+ },
318
+ );
319
+
320
+ // CRITICAL FIX #1: Set the OAuth state on the provider before calling auth()
321
+ // This ensures the state parameter is included in the authorization URL
322
+ authProvider.setState(oauthState);
323
+
324
+ const transport = new StreamableHTTPClientTransport(
325
+ new URL(config.url),
326
+ { authProvider },
327
+ );
328
+
329
+ // 5. Start the auth flow - this will trigger redirectToAuthorization which opens the browser
330
+ // CRITICAL FIX #2: Check the return value of auth() instead of catching UnauthorizedError
331
+ // The SDK returns 'REDIRECT' when it needs browser interaction, not an error
332
+ const authResult = await auth(authProvider, { serverUrl: config.url });
333
+
334
+ if (authResult === "AUTHORIZED") {
335
+ // Auth succeeded without needing browser interaction (e.g., had valid tokens)
336
+ ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully!`, "info");
337
+ } else if (authResult === "REDIRECT") {
338
+ // Browser was opened, wait for the callback from the user
339
+ ctx.ui.notify(
340
+ `pi-mcp: Browser opened for ${serverName}. Complete authorization to continue...`,
341
+ "info",
342
+ );
343
+
344
+ // 6. Wait for the callback (this blocks until the user authorizes)
345
+ const code = await callbackPromise;
346
+
347
+ // 7. Complete the OAuth flow with the authorization code
348
+ await transport.finishAuth(code);
349
+
350
+ ctx.ui.notify(`pi-mcp: ${serverName} authenticated successfully!`, "info");
351
+ } else {
352
+ throw new McpError(
353
+ `Unexpected auth result: ${authResult}`,
354
+ serverName,
355
+ "protocol",
356
+ );
357
+ }
358
+
359
+ // 8. Close the transport (we'll create a new one when starting the server)
360
+ await transport.close().catch(() => {});
361
+
362
+ // 9. Start the server with fresh auth credentials
363
+ await manager.startServer(serverName, ctx.cwd);
364
+
365
+ } catch (err) {
366
+ const msg = err instanceof McpError ? err.userMessage : String(err);
367
+ ctx.ui.notify(`pi-mcp: Authentication failed for ${serverName} — ${msg}`, "error");
368
+
369
+ // Clean up on error
370
+ if (oauthState) {
371
+ cancelCallback(oauthState);
372
+ }
373
+ }
374
+ },
375
+ });
376
+ }