@mariozechner/pi-coding-agent 0.36.0 → 0.37.1
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 +30 -0
- package/README.md +27 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +3 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +25 -8
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +9 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +115 -18
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/export-html/template.css +55 -0
- package/dist/core/export-html/template.js +124 -7
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +1 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +9 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -3
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +50 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +39 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -0
- package/dist/modes/interactive/components/login-dialog.js +135 -0
- package/dist/modes/interactive/components/login-dialog.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +5 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +211 -84
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +4 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/sdk.md +12 -4
- package/examples/extensions/claude-rules.ts +83 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +7 -1
- package/examples/sdk/06-extensions.ts +5 -7
- package/examples/sdk/12-full-control.ts +2 -6
- package/package.json +12 -8
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credential storage for API keys and OAuth tokens.
|
|
3
3
|
* Handles loading, saving, and refreshing credentials from auth.json.
|
|
4
|
+
*
|
|
5
|
+
* Uses file locking to prevent race conditions when multiple pi instances
|
|
6
|
+
* try to refresh tokens simultaneously.
|
|
4
7
|
*/
|
|
5
8
|
import { type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai";
|
|
6
9
|
export type ApiKeyCredential = {
|
|
@@ -85,17 +88,22 @@ export declare class AuthStorage {
|
|
|
85
88
|
placeholder?: string;
|
|
86
89
|
}) => Promise<string>;
|
|
87
90
|
onProgress?: (message: string) => void;
|
|
91
|
+
/** For providers with local callback servers (e.g., openai-codex), races with browser callback */
|
|
92
|
+
onManualCodeInput?: () => Promise<string>;
|
|
93
|
+
/** For cancellation support (e.g., github-copilot polling) */
|
|
94
|
+
signal?: AbortSignal;
|
|
88
95
|
}): Promise<void>;
|
|
89
96
|
/**
|
|
90
97
|
* Logout from a provider.
|
|
91
98
|
*/
|
|
92
99
|
logout(provider: string): void;
|
|
100
|
+
private refreshOAuthTokenWithLock;
|
|
93
101
|
/**
|
|
94
102
|
* Get API key for a provider.
|
|
95
103
|
* Priority:
|
|
96
104
|
* 1. Runtime override (CLI --api-key)
|
|
97
105
|
* 2. API key from auth.json
|
|
98
|
-
* 3. OAuth token from auth.json (auto-refreshed)
|
|
106
|
+
* 3. OAuth token from auth.json (auto-refreshed with locking)
|
|
99
107
|
* 4. Environment variable
|
|
100
108
|
* 5. Fallback resolver (models.json custom providers)
|
|
101
109
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAQN,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,OAAO,CAAC;CACd,GAAG,gBAAgB,CAAC;AAErB,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE7D;;GAEG;AACH,qBAAa,WAAW;IAKX,OAAO,CAAC,QAAQ;IAJ5B,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,gBAAgB,CAAC,CAA2C;IAEpE,YAAoB,QAAQ,EAAE,MAAM,EAEnC;IAED;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEvD;IAED;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1C;IAED;;;OAGG;IACH,mBAAmB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAE5E;IAED;;OAEG;IACH,MAAM,IAAI,IAAI,CAUb;IAED;;OAEG;IACH,OAAO,CAAC,IAAI;IASZ;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEhD;IAED;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,GAAG,IAAI,CAGtD;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG7B;IAED;;OAEG;IACH,IAAI,IAAI,MAAM,EAAE,CAEf;IAED;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7B;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMjC;IAED;;OAEG;IACH,MAAM,IAAI,eAAe,CAExB;IAED;;OAEG;IACG,KAAK,CACV,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE;QACV,MAAM,EAAE,CAAC,IAAI,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,YAAY,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;QAC/D,QAAQ,EAAE,CAAC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;QACjF,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;KACvC,GACC,OAAO,CAAC,IAAI,CAAC,CA+Bf;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE7B;IAED;;;;;;;;OAQG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwC7D;CACD","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\tloginOpenAICodex,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname } from \"path\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(\n\t\tprovider: OAuthProvider,\n\t\tcallbacks: {\n\t\t\tonAuth: (info: { url: string; instructions?: string }) => void;\n\t\t\tonPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;\n\t\t\tonProgress?: (message: string) => void;\n\t\t},\n\t): Promise<void> {\n\t\tlet credentials: OAuthCredentials;\n\n\t\tswitch (provider) {\n\t\t\tcase \"anthropic\":\n\t\t\t\tcredentials = await loginAnthropic(\n\t\t\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"github-copilot\":\n\t\t\t\tcredentials = await loginGitHubCopilot({\n\t\t\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase \"google-gemini-cli\":\n\t\t\t\tcredentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tcase \"google-antigravity\":\n\t\t\t\tcredentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tcase \"openai-codex\":\n\t\t\t\tcredentials = await loginOpenAICodex(callbacks);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t\t}\n\n\t\tthis.set(provider, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(provider: string): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(provider);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[provider];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn cred.key;\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\t// Filter to only oauth credentials for getOAuthApiKey\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.data[provider] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\t\tthis.save();\n\t\t\t\t\treturn result.apiKey;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.remove(provider);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(provider);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(provider) ?? undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAQN,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,MAAM,qBAAqB,CAAC;AAK7B,MAAM,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,OAAO,CAAC;CACd,GAAG,gBAAgB,CAAC;AAErB,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE7D;;GAEG;AACH,qBAAa,WAAW;IAKX,OAAO,CAAC,QAAQ;IAJ5B,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,gBAAgB,CAAC,CAA2C;IAEpE,YAAoB,QAAQ,EAAE,MAAM,EAEnC;IAED;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEvD;IAED;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1C;IAED;;;OAGG;IACH,mBAAmB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAE5E;IAED;;OAEG;IACH,MAAM,IAAI,IAAI,CAUb;IAED;;OAEG;IACH,OAAO,CAAC,IAAI;IASZ;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEhD;IAED;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,GAAG,IAAI,CAGtD;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG7B;IAED;;OAEG;IACH,IAAI,IAAI,MAAM,EAAE,CAEf;IAED;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7B;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMjC;IAED;;OAEG;IACH,MAAM,IAAI,eAAe,CAExB;IAED;;OAEG;IACG,KAAK,CACV,QAAQ,EAAE,aAAa,EACvB,SAAS,EAAE;QACV,MAAM,EAAE,CAAC,IAAI,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,YAAY,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAC;QAC/D,QAAQ,EAAE,CAAC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;QACjF,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QACvC,kGAAkG;QAClG,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;QAC1C,8DAA8D;QAC9D,MAAM,CAAC,EAAE,WAAW,CAAC;KACrB,GACC,OAAO,CAAC,IAAI,CAAC,CAqCf;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE7B;YAOa,yBAAyB;IA4EvC;;;;;;;;OAQG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0D7D;CACD","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n *\n * Uses file locking to prevent race conditions when multiple pi instances\n * try to refresh tokens simultaneously.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\tloginOpenAICodex,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname } from \"path\";\nimport lockfile from \"proper-lockfile\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(\n\t\tprovider: OAuthProvider,\n\t\tcallbacks: {\n\t\t\tonAuth: (info: { url: string; instructions?: string }) => void;\n\t\t\tonPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;\n\t\t\tonProgress?: (message: string) => void;\n\t\t\t/** For providers with local callback servers (e.g., openai-codex), races with browser callback */\n\t\t\tonManualCodeInput?: () => Promise<string>;\n\t\t\t/** For cancellation support (e.g., github-copilot polling) */\n\t\t\tsignal?: AbortSignal;\n\t\t},\n\t): Promise<void> {\n\t\tlet credentials: OAuthCredentials;\n\n\t\tswitch (provider) {\n\t\t\tcase \"anthropic\":\n\t\t\t\tcredentials = await loginAnthropic(\n\t\t\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"github-copilot\":\n\t\t\t\tcredentials = await loginGitHubCopilot({\n\t\t\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t\tsignal: callbacks.signal,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase \"google-gemini-cli\":\n\t\t\t\tcredentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);\n\t\t\t\tbreak;\n\t\t\tcase \"google-antigravity\":\n\t\t\t\tcredentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);\n\t\t\t\tbreak;\n\t\t\tcase \"openai-codex\":\n\t\t\t\tcredentials = await loginOpenAICodex({\n\t\t\t\t\tonAuth: callbacks.onAuth,\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t\tonManualCodeInput: callbacks.onManualCodeInput,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t\t}\n\n\t\tthis.set(provider, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Refresh OAuth token with file locking to prevent race conditions.\n\t * Multiple pi instances may try to refresh simultaneously when tokens expire.\n\t * This ensures only one instance refreshes while others wait and use the result.\n\t */\n\tprivate async refreshOAuthTokenWithLock(\n\t\tprovider: OAuthProvider,\n\t): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {\n\t\t// Ensure auth file exists for locking\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tconst dir = dirname(this.authPath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t\t}\n\t\t\twriteFileSync(this.authPath, \"{}\", \"utf-8\");\n\t\t\tchmodSync(this.authPath, 0o600);\n\t\t}\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\n\t\ttry {\n\t\t\t// Acquire exclusive lock with retry and timeout\n\t\t\t// Use generous retry window to handle slow token endpoints\n\t\t\trelease = await lockfile.lock(this.authPath, {\n\t\t\t\tretries: {\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: 100,\n\t\t\t\t\tmaxTimeout: 10000,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tstale: 30000, // Consider lock stale after 30 seconds\n\t\t\t});\n\n\t\t\t// Re-read file after acquiring lock - another instance may have refreshed\n\t\t\tthis.reload();\n\n\t\t\tconst cred = this.data[provider];\n\t\t\tif (cred?.type !== \"oauth\") {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Check if token is still expired after re-reading\n\t\t\t// (another instance may have already refreshed it)\n\t\t\tif (Date.now() < cred.expires) {\n\t\t\t\t// Token is now valid - another instance refreshed it\n\t\t\t\tconst needsProjectId = provider === \"google-gemini-cli\" || provider === \"google-antigravity\";\n\t\t\t\tconst apiKey = needsProjectId\n\t\t\t\t\t? JSON.stringify({ token: cred.access, projectId: cred.projectId })\n\t\t\t\t\t: cred.access;\n\t\t\t\treturn { apiKey, newCredentials: cred };\n\t\t\t}\n\n\t\t\t// Token still expired, we need to refresh\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await getOAuthApiKey(provider, oauthCreds);\n\t\t\tif (result) {\n\t\t\t\tthis.data[provider] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\tthis.save();\n\t\t\t\treturn result;\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} finally {\n\t\t\t// Always release the lock\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore unlock errors (lock may have been compromised)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed with locking)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(provider: string): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(provider);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[provider];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn cred.key;\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\t// Check if token needs refresh\n\t\t\tconst needsRefresh = Date.now() >= cred.expires;\n\n\t\t\tif (needsRefresh) {\n\t\t\t\t// Use locked refresh to prevent race conditions\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.refreshOAuthTokenWithLock(provider as OAuthProvider);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treturn result.apiKey;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Refresh failed - re-read file to check if another instance succeeded\n\t\t\t\t\tthis.reload();\n\t\t\t\t\tconst updatedCred = this.data[provider];\n\n\t\t\t\t\tif (updatedCred?.type === \"oauth\" && Date.now() < updatedCred.expires) {\n\t\t\t\t\t\t// Another instance refreshed successfully, use those credentials\n\t\t\t\t\t\tconst needsProjectId = provider === \"google-gemini-cli\" || provider === \"google-antigravity\";\n\t\t\t\t\t\treturn needsProjectId\n\t\t\t\t\t\t\t? JSON.stringify({ token: updatedCred.access, projectId: updatedCred.projectId })\n\t\t\t\t\t\t\t: updatedCred.access;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Refresh truly failed - DO NOT remove credentials\n\t\t\t\t\t// User can retry or re-authenticate manually\n\t\t\t\t\tconst errorMessage = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`OAuth token refresh failed for ${provider}: ${errorMessage}. ` +\n\t\t\t\t\t\t\t`Please try again or re-authenticate with /login.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Token not expired, use current access token\n\t\t\t\tconst needsProjectId = provider === \"google-gemini-cli\" || provider === \"google-antigravity\";\n\t\t\t\treturn needsProjectId ? JSON.stringify({ token: cred.access, projectId: cred.projectId }) : cred.access;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(provider);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(provider) ?? undefined;\n\t}\n}\n"]}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credential storage for API keys and OAuth tokens.
|
|
3
3
|
* Handles loading, saving, and refreshing credentials from auth.json.
|
|
4
|
+
*
|
|
5
|
+
* Uses file locking to prevent race conditions when multiple pi instances
|
|
6
|
+
* try to refresh tokens simultaneously.
|
|
4
7
|
*/
|
|
5
8
|
import { getEnvApiKey, getOAuthApiKey, loginAnthropic, loginAntigravity, loginGeminiCli, loginGitHubCopilot, loginOpenAICodex, } from "@mariozechner/pi-ai";
|
|
6
9
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
10
|
import { dirname } from "path";
|
|
11
|
+
import lockfile from "proper-lockfile";
|
|
8
12
|
/**
|
|
9
13
|
* Credential storage backed by a JSON file.
|
|
10
14
|
*/
|
|
@@ -130,16 +134,22 @@ export class AuthStorage {
|
|
|
130
134
|
onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),
|
|
131
135
|
onPrompt: callbacks.onPrompt,
|
|
132
136
|
onProgress: callbacks.onProgress,
|
|
137
|
+
signal: callbacks.signal,
|
|
133
138
|
});
|
|
134
139
|
break;
|
|
135
140
|
case "google-gemini-cli":
|
|
136
|
-
credentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);
|
|
141
|
+
credentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);
|
|
137
142
|
break;
|
|
138
143
|
case "google-antigravity":
|
|
139
|
-
credentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);
|
|
144
|
+
credentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);
|
|
140
145
|
break;
|
|
141
146
|
case "openai-codex":
|
|
142
|
-
credentials = await loginOpenAICodex(
|
|
147
|
+
credentials = await loginOpenAICodex({
|
|
148
|
+
onAuth: callbacks.onAuth,
|
|
149
|
+
onPrompt: callbacks.onPrompt,
|
|
150
|
+
onProgress: callbacks.onProgress,
|
|
151
|
+
onManualCodeInput: callbacks.onManualCodeInput,
|
|
152
|
+
});
|
|
143
153
|
break;
|
|
144
154
|
default:
|
|
145
155
|
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
@@ -152,12 +162,84 @@ export class AuthStorage {
|
|
|
152
162
|
logout(provider) {
|
|
153
163
|
this.remove(provider);
|
|
154
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Refresh OAuth token with file locking to prevent race conditions.
|
|
167
|
+
* Multiple pi instances may try to refresh simultaneously when tokens expire.
|
|
168
|
+
* This ensures only one instance refreshes while others wait and use the result.
|
|
169
|
+
*/
|
|
170
|
+
async refreshOAuthTokenWithLock(provider) {
|
|
171
|
+
// Ensure auth file exists for locking
|
|
172
|
+
if (!existsSync(this.authPath)) {
|
|
173
|
+
const dir = dirname(this.authPath);
|
|
174
|
+
if (!existsSync(dir)) {
|
|
175
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
176
|
+
}
|
|
177
|
+
writeFileSync(this.authPath, "{}", "utf-8");
|
|
178
|
+
chmodSync(this.authPath, 0o600);
|
|
179
|
+
}
|
|
180
|
+
let release;
|
|
181
|
+
try {
|
|
182
|
+
// Acquire exclusive lock with retry and timeout
|
|
183
|
+
// Use generous retry window to handle slow token endpoints
|
|
184
|
+
release = await lockfile.lock(this.authPath, {
|
|
185
|
+
retries: {
|
|
186
|
+
retries: 10,
|
|
187
|
+
factor: 2,
|
|
188
|
+
minTimeout: 100,
|
|
189
|
+
maxTimeout: 10000,
|
|
190
|
+
randomize: true,
|
|
191
|
+
},
|
|
192
|
+
stale: 30000, // Consider lock stale after 30 seconds
|
|
193
|
+
});
|
|
194
|
+
// Re-read file after acquiring lock - another instance may have refreshed
|
|
195
|
+
this.reload();
|
|
196
|
+
const cred = this.data[provider];
|
|
197
|
+
if (cred?.type !== "oauth") {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Check if token is still expired after re-reading
|
|
201
|
+
// (another instance may have already refreshed it)
|
|
202
|
+
if (Date.now() < cred.expires) {
|
|
203
|
+
// Token is now valid - another instance refreshed it
|
|
204
|
+
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
|
205
|
+
const apiKey = needsProjectId
|
|
206
|
+
? JSON.stringify({ token: cred.access, projectId: cred.projectId })
|
|
207
|
+
: cred.access;
|
|
208
|
+
return { apiKey, newCredentials: cred };
|
|
209
|
+
}
|
|
210
|
+
// Token still expired, we need to refresh
|
|
211
|
+
const oauthCreds = {};
|
|
212
|
+
for (const [key, value] of Object.entries(this.data)) {
|
|
213
|
+
if (value.type === "oauth") {
|
|
214
|
+
oauthCreds[key] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const result = await getOAuthApiKey(provider, oauthCreds);
|
|
218
|
+
if (result) {
|
|
219
|
+
this.data[provider] = { type: "oauth", ...result.newCredentials };
|
|
220
|
+
this.save();
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
// Always release the lock
|
|
227
|
+
if (release) {
|
|
228
|
+
try {
|
|
229
|
+
await release();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Ignore unlock errors (lock may have been compromised)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
155
237
|
/**
|
|
156
238
|
* Get API key for a provider.
|
|
157
239
|
* Priority:
|
|
158
240
|
* 1. Runtime override (CLI --api-key)
|
|
159
241
|
* 2. API key from auth.json
|
|
160
|
-
* 3. OAuth token from auth.json (auto-refreshed)
|
|
242
|
+
* 3. OAuth token from auth.json (auto-refreshed with locking)
|
|
161
243
|
* 4. Environment variable
|
|
162
244
|
* 5. Fallback resolver (models.json custom providers)
|
|
163
245
|
*/
|
|
@@ -172,23 +254,38 @@ export class AuthStorage {
|
|
|
172
254
|
return cred.key;
|
|
173
255
|
}
|
|
174
256
|
if (cred?.type === "oauth") {
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
257
|
+
// Check if token needs refresh
|
|
258
|
+
const needsRefresh = Date.now() >= cred.expires;
|
|
259
|
+
if (needsRefresh) {
|
|
260
|
+
// Use locked refresh to prevent race conditions
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.refreshOAuthTokenWithLock(provider);
|
|
263
|
+
if (result) {
|
|
264
|
+
return result.apiKey;
|
|
265
|
+
}
|
|
180
266
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
267
|
+
catch (err) {
|
|
268
|
+
// Refresh failed - re-read file to check if another instance succeeded
|
|
269
|
+
this.reload();
|
|
270
|
+
const updatedCred = this.data[provider];
|
|
271
|
+
if (updatedCred?.type === "oauth" && Date.now() < updatedCred.expires) {
|
|
272
|
+
// Another instance refreshed successfully, use those credentials
|
|
273
|
+
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
|
274
|
+
return needsProjectId
|
|
275
|
+
? JSON.stringify({ token: updatedCred.access, projectId: updatedCred.projectId })
|
|
276
|
+
: updatedCred.access;
|
|
277
|
+
}
|
|
278
|
+
// Refresh truly failed - DO NOT remove credentials
|
|
279
|
+
// User can retry or re-authenticate manually
|
|
280
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
281
|
+
throw new Error(`OAuth token refresh failed for ${provider}: ${errorMessage}. ` +
|
|
282
|
+
`Please try again or re-authenticate with /login.`);
|
|
188
283
|
}
|
|
189
284
|
}
|
|
190
|
-
|
|
191
|
-
|
|
285
|
+
else {
|
|
286
|
+
// Token not expired, use current access token
|
|
287
|
+
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
|
288
|
+
return needsProjectId ? JSON.stringify({ token: cred.access, projectId: cred.projectId }) : cred.access;
|
|
192
289
|
}
|
|
193
290
|
}
|
|
194
291
|
// Fall back to environment variable
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-storage.js","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\tloginOpenAICodex,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname } from \"path\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(\n\t\tprovider: OAuthProvider,\n\t\tcallbacks: {\n\t\t\tonAuth: (info: { url: string; instructions?: string }) => void;\n\t\t\tonPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;\n\t\t\tonProgress?: (message: string) => void;\n\t\t},\n\t): Promise<void> {\n\t\tlet credentials: OAuthCredentials;\n\n\t\tswitch (provider) {\n\t\t\tcase \"anthropic\":\n\t\t\t\tcredentials = await loginAnthropic(\n\t\t\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"github-copilot\":\n\t\t\t\tcredentials = await loginGitHubCopilot({\n\t\t\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase \"google-gemini-cli\":\n\t\t\t\tcredentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tcase \"google-antigravity\":\n\t\t\t\tcredentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);\n\t\t\t\tbreak;\n\t\t\tcase \"openai-codex\":\n\t\t\t\tcredentials = await loginOpenAICodex(callbacks);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t\t}\n\n\t\tthis.set(provider, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(provider: string): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(provider);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[provider];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn cred.key;\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\t// Filter to only oauth credentials for getOAuthApiKey\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.data[provider] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\t\tthis.save();\n\t\t\t\t\treturn result.apiKey;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.remove(provider);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(provider);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(provider) ?? undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-storage.js","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n *\n * Uses file locking to prevent race conditions when multiple pi instances\n * try to refresh tokens simultaneously.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tloginAnthropic,\n\tloginAntigravity,\n\tloginGeminiCli,\n\tloginGitHubCopilot,\n\tloginOpenAICodex,\n\ttype OAuthCredentials,\n\ttype OAuthProvider,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname } from \"path\";\nimport lockfile from \"proper-lockfile\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(\n\t\tprovider: OAuthProvider,\n\t\tcallbacks: {\n\t\t\tonAuth: (info: { url: string; instructions?: string }) => void;\n\t\t\tonPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;\n\t\t\tonProgress?: (message: string) => void;\n\t\t\t/** For providers with local callback servers (e.g., openai-codex), races with browser callback */\n\t\t\tonManualCodeInput?: () => Promise<string>;\n\t\t\t/** For cancellation support (e.g., github-copilot polling) */\n\t\t\tsignal?: AbortSignal;\n\t\t},\n\t): Promise<void> {\n\t\tlet credentials: OAuthCredentials;\n\n\t\tswitch (provider) {\n\t\t\tcase \"anthropic\":\n\t\t\t\tcredentials = await loginAnthropic(\n\t\t\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"github-copilot\":\n\t\t\t\tcredentials = await loginGitHubCopilot({\n\t\t\t\t\tonAuth: (url, instructions) => callbacks.onAuth({ url, instructions }),\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t\tsignal: callbacks.signal,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase \"google-gemini-cli\":\n\t\t\t\tcredentials = await loginGeminiCli(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);\n\t\t\t\tbreak;\n\t\t\tcase \"google-antigravity\":\n\t\t\t\tcredentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress, callbacks.onManualCodeInput);\n\t\t\t\tbreak;\n\t\t\tcase \"openai-codex\":\n\t\t\t\tcredentials = await loginOpenAICodex({\n\t\t\t\t\tonAuth: callbacks.onAuth,\n\t\t\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\t\t\tonProgress: callbacks.onProgress,\n\t\t\t\t\tonManualCodeInput: callbacks.onManualCodeInput,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`Unknown OAuth provider: ${provider}`);\n\t\t}\n\n\t\tthis.set(provider, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Refresh OAuth token with file locking to prevent race conditions.\n\t * Multiple pi instances may try to refresh simultaneously when tokens expire.\n\t * This ensures only one instance refreshes while others wait and use the result.\n\t */\n\tprivate async refreshOAuthTokenWithLock(\n\t\tprovider: OAuthProvider,\n\t): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {\n\t\t// Ensure auth file exists for locking\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tconst dir = dirname(this.authPath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t\t}\n\t\t\twriteFileSync(this.authPath, \"{}\", \"utf-8\");\n\t\t\tchmodSync(this.authPath, 0o600);\n\t\t}\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\n\t\ttry {\n\t\t\t// Acquire exclusive lock with retry and timeout\n\t\t\t// Use generous retry window to handle slow token endpoints\n\t\t\trelease = await lockfile.lock(this.authPath, {\n\t\t\t\tretries: {\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: 100,\n\t\t\t\t\tmaxTimeout: 10000,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tstale: 30000, // Consider lock stale after 30 seconds\n\t\t\t});\n\n\t\t\t// Re-read file after acquiring lock - another instance may have refreshed\n\t\t\tthis.reload();\n\n\t\t\tconst cred = this.data[provider];\n\t\t\tif (cred?.type !== \"oauth\") {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Check if token is still expired after re-reading\n\t\t\t// (another instance may have already refreshed it)\n\t\t\tif (Date.now() < cred.expires) {\n\t\t\t\t// Token is now valid - another instance refreshed it\n\t\t\t\tconst needsProjectId = provider === \"google-gemini-cli\" || provider === \"google-antigravity\";\n\t\t\t\tconst apiKey = needsProjectId\n\t\t\t\t\t? JSON.stringify({ token: cred.access, projectId: cred.projectId })\n\t\t\t\t\t: cred.access;\n\t\t\t\treturn { apiKey, newCredentials: cred };\n\t\t\t}\n\n\t\t\t// Token still expired, we need to refresh\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await getOAuthApiKey(provider, oauthCreds);\n\t\t\tif (result) {\n\t\t\t\tthis.data[provider] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\tthis.save();\n\t\t\t\treturn result;\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} finally {\n\t\t\t// Always release the lock\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore unlock errors (lock may have been compromised)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed with locking)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(provider: string): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(provider);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[provider];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn cred.key;\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\t// Check if token needs refresh\n\t\t\tconst needsRefresh = Date.now() >= cred.expires;\n\n\t\t\tif (needsRefresh) {\n\t\t\t\t// Use locked refresh to prevent race conditions\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.refreshOAuthTokenWithLock(provider as OAuthProvider);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treturn result.apiKey;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Refresh failed - re-read file to check if another instance succeeded\n\t\t\t\t\tthis.reload();\n\t\t\t\t\tconst updatedCred = this.data[provider];\n\n\t\t\t\t\tif (updatedCred?.type === \"oauth\" && Date.now() < updatedCred.expires) {\n\t\t\t\t\t\t// Another instance refreshed successfully, use those credentials\n\t\t\t\t\t\tconst needsProjectId = provider === \"google-gemini-cli\" || provider === \"google-antigravity\";\n\t\t\t\t\t\treturn needsProjectId\n\t\t\t\t\t\t\t? JSON.stringify({ token: updatedCred.access, projectId: updatedCred.projectId })\n\t\t\t\t\t\t\t: updatedCred.access;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Refresh truly failed - DO NOT remove credentials\n\t\t\t\t\t// User can retry or re-authenticate manually\n\t\t\t\t\tconst errorMessage = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`OAuth token refresh failed for ${provider}: ${errorMessage}. ` +\n\t\t\t\t\t\t\t`Please try again or re-authenticate with /login.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Token not expired, use current access token\n\t\t\t\tconst needsProjectId = provider === \"google-gemini-cli\" || provider === \"google-antigravity\";\n\t\t\t\treturn needsProjectId ? JSON.stringify({ token: cred.access, projectId: cred.projectId }) : cred.access;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(provider);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(provider) ?? undefined;\n\t}\n}\n"]}
|
|
@@ -280,10 +280,65 @@
|
|
|
280
280
|
color: var(--userMessageText);
|
|
281
281
|
padding: var(--line-height);
|
|
282
282
|
border-radius: 4px;
|
|
283
|
+
position: relative;
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
.assistant-message {
|
|
286
287
|
padding: 0;
|
|
288
|
+
position: relative;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Copy link button - appears on hover */
|
|
292
|
+
.copy-link-btn {
|
|
293
|
+
position: absolute;
|
|
294
|
+
top: 8px;
|
|
295
|
+
right: 8px;
|
|
296
|
+
width: 28px;
|
|
297
|
+
height: 28px;
|
|
298
|
+
padding: 6px;
|
|
299
|
+
background: var(--container-bg);
|
|
300
|
+
border: 1px solid var(--dim);
|
|
301
|
+
border-radius: 4px;
|
|
302
|
+
color: var(--muted);
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
opacity: 0;
|
|
305
|
+
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
|
306
|
+
display: flex;
|
|
307
|
+
align-items: center;
|
|
308
|
+
justify-content: center;
|
|
309
|
+
z-index: 10;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.user-message:hover .copy-link-btn,
|
|
313
|
+
.assistant-message:hover .copy-link-btn {
|
|
314
|
+
opacity: 1;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.copy-link-btn:hover {
|
|
318
|
+
background: var(--accent);
|
|
319
|
+
color: var(--body-bg);
|
|
320
|
+
border-color: var(--accent);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.copy-link-btn.copied {
|
|
324
|
+
background: var(--success, #22c55e);
|
|
325
|
+
color: white;
|
|
326
|
+
border-color: var(--success, #22c55e);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Highlight effect for deep-linked messages */
|
|
330
|
+
.user-message.highlight,
|
|
331
|
+
.assistant-message.highlight {
|
|
332
|
+
animation: highlight-pulse 2s ease-out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@keyframes highlight-pulse {
|
|
336
|
+
0% {
|
|
337
|
+
box-shadow: 0 0 0 3px var(--accent);
|
|
338
|
+
}
|
|
339
|
+
100% {
|
|
340
|
+
box-shadow: 0 0 0 0 transparent;
|
|
341
|
+
}
|
|
287
342
|
}
|
|
288
343
|
|
|
289
344
|
.assistant-message > .message-timestamp {
|
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
bytes[i] = binary.charCodeAt(i);
|
|
13
13
|
}
|
|
14
14
|
const data = JSON.parse(new TextDecoder('utf-8').decode(bytes));
|
|
15
|
-
const { header, entries, leafId, systemPrompt, tools } = data;
|
|
15
|
+
const { header, entries, leafId: defaultLeafId, systemPrompt, tools } = data;
|
|
16
|
+
|
|
17
|
+
// ============================================================
|
|
18
|
+
// URL PARAMETER HANDLING
|
|
19
|
+
// ============================================================
|
|
20
|
+
|
|
21
|
+
// Parse URL parameters for deep linking: leafId and targetId
|
|
22
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
23
|
+
const urlLeafId = urlParams.get('leafId');
|
|
24
|
+
const urlTargetId = urlParams.get('targetId');
|
|
25
|
+
// Use URL leafId if provided, otherwise fall back to session default
|
|
26
|
+
const leafId = urlLeafId || defaultLeafId;
|
|
16
27
|
|
|
17
28
|
// ============================================================
|
|
18
29
|
// DATA STRUCTURES
|
|
@@ -777,16 +788,99 @@
|
|
|
777
788
|
return html;
|
|
778
789
|
}
|
|
779
790
|
|
|
791
|
+
/**
|
|
792
|
+
* Build a shareable URL for a specific message.
|
|
793
|
+
* URL format: base?gistId&leafId=<leafId>&targetId=<entryId>
|
|
794
|
+
*/
|
|
795
|
+
function buildShareUrl(entryId) {
|
|
796
|
+
// Check for injected base URL (used when loaded in iframe via srcdoc)
|
|
797
|
+
const baseUrlMeta = document.querySelector('meta[name="pi-share-base-url"]');
|
|
798
|
+
const baseUrl = baseUrlMeta ? baseUrlMeta.content : window.location.href.split('?')[0];
|
|
799
|
+
|
|
800
|
+
const url = new URL(window.location.href);
|
|
801
|
+
// Find the gist ID (first query param without value, e.g., ?abc123)
|
|
802
|
+
const gistId = Array.from(url.searchParams.keys()).find(k => !url.searchParams.get(k));
|
|
803
|
+
|
|
804
|
+
// Build the share URL
|
|
805
|
+
const params = new URLSearchParams();
|
|
806
|
+
params.set('leafId', currentLeafId);
|
|
807
|
+
params.set('targetId', entryId);
|
|
808
|
+
|
|
809
|
+
// If we have an injected base URL (iframe context), use it directly
|
|
810
|
+
if (baseUrlMeta) {
|
|
811
|
+
return `${baseUrl}&${params.toString()}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Otherwise build from current location (direct file access)
|
|
815
|
+
url.search = gistId ? `?${gistId}&${params.toString()}` : `?${params.toString()}`;
|
|
816
|
+
return url.toString();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Copy text to clipboard with visual feedback.
|
|
821
|
+
* Uses navigator.clipboard with fallback to execCommand for HTTP contexts.
|
|
822
|
+
*/
|
|
823
|
+
async function copyToClipboard(text, button) {
|
|
824
|
+
let success = false;
|
|
825
|
+
try {
|
|
826
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
827
|
+
await navigator.clipboard.writeText(text);
|
|
828
|
+
success = true;
|
|
829
|
+
}
|
|
830
|
+
} catch (err) {
|
|
831
|
+
// Clipboard API failed, try fallback
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Fallback for HTTP or when Clipboard API is unavailable
|
|
835
|
+
if (!success) {
|
|
836
|
+
try {
|
|
837
|
+
const textarea = document.createElement('textarea');
|
|
838
|
+
textarea.value = text;
|
|
839
|
+
textarea.style.position = 'fixed';
|
|
840
|
+
textarea.style.opacity = '0';
|
|
841
|
+
document.body.appendChild(textarea);
|
|
842
|
+
textarea.select();
|
|
843
|
+
success = document.execCommand('copy');
|
|
844
|
+
document.body.removeChild(textarea);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
console.error('Failed to copy:', err);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (success && button) {
|
|
851
|
+
const originalHtml = button.innerHTML;
|
|
852
|
+
button.innerHTML = '✓';
|
|
853
|
+
button.classList.add('copied');
|
|
854
|
+
setTimeout(() => {
|
|
855
|
+
button.innerHTML = originalHtml;
|
|
856
|
+
button.classList.remove('copied');
|
|
857
|
+
}, 1500);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Render the copy-link button HTML for a message.
|
|
863
|
+
*/
|
|
864
|
+
function renderCopyLinkButton(entryId) {
|
|
865
|
+
return `<button class="copy-link-btn" data-entry-id="${entryId}" title="Copy link to this message">
|
|
866
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
867
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
868
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
869
|
+
</svg>
|
|
870
|
+
</button>`;
|
|
871
|
+
}
|
|
872
|
+
|
|
780
873
|
function renderEntry(entry) {
|
|
781
874
|
const ts = formatTimestamp(entry.timestamp);
|
|
782
875
|
const tsHtml = ts ? `<div class="message-timestamp">${ts}</div>` : '';
|
|
783
876
|
const entryId = `entry-${entry.id}`;
|
|
877
|
+
const copyBtnHtml = renderCopyLinkButton(entry.id);
|
|
784
878
|
|
|
785
879
|
if (entry.type === 'message') {
|
|
786
880
|
const msg = entry.message;
|
|
787
881
|
|
|
788
882
|
if (msg.role === 'user') {
|
|
789
|
-
let html = `<div class="user-message" id="${entryId}">${tsHtml}`;
|
|
883
|
+
let html = `<div class="user-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
790
884
|
const content = msg.content;
|
|
791
885
|
|
|
792
886
|
if (Array.isArray(content)) {
|
|
@@ -810,7 +904,7 @@
|
|
|
810
904
|
}
|
|
811
905
|
|
|
812
906
|
if (msg.role === 'assistant') {
|
|
813
|
-
let html = `<div class="assistant-message" id="${entryId}">${tsHtml}`;
|
|
907
|
+
let html = `<div class="assistant-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
|
814
908
|
|
|
815
909
|
for (const block of msg.content) {
|
|
816
910
|
if (block.type === 'text' && block.text.trim()) {
|
|
@@ -1023,7 +1117,7 @@
|
|
|
1023
1117
|
return node;
|
|
1024
1118
|
}
|
|
1025
1119
|
|
|
1026
|
-
function navigateTo(targetId, scrollMode = 'target') {
|
|
1120
|
+
function navigateTo(targetId, scrollMode = 'target', scrollToEntryId = null) {
|
|
1027
1121
|
currentLeafId = targetId;
|
|
1028
1122
|
const path = getPath(targetId);
|
|
1029
1123
|
|
|
@@ -1045,15 +1139,32 @@
|
|
|
1045
1139
|
messagesEl.innerHTML = '';
|
|
1046
1140
|
messagesEl.appendChild(fragment);
|
|
1047
1141
|
|
|
1142
|
+
// Attach click handlers for copy-link buttons
|
|
1143
|
+
messagesEl.querySelectorAll('.copy-link-btn').forEach(btn => {
|
|
1144
|
+
btn.addEventListener('click', (e) => {
|
|
1145
|
+
e.stopPropagation();
|
|
1146
|
+
const entryId = btn.dataset.entryId;
|
|
1147
|
+
const shareUrl = buildShareUrl(entryId);
|
|
1148
|
+
copyToClipboard(shareUrl, btn);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1048
1152
|
// Use setTimeout(0) to ensure DOM is fully laid out before scrolling
|
|
1049
1153
|
setTimeout(() => {
|
|
1050
1154
|
const content = document.getElementById('content');
|
|
1051
1155
|
if (scrollMode === 'bottom') {
|
|
1052
1156
|
content.scrollTop = content.scrollHeight;
|
|
1053
1157
|
} else if (scrollMode === 'target') {
|
|
1054
|
-
|
|
1158
|
+
// If scrollToEntryId is provided, scroll to that specific entry
|
|
1159
|
+
const scrollTargetId = scrollToEntryId || targetId;
|
|
1160
|
+
const targetEl = document.getElementById(`entry-${scrollTargetId}`);
|
|
1055
1161
|
if (targetEl) {
|
|
1056
1162
|
targetEl.scrollIntoView({ block: 'center' });
|
|
1163
|
+
// Briefly highlight the target message
|
|
1164
|
+
if (scrollToEntryId) {
|
|
1165
|
+
targetEl.classList.add('highlight');
|
|
1166
|
+
setTimeout(() => targetEl.classList.remove('highlight'), 2000);
|
|
1167
|
+
}
|
|
1057
1168
|
}
|
|
1058
1169
|
}
|
|
1059
1170
|
}, 0);
|
|
@@ -1188,9 +1299,15 @@
|
|
|
1188
1299
|
}
|
|
1189
1300
|
});
|
|
1190
1301
|
|
|
1191
|
-
// Initial render
|
|
1302
|
+
// Initial render
|
|
1303
|
+
// If URL has targetId, scroll to that specific message; otherwise stay at top
|
|
1192
1304
|
if (leafId) {
|
|
1193
|
-
|
|
1305
|
+
if (urlTargetId && byId.has(urlTargetId)) {
|
|
1306
|
+
// Deep link: navigate to leaf and scroll to target message
|
|
1307
|
+
navigateTo(leafId, 'target', urlTargetId);
|
|
1308
|
+
} else {
|
|
1309
|
+
navigateTo(leafId, 'none');
|
|
1310
|
+
}
|
|
1194
1311
|
} else if (entries.length > 0) {
|
|
1195
1312
|
// Fallback: use last entry if no leafId
|
|
1196
1313
|
navigateTo(entries[entries.length - 1].id, 'none');
|