@oh-my-pi/pi-coding-agent 16.1.2 → 16.1.3

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 (48) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/cli.js +3046 -3047
  3. package/dist/types/config/model-resolver.d.ts +3 -3
  4. package/dist/types/mnemopi/embed-client.d.ts +70 -0
  5. package/dist/types/mnemopi/embed-protocol.d.ts +52 -0
  6. package/dist/types/mnemopi/embed-worker.d.ts +12 -0
  7. package/dist/types/mnemopi/state.d.ts +9 -1
  8. package/dist/types/session/agent-storage.d.ts +2 -0
  9. package/dist/types/session/auth-broker-config.d.ts +3 -2
  10. package/dist/types/session/history-storage.d.ts +1 -1
  11. package/dist/types/tools/image-gen.d.ts +2 -2
  12. package/dist/types/utils/image-loading.d.ts +1 -1
  13. package/dist/types/utils/ipc.d.ts +22 -0
  14. package/dist/types/web/search/providers/perplexity-auth.d.ts +37 -0
  15. package/package.json +12 -12
  16. package/src/cli.ts +8 -0
  17. package/src/commands/token.ts +52 -33
  18. package/src/config/append-only-context-mode.ts +45 -0
  19. package/src/config/model-discovery.ts +3 -0
  20. package/src/config/model-registry.ts +21 -3
  21. package/src/config/model-resolver.ts +31 -8
  22. package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
  23. package/src/lsp/client.ts +24 -0
  24. package/src/mnemopi/backend.ts +49 -3
  25. package/src/mnemopi/embed-client.ts +401 -0
  26. package/src/mnemopi/embed-protocol.ts +35 -0
  27. package/src/mnemopi/embed-worker.ts +113 -0
  28. package/src/mnemopi/state.ts +29 -1
  29. package/src/modes/components/custom-editor.ts +1 -1
  30. package/src/modes/components/model-selector.ts +2 -2
  31. package/src/modes/components/welcome.ts +1 -1
  32. package/src/modes/controllers/event-controller.ts +8 -0
  33. package/src/modes/controllers/selector-controller.ts +2 -2
  34. package/src/modes/theme/theme.ts +69 -0
  35. package/src/sdk.ts +4 -0
  36. package/src/session/agent-session.ts +8 -0
  37. package/src/session/agent-storage.ts +14 -0
  38. package/src/session/auth-broker-config.ts +2 -1
  39. package/src/session/history-storage.ts +13 -1
  40. package/src/stt/asr-client.ts +2 -7
  41. package/src/tiny/title-client.ts +2 -7
  42. package/src/tools/image-gen.ts +4 -8
  43. package/src/tools/render-utils.ts +4 -1
  44. package/src/tts/tts-client.ts +2 -7
  45. package/src/utils/image-loading.ts +12 -2
  46. package/src/utils/ipc.ts +38 -0
  47. package/src/web/search/providers/perplexity-auth.ts +133 -0
  48. package/src/web/search/providers/perplexity.ts +2 -125
@@ -0,0 +1,133 @@
1
+ import type { AuthStorage, OAuthAccess } from "@oh-my-pi/pi-ai";
2
+ import { $env } from "@oh-my-pi/pi-utils";
3
+
4
+ export const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
5
+ export const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
6
+ export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
7
+ export const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
8
+
9
+ export interface ApiConfig {
10
+ type: "api_key";
11
+ apiKey: string;
12
+ provider: "perplexity" | "openrouter";
13
+ chatBaseUrl: string;
14
+ responsesBaseUrl: string;
15
+ modelPrefix: string;
16
+ useResponses: boolean;
17
+ }
18
+
19
+ export type PerplexityAuth =
20
+ | ApiConfig
21
+ | {
22
+ type: "oauth";
23
+ access: OAuthAccess;
24
+ }
25
+ | {
26
+ type: "cookies";
27
+ cookies: string;
28
+ }
29
+ | {
30
+ type: "anonymous";
31
+ };
32
+
33
+ export interface PerplexityAuthOptions {
34
+ signal?: AbortSignal;
35
+ forceRefresh?: boolean;
36
+ }
37
+
38
+ /** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
39
+ export async function getApiConfigs(
40
+ authStorage: AuthStorage,
41
+ sessionId: string | undefined,
42
+ options?: PerplexityAuthOptions,
43
+ ): Promise<ApiConfig[]> {
44
+ const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
45
+ const configs: ApiConfig[] = [];
46
+
47
+ const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, options);
48
+ if (perplexityKey) {
49
+ configs.push({
50
+ type: "api_key",
51
+ apiKey: perplexityKey,
52
+ provider: "perplexity",
53
+ chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
54
+ responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
55
+ modelPrefix: "",
56
+ useResponses,
57
+ });
58
+ }
59
+
60
+ const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, options);
61
+ if (openrouterKey) {
62
+ configs.push({
63
+ type: "api_key",
64
+ apiKey: openrouterKey,
65
+ provider: "openrouter",
66
+ chatBaseUrl: OPENROUTER_BASE_URL,
67
+ responsesBaseUrl: OPENROUTER_BASE_URL,
68
+ modelPrefix: "perplexity/",
69
+ useResponses,
70
+ });
71
+ }
72
+
73
+ return configs;
74
+ }
75
+
76
+ /**
77
+ * Decode a Perplexity JWT's `exp` claim, in ms. Returns `undefined` when the
78
+ * token has no `exp` (which is the common case — Perplexity sessions are
79
+ * server-side and effectively non-expiring from the client's POV).
80
+ */
81
+ export function jwtExpiryMs(token: string): number | undefined {
82
+ const parts = token.split(".");
83
+ if (parts.length !== 3) return undefined;
84
+ const payload = parts[1];
85
+ if (!payload) return undefined;
86
+ try {
87
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { exp?: unknown };
88
+ if (typeof decoded.exp !== "number" || !Number.isFinite(decoded.exp)) return undefined;
89
+ return decoded.exp * 1000;
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ /** Collect all available auth methods to try in priority order */
96
+ export async function getAvailableAuthMethods(
97
+ authStorage: AuthStorage,
98
+ sessionId: string | undefined,
99
+ options?: PerplexityAuthOptions,
100
+ ): Promise<PerplexityAuth[]> {
101
+ const methods: PerplexityAuth[] = [];
102
+
103
+ // 1. Cookies take precedence over OAuth as noted in comments/docs
104
+ const cookies = $env.PERPLEXITY_COOKIES?.trim();
105
+ if (cookies) {
106
+ methods.push({ type: "cookies", cookies });
107
+ }
108
+
109
+ // 2. Perplexity OAuth (session bearer)
110
+ try {
111
+ const access = await authStorage.getOAuthAccess("perplexity", sessionId, options);
112
+ const token = access?.accessToken;
113
+ if (access && token) {
114
+ const jwtExpiry = jwtExpiryMs(token);
115
+ if (jwtExpiry === undefined || jwtExpiry > Date.now() + OAUTH_EXPIRY_BUFFER_MS) {
116
+ methods.push({ type: "oauth", access });
117
+ }
118
+ }
119
+ } catch {
120
+ // ignored
121
+ }
122
+
123
+ // 3. API key configs (direct, then openrouter)
124
+ const apiConfigs = await getApiConfigs(authStorage, sessionId, options);
125
+ methods.push(...apiConfigs);
126
+
127
+ // 4. Fallback to Perplexity free (anonymous)
128
+ if (methods.length === 0) {
129
+ methods.push({ type: "anonymous" });
130
+ }
131
+
132
+ return methods;
133
+ }
@@ -14,7 +14,6 @@ import {
14
14
  type AuthStorage,
15
15
  type Context,
16
16
  type FetchImpl,
17
- type OAuthAccess,
18
17
  type Usage,
19
18
  withOAuthAccess,
20
19
  } from "@oh-my-pi/pi-ai";
@@ -34,36 +33,19 @@ import { SearchProviderError } from "../../../web/search/types";
34
33
  import { dateToAgeSeconds } from "../utils";
35
34
  import type { SearchParams } from "./base";
36
35
  import { SearchProvider } from "./base";
36
+ import { type ApiConfig, getAvailableAuthMethods } from "./perplexity-auth";
37
37
  import { classifyProviderHttpError, withHardTimeout } from "./utils";
38
38
 
39
- const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
40
- const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
41
- const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
42
39
  const PERPLEXITY_OAUTH_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask";
43
40
 
44
41
  const DEFAULT_MAX_TOKENS = 8192;
45
42
  const DEFAULT_TEMPERATURE = 0.2;
46
43
  const DEFAULT_NUM_SEARCH_RESULTS = 20;
47
- const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
48
44
  const OAUTH_API_VERSION = "2.18";
49
45
  const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
50
46
  const ANONYMOUS_USER_AGENT =
51
47
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
52
48
 
53
- type PerplexityAuth =
54
- | ApiConfig
55
- | {
56
- type: "oauth";
57
- access: OAuthAccess;
58
- }
59
- | {
60
- type: "cookies";
61
- cookies: string;
62
- }
63
- | {
64
- type: "anonymous";
65
- };
66
-
67
49
  interface PerplexityOAuthStreamMarkdownBlock {
68
50
  answer?: string;
69
51
  chunks?: string[];
@@ -289,111 +271,6 @@ export interface PerplexitySearchParams {
289
271
  fetch?: FetchImpl;
290
272
  }
291
273
 
292
- interface ApiConfig {
293
- type: "api_key";
294
- apiKey: string;
295
- provider: "perplexity" | "openrouter";
296
- chatBaseUrl: string;
297
- responsesBaseUrl: string;
298
- modelPrefix: string;
299
- useResponses: boolean;
300
- }
301
-
302
- /** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
303
- async function getApiConfigs(
304
- authStorage: AuthStorage,
305
- sessionId: string | undefined,
306
- signal: AbortSignal | undefined,
307
- ): Promise<ApiConfig[]> {
308
- const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
309
- const configs: ApiConfig[] = [];
310
-
311
- const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, { signal });
312
- if (perplexityKey) {
313
- configs.push({
314
- type: "api_key",
315
- apiKey: perplexityKey,
316
- provider: "perplexity",
317
- chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
318
- responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
319
- modelPrefix: "",
320
- useResponses,
321
- });
322
- }
323
-
324
- const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, { signal });
325
- if (openrouterKey) {
326
- configs.push({
327
- type: "api_key",
328
- apiKey: openrouterKey,
329
- provider: "openrouter",
330
- chatBaseUrl: OPENROUTER_BASE_URL,
331
- responsesBaseUrl: OPENROUTER_BASE_URL,
332
- modelPrefix: "perplexity/",
333
- useResponses,
334
- });
335
- }
336
-
337
- return configs;
338
- }
339
-
340
- /**
341
- * Decode a Perplexity JWT's `exp` claim, in ms. Returns `undefined` when the
342
- * token has no `exp` (which is the common case — Perplexity sessions are
343
- * server-side and effectively non-expiring from the client's POV).
344
- */
345
- function jwtExpiryMs(token: string): number | undefined {
346
- const parts = token.split(".");
347
- if (parts.length !== 3) return undefined;
348
- const payload = parts[1];
349
- if (!payload) return undefined;
350
- try {
351
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as { exp?: unknown };
352
- if (typeof decoded.exp !== "number" || !Number.isFinite(decoded.exp)) return undefined;
353
- return decoded.exp * 1000;
354
- } catch {
355
- return undefined;
356
- }
357
- }
358
-
359
- /** Collect all available auth methods to try in priority order */
360
- async function getAvailableAuthMethods(
361
- authStorage: AuthStorage,
362
- sessionId: string | undefined,
363
- signal: AbortSignal | undefined,
364
- ): Promise<PerplexityAuth[]> {
365
- const methods: PerplexityAuth[] = [];
366
-
367
- // 1. Perplexity OAuth & Cookies (same priority - highest)
368
- try {
369
- const access = await authStorage.getOAuthAccess("perplexity", sessionId, { signal });
370
- const token = access?.accessToken;
371
- if (access && token) {
372
- const jwtExpiry = jwtExpiryMs(token);
373
- if (jwtExpiry === undefined || jwtExpiry > Date.now() + OAUTH_EXPIRY_BUFFER_MS) {
374
- methods.push({ type: "oauth", access });
375
- }
376
- }
377
- } catch {
378
- // ignored
379
- }
380
-
381
- const cookies = $env.PERPLEXITY_COOKIES?.trim();
382
- if (cookies) {
383
- methods.push({ type: "cookies", cookies });
384
- }
385
-
386
- const apiConfigs = await getApiConfigs(authStorage, sessionId, signal);
387
- methods.push(...apiConfigs);
388
-
389
- // 5. Fallback to Perplexity free (anonymous)
390
- if (methods.length === 0) {
391
- methods.push({ type: "anonymous" });
392
- }
393
-
394
- return methods;
395
- }
396
-
397
274
  interface PerplexityApiStreamMetadata {
398
275
  id?: string;
399
276
  model?: string;
@@ -904,7 +781,7 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
904
781
  request.search_recency_filter = params.search_recency_filter;
905
782
  }
906
783
 
907
- const authMethods = await getAvailableAuthMethods(params.authStorage, params.sessionId, params.signal);
784
+ const authMethods = await getAvailableAuthMethods(params.authStorage, params.sessionId, { signal: params.signal });
908
785
  let lastError: unknown;
909
786
 
910
787
  for (const auth of authMethods) {