@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,367 @@
1
+ /**
2
+ * File-based OAuthClientProvider for MCP servers.
3
+ *
4
+ * Implements the full OAuth2 Authorization Code flow with PKCE and
5
+ * Dynamic Client Registration (RFC 7591) as required by the MCP spec.
6
+ *
7
+ * Token and client state are persisted per-server under ~/.pi/agent/mcp-auth/
8
+ * so they survive pi restarts without requiring re-authorization.
9
+ *
10
+ * Usage: config adds `auth: { ... }` to a server config. This module
11
+ * constructs an OAuthClientProvider and the transport factory wires it in.
12
+ */
13
+
14
+ import type { OAuthClientProvider, OAuthDiscoveryState } from "@modelcontextprotocol/sdk/client/auth.js";
15
+ import type { OAuthClientMetadata, OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
16
+ import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { createHash } from "node:crypto";
20
+
21
+ // ─── Types ────────────────────────────────────────────────────────────────────
22
+
23
+ /** Auth config in mcp.json server entry. Matches the Zod AuthConfigSchema in config.ts. */
24
+ export interface AuthConfig {
25
+ /** Auth type. Currently only "oauth" is supported. */
26
+ type?: "oauth" | undefined;
27
+ /**
28
+ * Callback URL the OAuth server redirects to after authorization.
29
+ * Default: auto-detected local callback server.
30
+ */
31
+ redirectUrl?: string | undefined;
32
+ /**
33
+ * Optional scope to request during authorization.
34
+ */
35
+ scope?: string | undefined;
36
+ /**
37
+ * Pre-registered client_id (skip dynamic client registration).
38
+ */
39
+ clientId?: string | undefined;
40
+ /**
41
+ * Pre-registered client_secret.
42
+ */
43
+ clientSecret?: string | undefined;
44
+ }
45
+
46
+ // ─── Callback Server Port ───────────────────────────────────────────────────────
47
+
48
+ let callbackPort = 19876;
49
+
50
+ /** Set the callback server port. Called by the auth flow when the server starts. */
51
+ export function setCallbackPort(port: number): void {
52
+ callbackPort = port;
53
+ }
54
+
55
+ /** Get the current callback server port. */
56
+ export function getCallbackPort(): number {
57
+ return callbackPort;
58
+ }
59
+
60
+ // ─── Persistent State Types ───────────────────────────────────────────────────
61
+
62
+ interface StoredClientInfo {
63
+ client_id: string;
64
+ client_secret: string | undefined;
65
+ }
66
+
67
+ interface StoredTokens {
68
+ access_token: string;
69
+ token_type: string | undefined;
70
+ refresh_token: string | undefined;
71
+ expires_in: number | undefined;
72
+ scope: string | undefined;
73
+ /** ISO timestamp when tokens were saved (for expiry estimation). */
74
+ saved_at: string | undefined;
75
+ }
76
+
77
+ interface StoredState {
78
+ clientInfo: StoredClientInfo | undefined;
79
+ tokens: StoredTokens | undefined;
80
+ codeVerifier: string | undefined;
81
+ discoveryState: OAuthDiscoveryState | undefined;
82
+ }
83
+
84
+ // ─── File helpers ─────────────────────────────────────────────────────────────
85
+
86
+ const AUTH_DIR = join(homedir(), ".pi", "agent", "mcp-auth");
87
+
88
+ function statePath(serverName: string): string {
89
+ // Hash the server name to avoid filesystem issues with special chars
90
+ const hash = createHash("sha256").update(serverName).digest("hex").slice(0, 16);
91
+ return join(AUTH_DIR, `${hash}.json`);
92
+ }
93
+
94
+ async function loadState(serverName: string): Promise<StoredState> {
95
+ try {
96
+ const raw = await readFile(statePath(serverName), "utf8");
97
+ const parsed = JSON.parse(raw) as StoredState;
98
+ return {
99
+ clientInfo: parsed.clientInfo ?? undefined,
100
+ tokens: parsed.tokens ?? undefined,
101
+ codeVerifier: parsed.codeVerifier ?? undefined,
102
+ discoveryState: parsed.discoveryState ?? undefined,
103
+ };
104
+ } catch {
105
+ return {
106
+ clientInfo: undefined,
107
+ tokens: undefined,
108
+ codeVerifier: undefined,
109
+ discoveryState: undefined,
110
+ };
111
+ }
112
+ }
113
+
114
+ async function saveState(serverName: string, state: StoredState): Promise<void> {
115
+ await mkdir(AUTH_DIR, { recursive: true });
116
+ // Only write defined fields
117
+ const toWrite: Record<string, unknown> = {};
118
+ if (state.clientInfo !== undefined) toWrite.clientInfo = state.clientInfo;
119
+ if (state.tokens !== undefined) toWrite.tokens = state.tokens;
120
+ if (state.codeVerifier !== undefined) toWrite.codeVerifier = state.codeVerifier;
121
+ if (state.discoveryState !== undefined) toWrite.discoveryState = state.discoveryState;
122
+ await writeFile(statePath(serverName), JSON.stringify(toWrite, null, 2), "utf8");
123
+ }
124
+
125
+ // ─── OAuthClientProvider Implementation ────────────────────────────────────────
126
+
127
+ export class McpOAuthProvider implements OAuthClientProvider {
128
+ private serverName: string;
129
+ private authConfig: AuthConfig;
130
+ private _redirectUrl: string | undefined;
131
+ private _onAuthRequired: ((url: URL) => void | Promise<void>) | undefined;
132
+ private _oauthState: string | undefined;
133
+
134
+ constructor(
135
+ serverName: string,
136
+ authConfig: AuthConfig,
137
+ onAuthRequired?: (url: URL) => void | Promise<void>,
138
+ ) {
139
+ this.serverName = serverName;
140
+ this.authConfig = authConfig;
141
+ this._redirectUrl = authConfig.redirectUrl;
142
+ this._onAuthRequired = onAuthRequired;
143
+ }
144
+
145
+ // --- redirectUrl ---
146
+
147
+ get redirectUrl(): string | URL {
148
+ // Use configured redirect URL if provided, otherwise use the callback server
149
+ // Use 127.0.0.1 explicitly (IPv4) to match the callback server binding
150
+ return this._redirectUrl || `http://127.0.0.1:${callbackPort}/callback`;
151
+ }
152
+
153
+ // --- clientMetadata ---
154
+
155
+ get clientMetadata(): OAuthClientMetadata {
156
+ const redirectUrl = String(this.redirectUrl);
157
+ return {
158
+ client_name: `pi-mcp/${this.serverName}`,
159
+ redirect_uris: [redirectUrl],
160
+ grant_types: ["authorization_code", "refresh_token"],
161
+ response_types: ["code"],
162
+ token_endpoint_auth_method: this.authConfig.clientSecret ? "client_secret_basic" : "none",
163
+ };
164
+ }
165
+
166
+ // --- clientInformation ---
167
+
168
+ async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
169
+ // If static credentials provided, use those
170
+ if (this.authConfig.clientId) {
171
+ return {
172
+ client_id: this.authConfig.clientId,
173
+ ...(this.authConfig.clientSecret && { client_secret: this.authConfig.clientSecret }),
174
+ };
175
+ }
176
+ // Otherwise load from persisted DCR state
177
+ const state = await loadState(this.serverName);
178
+ if (state.clientInfo) {
179
+ return {
180
+ client_id: state.clientInfo.client_id,
181
+ ...(state.clientInfo.client_secret && { client_secret: state.clientInfo.client_secret }),
182
+ };
183
+ }
184
+ return undefined;
185
+ }
186
+
187
+ // --- saveClientInformation (DCR) ---
188
+
189
+ async saveClientInformation(clientInformation: OAuthClientInformationMixed): Promise<void> {
190
+ const state = await loadState(this.serverName);
191
+ state.clientInfo = {
192
+ client_id: clientInformation.client_id,
193
+ client_secret: clientInformation.client_secret,
194
+ };
195
+ await saveState(this.serverName, state);
196
+ }
197
+
198
+ // --- tokens ---
199
+
200
+ async tokens(): Promise<OAuthTokens | undefined> {
201
+ const state = await loadState(this.serverName);
202
+ if (!state.tokens) return undefined;
203
+
204
+ // Always return stored tokens — even if expired.
205
+ // The SDK's auth() function checks tokens?.refresh_token and attempts
206
+ // silent refresh before falling back to a new authorization flow.
207
+ // Returning undefined for expired tokens would prevent that silent refresh
208
+ // and force the user to re-authenticate via browser every time.
209
+ //
210
+ // Flow when we return expired tokens:
211
+ // transport sends expired access_token → 401
212
+ // → auth() sees refresh_token → silent refresh → success → retry
213
+ //
214
+ // Flow when we return undefined (WRONG):
215
+ // transport has no token → auth() → no refresh possible → REDIRECT
216
+ // → user must re-authenticate in browser
217
+
218
+ // Build OAuthTokens — only include defined fields
219
+ const tokens: Record<string, string> = {
220
+ access_token: state.tokens.access_token,
221
+ };
222
+ if (state.tokens.token_type !== undefined) tokens.token_type = state.tokens.token_type;
223
+ if (state.tokens.refresh_token !== undefined) tokens.refresh_token = state.tokens.refresh_token;
224
+ return tokens as unknown as OAuthTokens;
225
+ }
226
+
227
+ // --- saveTokens ---
228
+
229
+ async saveTokens(tokens: OAuthTokens): Promise<void> {
230
+ const state = await loadState(this.serverName);
231
+ state.tokens = {
232
+ access_token: tokens.access_token,
233
+ token_type: tokens.token_type,
234
+ refresh_token: tokens.refresh_token,
235
+ expires_in: tokens.expires_in,
236
+ scope: tokens.scope,
237
+ saved_at: new Date().toISOString(),
238
+ };
239
+ await saveState(this.serverName, state);
240
+ }
241
+
242
+ // --- redirectToAuthorization ---
243
+
244
+ async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
245
+ if (this._onAuthRequired) {
246
+ await this._onAuthRequired(authorizationUrl);
247
+ } else {
248
+ // Fallback: just log it
249
+ console.error(
250
+ `[pi-mcp] OAuth authorization required for "${this.serverName}".`,
251
+ `\n Open this URL in your browser: ${authorizationUrl.toString()}`,
252
+ );
253
+ }
254
+ }
255
+
256
+ // --- PKCE code verifier ---
257
+
258
+ async saveCodeVerifier(codeVerifier: string): Promise<void> {
259
+ const state = await loadState(this.serverName);
260
+ state.codeVerifier = codeVerifier;
261
+ await saveState(this.serverName, state);
262
+ }
263
+
264
+ async codeVerifier(): Promise<string> {
265
+ const state = await loadState(this.serverName);
266
+ if (!state.codeVerifier) {
267
+ throw new Error(`[pi-mcp] No PKCE code verifier found for "${this.serverName}"`);
268
+ }
269
+ return state.codeVerifier;
270
+ }
271
+
272
+ // --- Discovery state caching ---
273
+
274
+ async saveDiscoveryState(discState: OAuthDiscoveryState): Promise<void> {
275
+ const state = await loadState(this.serverName);
276
+ state.discoveryState = discState;
277
+ await saveState(this.serverName, state);
278
+ }
279
+
280
+ async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
281
+ const state = await loadState(this.serverName);
282
+ return state.discoveryState;
283
+ }
284
+
285
+ // --- OAuth state parameter (CSRF protection) ---
286
+
287
+ /**
288
+ * Set the OAuth state parameter before calling auth().
289
+ * This should be called with a cryptographically random value.
290
+ */
291
+ setState(state: string): void {
292
+ this._oauthState = state;
293
+ }
294
+
295
+ /**
296
+ * Returns the OAuth state parameter for CSRF protection.
297
+ * This is called by the SDK's auth() function when building the authorization URL.
298
+ * Returns empty string if no state has been set (no CSRF protection).
299
+ */
300
+ async state(): Promise<string> {
301
+ return this._oauthState || "";
302
+ }
303
+
304
+ // --- Credential invalidation ---
305
+
306
+ async invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise<void> {
307
+ const state = await loadState(this.serverName);
308
+ switch (scope) {
309
+ case "all":
310
+ state.clientInfo = undefined;
311
+ state.tokens = undefined;
312
+ state.codeVerifier = undefined;
313
+ state.discoveryState = undefined;
314
+ break;
315
+ case "client":
316
+ state.clientInfo = undefined;
317
+ break;
318
+ case "tokens":
319
+ state.tokens = undefined;
320
+ break;
321
+ case "verifier":
322
+ state.codeVerifier = undefined;
323
+ break;
324
+ case "discovery":
325
+ state.discoveryState = undefined;
326
+ break;
327
+ }
328
+ await saveState(this.serverName, state);
329
+ }
330
+ }
331
+
332
+ // ─── Public Helpers ────────────────────────────────────────────────────────────
333
+
334
+ /**
335
+ * Get auth status info for a server — whether tokens exist, when they were saved, etc.
336
+ * Returns null if no auth state file exists at all.
337
+ */
338
+ export async function getAuthStatus(serverName: string): Promise<{
339
+ hasTokens: boolean;
340
+ hasClientInfo: boolean;
341
+ savedAt: string | undefined;
342
+ scope: string | undefined;
343
+ } | null> {
344
+ const state = await loadState(serverName);
345
+ if (
346
+ state.clientInfo === undefined &&
347
+ state.tokens === undefined &&
348
+ state.codeVerifier === undefined &&
349
+ state.discoveryState === undefined
350
+ ) {
351
+ return null;
352
+ }
353
+ return {
354
+ hasTokens: state.tokens !== undefined,
355
+ hasClientInfo: state.clientInfo !== undefined,
356
+ savedAt: state.tokens?.saved_at,
357
+ scope: state.tokens?.scope,
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Reset all OAuth state for a server (tokens, client info, PKCE verifier, discovery).
363
+ * Used to force re-authorization on next connection.
364
+ */
365
+ export async function resetAuth(serverName: string): Promise<void> {
366
+ await unlink(statePath(serverName)).catch(() => {});
367
+ }