@getpaseo/server 0.11.0-beta.1 → 0.11.0-beta.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 (56) hide show
  1. package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -3
  2. package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +131 -0
  3. package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +102 -50
  4. package/dist/server/builtin-plugins/claude-usage-source/shared/input.ts +13 -1
  5. package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +2 -3
  6. package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +131 -0
  7. package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +70 -30
  8. package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +11 -1
  9. package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
  10. package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +28 -39
  11. package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +6 -2
  12. package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
  13. package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
  14. package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
  15. package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
  16. package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
  17. package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
  18. package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
  19. package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
  20. package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
  21. package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
  22. package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
  23. package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
  24. package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
  25. package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
  26. package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
  27. package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
  28. package/dist/server/builtin-plugins/package.json +1 -1
  29. package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
  30. package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
  31. package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
  32. package/dist/server/server/orchestration-skills/internal/operations.js +19 -5
  33. package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
  34. package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
  35. package/dist/server/server/plugins/builtin/index.js +6 -0
  36. package/dist/server/server/plugins/compiler.js +4 -0
  37. package/dist/server/server/plugins/index.d.ts +18 -3
  38. package/dist/server/server/plugins/index.js +1 -2
  39. package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
  40. package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
  41. package/dist/server/server/plugins/plugin-process.js +2 -7
  42. package/dist/server/server/plugins/runtime.d.ts +0 -1
  43. package/dist/server/server/plugins/runtime.js +0 -11
  44. package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
  45. package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
  46. package/dist/server/server/plugins/usage-sources/index.d.ts +7 -9
  47. package/dist/server/server/plugins/usage-sources/index.js +98 -71
  48. package/dist/server/web-ui/_expo/static/js/web/{index-87a07caf197cc320e593332ed2d5e20f.js → index-1a3199cb049d45e47bfd4c216ce98838.js} +13 -13
  49. package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.br +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.gz +0 -0
  51. package/dist/server/web-ui/index.html +1 -1
  52. package/dist/server/web-ui/index.html.br +0 -0
  53. package/dist/server/web-ui/index.html.gz +0 -0
  54. package/package.json +6 -6
  55. package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.br +0 -0
  56. package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.gz +0 -0
@@ -1,6 +1,6 @@
1
1
  import type { PluginServerContext } from "@getpaseo/plugin/server";
2
2
  import { inputSchema } from "./shared/input.js";
3
- import { fetchUsage, identify } from "./server/usage.js";
3
+ import { discover, fetchUsage } from "./server/usage.js";
4
4
 
5
5
  export default function contribute(server: PluginServerContext) {
6
6
  server.registerUsageSource({
@@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) {
8
8
  label: "Claude",
9
9
  icon: "icon.svg",
10
10
  input: inputSchema,
11
- discover: async () => [{}],
12
- identify,
11
+ discover: () => discover(),
13
12
  fetch: fetchUsage,
14
13
  });
15
14
  return () => {};
@@ -0,0 +1,131 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { z } from "zod";
6
+ import { routeSchema } from "../shared/input.js";
7
+
8
+ export interface StoreLookup {
9
+ home?: string;
10
+ env?: NodeJS.ProcessEnv;
11
+ platform?: NodeJS.Platform;
12
+ now?: () => number;
13
+ sqlite?: () => Sqlite | undefined;
14
+ }
15
+ interface Database {
16
+ prepare(sql: string): { all(...args: string[]): unknown[] };
17
+ close(): void;
18
+ }
19
+ export interface Sqlite {
20
+ DatabaseSync: new (path: string, options: { readOnly: boolean }) => Database;
21
+ }
22
+ export const oauthSchema = z.object({
23
+ access: z.string().min(1),
24
+ accountId: z.string().optional(),
25
+ expires: z.number().optional(),
26
+ });
27
+ const rowSchema = z.object({ id: z.number().int().positive(), data: z.string() });
28
+ export type Route = z.infer<typeof routeSchema>;
29
+
30
+ export async function readJson(path: string): Promise<unknown> {
31
+ try {
32
+ return JSON.parse(await readFile(path, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export function piAuthPath(lookup: StoreLookup): string {
39
+ const home = lookup.home ?? homedir();
40
+ const env = lookup.env ?? process.env;
41
+ const configured = env.PI_CODING_AGENT_DIR?.trim();
42
+ if (!configured) return join(home, ".pi", "agent", "auth.json");
43
+ if (configured === "~") return join(home, "auth.json");
44
+ const directory = configured.startsWith("~/")
45
+ ? join(home, configured.slice(2))
46
+ : resolve(configured);
47
+ return join(directory, "auth.json");
48
+ }
49
+
50
+ function ompDatabasePath(lookup: StoreLookup): string {
51
+ const home = lookup.home ?? homedir();
52
+ const env = lookup.env ?? process.env;
53
+ const platform = lookup.platform ?? process.platform;
54
+ const profile = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim() || "default";
55
+ const base = join(home, env.PI_CONFIG_DIR || ".omp");
56
+ const configRoot = profile === "default" ? base : join(base, "profiles", profile);
57
+ const defaultAgentDir = join(configRoot, "agent");
58
+ const agentDir =
59
+ profile === "default" && env.PI_CODING_AGENT_DIR
60
+ ? resolve(env.PI_CODING_AGENT_DIR)
61
+ : defaultAgentDir;
62
+ if (
63
+ (platform === "linux" || platform === "darwin") &&
64
+ agentDir === defaultAgentDir &&
65
+ env.XDG_DATA_HOME
66
+ ) {
67
+ const xdgBase = join(env.XDG_DATA_HOME, "omp");
68
+ const xdgRoot = profile === "default" ? xdgBase : join(xdgBase, "profiles", profile);
69
+ if (existsSync(xdgRoot)) return join(xdgRoot, "agent.db");
70
+ }
71
+ return join(agentDir, "agent.db");
72
+ }
73
+
74
+ function builtinSqlite(): Sqlite | undefined {
75
+ // Node 20 must still compile/load the plugin; only OMP needs this newer builtin.
76
+ const parsed = z
77
+ .object({
78
+ DatabaseSync: z.custom<Sqlite["DatabaseSync"]>((value) => typeof value === "function"),
79
+ })
80
+ .safeParse(process.getBuiltinModule?.("node:sqlite"));
81
+ return parsed.success ? parsed.data : undefined;
82
+ }
83
+
84
+ function ompRows(path: string, lookup: StoreLookup) {
85
+ let db: Database | undefined;
86
+ try {
87
+ const sqlite = (lookup.sqlite ?? builtinSqlite)();
88
+ if (!sqlite) return [];
89
+ db = new sqlite.DatabaseSync(path, { readOnly: true });
90
+ const rows = db
91
+ .prepare(
92
+ "SELECT id, data FROM auth_credentials WHERE provider = ? AND credential_type = 'oauth' AND disabled_cause IS NULL ORDER BY id",
93
+ )
94
+ .all("anthropic");
95
+ return rows.flatMap((value) => {
96
+ const row = rowSchema.safeParse(value);
97
+ if (!row.success) return [];
98
+ try {
99
+ const oauth = oauthSchema.parse(JSON.parse(row.data.data));
100
+ return [{ id: row.data.id, oauth }];
101
+ } catch {
102
+ return [];
103
+ }
104
+ });
105
+ } catch {
106
+ return [];
107
+ } finally {
108
+ db?.close();
109
+ }
110
+ }
111
+
112
+ export function discoverOmp(lookup: StoreLookup): Route[] {
113
+ const path = ompDatabasePath(lookup);
114
+ return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id }));
115
+ }
116
+
117
+ export function readOmp(route: Extract<Route, { store: "omp" }>, lookup: StoreLookup) {
118
+ return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null;
119
+ }
120
+
121
+ export async function readHarness(
122
+ route: Extract<Route, { path: string }>,
123
+ lookup: StoreLookup = {},
124
+ ) {
125
+ if (route.store === "omp") return readOmp(route, lookup);
126
+ const provider = "anthropic";
127
+ const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path));
128
+ if (!parsed.success) return null;
129
+ const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]);
130
+ return oauth.success ? oauth.data : null;
131
+ }
@@ -1,3 +1,4 @@
1
+ import { discoverOmp, piAuthPath, readHarness, type StoreLookup } from "./stores.js";
1
2
  import type { UsageInput } from "../shared/input.js";
2
3
  import { execFile } from "node:child_process";
3
4
  import { existsSync, promises as fs } from "node:fs";
@@ -8,7 +9,8 @@ import { z } from "zod";
8
9
  import {
9
10
  hashAccountKey,
10
11
  toneFromUsedPct,
11
- unavailableUsage,
12
+ unavailable,
13
+ type UsageAccount,
12
14
  windowFromUsedPct,
13
15
  type UsageReport,
14
16
  type UsageWindow,
@@ -26,6 +28,7 @@ const ClaudeCredentialsSchema = z.object({
26
28
  claudeAiOauth: z
27
29
  .object({
28
30
  accessToken: z.string().optional(),
31
+ expiresAt: z.number().optional(),
29
32
  refreshToken: z.string().optional(),
30
33
  subscriptionType: z.string().optional(),
31
34
  rateLimitTier: z.string().optional(),
@@ -75,6 +78,7 @@ type ClaudeLimit = z.infer<typeof ClaudeLimitSchema>;
75
78
  const SCOPED_WEEKLY_KIND = "weekly_scoped";
76
79
 
77
80
  interface ClaudeCredentialRecord {
81
+ expires?: number;
78
82
  oauth: { accessToken: string } & NonNullable<ClaudeCredentials["claudeAiOauth"]>;
79
83
  }
80
84
 
@@ -332,21 +336,21 @@ export async function readClaudeKeychainCredentials(
332
336
  return null;
333
337
  }
334
338
 
335
- interface ClaudeCredentialLookup {
336
- platform?: NodeJS.Platform;
339
+ interface ClaudeCredentialLookup extends StoreLookup {
337
340
  readKeychainCredentials?: () => Promise<unknown | null>;
338
341
  claudeHome?: string;
339
- accountHome?: string;
340
342
  }
341
343
 
342
- /** Shared credential lookup for usage fetches and account identification. */
343
- export async function resolveClaudeCredentials(
344
- _input: UsageInput,
345
- lookup: ClaudeCredentialLookup = {},
344
+ function claudeCredentialPath(lookup: ClaudeCredentialLookup): string {
345
+ const home = lookup.home ?? homedir();
346
+ const env = lookup.env ?? process.env;
347
+ const claudeHome = lookup.claudeHome ?? env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
348
+ return join(claudeHome, ".credentials.json");
349
+ }
350
+
351
+ async function keychainCredentialRecord(
352
+ lookup: ClaudeCredentialLookup,
346
353
  ): Promise<ClaudeCredentialRecord | null> {
347
- const claudeHome = lookup.claudeHome ?? process.env["CLAUDE_HOME"] ?? join(homedir(), ".claude");
348
- const fileCredentials = await readCredentialFile(join(claudeHome, ".credentials.json"));
349
- if (fileCredentials) return fileCredentials;
350
354
  if ((lookup.platform ?? process.platform) !== "darwin") return null;
351
355
  const parsed = ClaudeCredentialsSchema.safeParse(
352
356
  await (lookup.readKeychainCredentials ?? readClaudeKeychainCredentials)(),
@@ -354,6 +358,55 @@ export async function resolveClaudeCredentials(
354
358
  return parsed.success ? toCredentialRecord(parsed.data) : null;
355
359
  }
356
360
 
361
+ export async function discover(
362
+ lookup: ClaudeCredentialLookup = {},
363
+ fetchApi: typeof fetch = fetch,
364
+ ): Promise<UsageAccount[]> {
365
+ // Claude Code only reads the file when its Keychain login is empty.
366
+ const primary: UsageInput = (await keychainCredentialRecord(lookup))
367
+ ? { route: { store: "keychain" } }
368
+ : { route: { store: "claude", path: claudeCredentialPath(lookup) } };
369
+ const candidates: UsageInput[] = [
370
+ primary,
371
+ { route: { store: "pi", path: piAuthPath(lookup) } },
372
+ ...discoverOmp(lookup).map((route) => ({ route })),
373
+ ];
374
+ const accounts: UsageAccount[] = [];
375
+ for (const input of candidates) {
376
+ const credentials = await resolveClaudeCredentials(input, lookup);
377
+ if (!credentials) continue;
378
+ const fallback = { key: hashAccountKey(JSON.stringify(input.route)), input };
379
+ if (credentials.expires !== undefined && credentials.expires <= (lookup.now ?? Date.now)()) {
380
+ accounts.push(fallback);
381
+ continue;
382
+ }
383
+ try {
384
+ const profile = await readProfile(
385
+ credentials.oauth.accessToken,
386
+ fetchApi,
387
+ lookup.now ?? Date.now,
388
+ );
389
+ accounts.push("status" in profile ? fallback : { ...profile, input });
390
+ } catch {
391
+ // Keep the login visible; fetching usage reports the vendor failure.
392
+ accounts.push(fallback);
393
+ }
394
+ }
395
+ return accounts;
396
+ }
397
+
398
+ /** Re-read the selected login; the harness owns token refresh. */
399
+ export async function resolveClaudeCredentials(
400
+ input: UsageInput,
401
+ lookup: ClaudeCredentialLookup = {},
402
+ ): Promise<ClaudeCredentialRecord | null> {
403
+ const route = input.route;
404
+ if (route.store === "claude") return readCredentialFile(route.path);
405
+ if (route.store === "keychain") return keychainCredentialRecord(lookup);
406
+ const oauth = await readHarness(route, lookup);
407
+ return oauth ? { oauth: { accessToken: oauth.access }, expires: oauth.expires } : null;
408
+ }
409
+
357
410
  async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
358
411
  if (!existsSync(path)) return null;
359
412
  try {
@@ -367,7 +420,9 @@ async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord
367
420
 
368
421
  function toCredentialRecord(credentials: ClaudeCredentials): ClaudeCredentialRecord | null {
369
422
  const oauth = credentials.claudeAiOauth;
370
- return oauth?.accessToken ? { oauth: { ...oauth, accessToken: oauth.accessToken } } : null;
423
+ return oauth?.accessToken
424
+ ? { oauth: { ...oauth, accessToken: oauth.accessToken }, expires: oauth.expiresAt }
425
+ : null;
371
426
  }
372
427
 
373
428
  export async function fetchUsage(
@@ -403,7 +458,7 @@ export async function fetchUsage(
403
458
  return parsed;
404
459
  }
405
460
 
406
- async function callClaudeApi(token: string): Promise<ClaudeUsageResponse | "NEEDS_AUTH"> {
461
+ async function callClaudeApi(token: string): Promise<ClaudeUsageResponse | number> {
407
462
  const res = await fetchApi("https://api.anthropic.com/api/oauth/usage", {
408
463
  signal: AbortSignal.timeout(15_000),
409
464
  headers: {
@@ -412,24 +467,38 @@ export async function fetchUsage(
412
467
  "anthropic-beta": CLAUDE_OAUTH_BETA,
413
468
  },
414
469
  });
415
- if (res.status === 401 || res.status === 403) return "NEEDS_AUTH";
470
+ if (res.status === 401 || res.status === 403) return res.status;
416
471
  if (!res.ok) throw new Error(`Claude usage API returned ${res.status}`);
417
472
  return ClaudeUsageResponseSchema.parse(await res.json());
418
473
  }
419
474
 
420
475
  const credentials = await resolveClaudeCredentials(input, credentialLookup);
421
- if (!credentials) {
422
- return unavailableUsage();
423
- }
476
+ if (!credentials) throw new Error("Claude login store no longer exists");
477
+ const refreshedBy =
478
+ input.route.store === "claude" || input.route.store === "keychain"
479
+ ? "claude"
480
+ : input.route.store;
481
+ if (
482
+ credentials.expires !== undefined &&
483
+ credentials.expires <= (credentialLookup.now ?? Date.now)()
484
+ )
485
+ return unavailable({
486
+ kind: "expired",
487
+ expiresAt: new Date(credentials.expires).toISOString(),
488
+ refreshedBy,
489
+ });
490
+ const profile = await cachedProfile(
491
+ credentials.oauth.accessToken,
492
+ (credentialLookup.now ?? Date.now)(),
493
+ );
494
+ if (profile && "status" in profile)
495
+ return unavailable({ kind: "rejected", status: profile.status, refreshedBy });
424
496
 
425
497
  const { oauth } = credentials;
426
498
  const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier);
427
499
  const resp = await callClaudeApi(oauth.accessToken);
428
500
 
429
- if (resp === "NEEDS_AUTH") {
430
- // Read-only on credentials; the Claude CLI owns refresh. See docs/providers.md.
431
- return unavailableUsage();
432
- }
501
+ if (typeof resp === "number") return unavailable({ kind: "rejected", status: resp, refreshedBy });
433
502
 
434
503
  const scoped = reconcileScopedLimits(
435
504
  legacyScopedLimits(resp),
@@ -464,44 +533,27 @@ export async function fetchUsage(
464
533
  }
465
534
 
466
535
  // The OAuth usage endpoint meters the active organization selected by the token.
467
- const ClaudeAccountSchema = z.object({
468
- accountUuid: z.string().min(1),
469
- organizationUuid: z.string().min(1),
470
- emailAddress: z.string().optional(),
471
- });
472
- type ClaudeIdentity = { key: string; label?: string } | null;
536
+ type ClaudeIdentity = { key: string; label?: string } | { status: number };
473
537
  const PROFILE_TTL_MS = 300_000;
474
538
  const PROFILE_CACHE_LIMIT = 128;
475
539
  const profileCache = new Map<string, { at: number; result: Promise<ClaudeIdentity> }>();
476
540
 
541
+ function cachedProfile(token: string, now: number): Promise<ClaudeIdentity> | undefined {
542
+ pruneProfileCache(now);
543
+ return profileCache.get(hashAccountKey(token))?.result;
544
+ }
545
+
477
546
  function pruneProfileCache(now: number): void {
478
547
  for (const [key, entry] of profileCache) {
479
548
  if (now - entry.at >= PROFILE_TTL_MS) profileCache.delete(key);
480
549
  }
481
550
  }
482
551
 
483
- export async function identify(
484
- input: UsageInput,
485
- fetchApi: typeof fetch = fetch,
486
- now: () => number = Date.now,
487
- credentialLookup: ClaudeCredentialLookup = {},
488
- ) {
489
- const directory = credentialLookup.accountHome ?? homedir();
490
- try {
491
- const config = z
492
- .object({ oauthAccount: ClaudeAccountSchema })
493
- .parse(JSON.parse(await fs.readFile(join(directory, ".claude.json"), "utf8")));
494
- const account = config.oauthAccount;
495
- return {
496
- key: `${account.accountUuid}.${account.organizationUuid}`,
497
- ...(account.emailAddress ? { label: account.emailAddress } : {}),
498
- };
499
- } catch {
500
- // Credentials may still be present even when account metadata is absent.
501
- }
502
- const credentials = await resolveClaudeCredentials(input, credentialLookup);
503
- if (!credentials) return null;
504
- const token = credentials.oauth.accessToken;
552
+ async function readProfile(
553
+ token: string,
554
+ fetchApi: typeof fetch,
555
+ now: () => number,
556
+ ): Promise<ClaudeIdentity> {
505
557
  const tokenHash = hashAccountKey(token);
506
558
  pruneProfileCache(now());
507
559
  const pending = profileCache.get(tokenHash);
@@ -520,7 +572,7 @@ export async function identify(
520
572
  "anthropic-beta": CLAUDE_OAUTH_BETA,
521
573
  },
522
574
  });
523
- if (response.status === 401 || response.status === 403) return null;
575
+ if (response.status === 401 || response.status === 403) return { status: response.status };
524
576
  if (!response.ok) throw new Error(`Claude profile API returned ${response.status}`);
525
577
  const profile = z
526
578
  .object({
@@ -1,3 +1,15 @@
1
1
  import { z } from "zod";
2
- export const inputSchema = z.object({}).strict();
2
+
3
+ export const routeSchema = z.discriminatedUnion("store", [
4
+ z.object({ store: z.enum(["claude", "pi"]), path: z.string().min(1) }).strict(),
5
+ z
6
+ .object({
7
+ store: z.literal("omp"),
8
+ path: z.string().min(1),
9
+ credentialId: z.number().int().positive(),
10
+ })
11
+ .strict(),
12
+ z.object({ store: z.literal("keychain") }).strict(),
13
+ ]);
14
+ export const inputSchema = z.object({ route: routeSchema }).strict();
3
15
  export type UsageInput = z.infer<typeof inputSchema>;
@@ -1,6 +1,6 @@
1
1
  import type { PluginServerContext } from "@getpaseo/plugin/server";
2
2
  import { inputSchema } from "./shared/input.js";
3
- import { fetchUsage, identify } from "./server/usage.js";
3
+ import { discover, fetchUsage } from "./server/usage.js";
4
4
 
5
5
  export default function contribute(server: PluginServerContext) {
6
6
  server.registerUsageSource({
@@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) {
8
8
  label: "Codex",
9
9
  icon: "icon.svg",
10
10
  input: inputSchema,
11
- discover: async () => [{}],
12
- identify,
11
+ discover: () => discover(),
13
12
  fetch: fetchUsage,
14
13
  });
15
14
  return () => {};
@@ -0,0 +1,131 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { z } from "zod";
6
+ import { routeSchema } from "../shared/input.js";
7
+
8
+ export interface StoreLookup {
9
+ home?: string;
10
+ env?: NodeJS.ProcessEnv;
11
+ platform?: NodeJS.Platform;
12
+ now?: () => number;
13
+ sqlite?: () => Sqlite | undefined;
14
+ }
15
+ interface Database {
16
+ prepare(sql: string): { all(...args: string[]): unknown[] };
17
+ close(): void;
18
+ }
19
+ export interface Sqlite {
20
+ DatabaseSync: new (path: string, options: { readOnly: boolean }) => Database;
21
+ }
22
+ export const oauthSchema = z.object({
23
+ access: z.string().min(1),
24
+ accountId: z.string().optional(),
25
+ expires: z.number().optional(),
26
+ });
27
+ const rowSchema = z.object({ id: z.number().int().positive(), data: z.string() });
28
+ export type Route = z.infer<typeof routeSchema>;
29
+
30
+ export async function readJson(path: string): Promise<unknown> {
31
+ try {
32
+ return JSON.parse(await readFile(path, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export function piAuthPath(lookup: StoreLookup): string {
39
+ const home = lookup.home ?? homedir();
40
+ const env = lookup.env ?? process.env;
41
+ const configured = env.PI_CODING_AGENT_DIR?.trim();
42
+ if (!configured) return join(home, ".pi", "agent", "auth.json");
43
+ if (configured === "~") return join(home, "auth.json");
44
+ const directory = configured.startsWith("~/")
45
+ ? join(home, configured.slice(2))
46
+ : resolve(configured);
47
+ return join(directory, "auth.json");
48
+ }
49
+
50
+ function ompDatabasePath(lookup: StoreLookup): string {
51
+ const home = lookup.home ?? homedir();
52
+ const env = lookup.env ?? process.env;
53
+ const platform = lookup.platform ?? process.platform;
54
+ const profile = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim() || "default";
55
+ const base = join(home, env.PI_CONFIG_DIR || ".omp");
56
+ const configRoot = profile === "default" ? base : join(base, "profiles", profile);
57
+ const defaultAgentDir = join(configRoot, "agent");
58
+ const agentDir =
59
+ profile === "default" && env.PI_CODING_AGENT_DIR
60
+ ? resolve(env.PI_CODING_AGENT_DIR)
61
+ : defaultAgentDir;
62
+ if (
63
+ (platform === "linux" || platform === "darwin") &&
64
+ agentDir === defaultAgentDir &&
65
+ env.XDG_DATA_HOME
66
+ ) {
67
+ const xdgBase = join(env.XDG_DATA_HOME, "omp");
68
+ const xdgRoot = profile === "default" ? xdgBase : join(xdgBase, "profiles", profile);
69
+ if (existsSync(xdgRoot)) return join(xdgRoot, "agent.db");
70
+ }
71
+ return join(agentDir, "agent.db");
72
+ }
73
+
74
+ function builtinSqlite(): Sqlite | undefined {
75
+ // Node 20 must still compile/load the plugin; only OMP needs this newer builtin.
76
+ const parsed = z
77
+ .object({
78
+ DatabaseSync: z.custom<Sqlite["DatabaseSync"]>((value) => typeof value === "function"),
79
+ })
80
+ .safeParse(process.getBuiltinModule?.("node:sqlite"));
81
+ return parsed.success ? parsed.data : undefined;
82
+ }
83
+
84
+ function ompRows(path: string, lookup: StoreLookup) {
85
+ let db: Database | undefined;
86
+ try {
87
+ const sqlite = (lookup.sqlite ?? builtinSqlite)();
88
+ if (!sqlite) return [];
89
+ db = new sqlite.DatabaseSync(path, { readOnly: true });
90
+ const rows = db
91
+ .prepare(
92
+ "SELECT id, data FROM auth_credentials WHERE provider = ? AND credential_type = 'oauth' AND disabled_cause IS NULL ORDER BY id",
93
+ )
94
+ .all("openai-codex");
95
+ return rows.flatMap((value) => {
96
+ const row = rowSchema.safeParse(value);
97
+ if (!row.success) return [];
98
+ try {
99
+ const oauth = oauthSchema.parse(JSON.parse(row.data.data));
100
+ return [{ id: row.data.id, oauth }];
101
+ } catch {
102
+ return [];
103
+ }
104
+ });
105
+ } catch {
106
+ return [];
107
+ } finally {
108
+ db?.close();
109
+ }
110
+ }
111
+
112
+ export function discoverOmp(lookup: StoreLookup): Route[] {
113
+ const path = ompDatabasePath(lookup);
114
+ return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id }));
115
+ }
116
+
117
+ export function readOmp(route: Extract<Route, { store: "omp" }>, lookup: StoreLookup) {
118
+ return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null;
119
+ }
120
+
121
+ export async function readHarness(
122
+ route: Extract<Route, { path: string }>,
123
+ lookup: StoreLookup = {},
124
+ ) {
125
+ if (route.store === "omp") return readOmp(route, lookup);
126
+ const provider = route.store === "opencode" ? "openai" : "openai-codex";
127
+ const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path));
128
+ if (!parsed.success) return null;
129
+ const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]);
130
+ return oauth.success ? oauth.data : null;
131
+ }