@camerontaylor/paseo-server 0.11.0-beta.1.fork.1 → 0.11.0-beta.3.fork.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.
Files changed (55) 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 +123 -70
  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 +103 -33
  8. package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +12 -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 +59 -34
  11. package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +7 -1
  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 +115 -71
  48. package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js → index-980574f58cb292611c985646d625a892.js} +12 -12
  49. package/dist/server/web-ui/_expo/static/js/web/index-980574f58cb292611c985646d625a892.js.br +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js.gz → index-980574f58cb292611c985646d625a892.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-5887a06cac5928321e7fd84d0fdf68a4.js.br +0 -0
@@ -1,6 +1,6 @@
1
1
  import type { PluginServerContext } from "@camerontaylor/paseo-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(),
@@ -74,13 +77,8 @@ type ClaudeLimit = z.infer<typeof ClaudeLimitSchema>;
74
77
 
75
78
  const SCOPED_WEEKLY_KIND = "weekly_scoped";
76
79
 
77
- // A 403 is not a stale token, so refreshing cannot clear it, and refreshing anyway rewrites
78
- // the credentials on every poll. What causes the refusal is not visible from here, so the
79
- // message names the refusal rather than guessing at a reason.
80
- const CLAUDE_USAGE_FORBIDDEN_MESSAGE =
81
- "Usage request was refused (HTTP 403). Check the daemon's network access to api.anthropic.com.";
82
-
83
80
  interface ClaudeCredentialRecord {
81
+ expires?: number;
84
82
  oauth: { accessToken: string } & NonNullable<ClaudeCredentials["claudeAiOauth"]>;
85
83
  }
86
84
 
@@ -338,27 +336,24 @@ export async function readClaudeKeychainCredentials(
338
336
  return null;
339
337
  }
340
338
 
341
- interface ClaudeCredentialLookup {
342
- platform?: NodeJS.Platform;
339
+ interface ClaudeCredentialLookup extends StoreLookup {
343
340
  readKeychainCredentials?: () => Promise<unknown | null>;
344
341
  claudeHome?: string;
345
- accountHome?: string;
346
342
  }
347
343
 
348
- /** Shared credential lookup for usage fetches and account identification. */
349
- export async function resolveClaudeCredentials(
350
- input: UsageInput,
351
- lookup: ClaudeCredentialLookup = {},
352
- ): Promise<ClaudeCredentialRecord | null> {
353
- if ("providerId" in input)
354
- return input.configDir ? readCredentialFile(join(input.configDir, ".credentials.json")) : null;
344
+ type RoutedInput = Extract<UsageInput, { route: unknown }>;
345
+
346
+ function claudeCredentialPath(lookup: ClaudeCredentialLookup): string {
347
+ const home = lookup.home ?? homedir();
348
+ const env = lookup.env ?? process.env;
355
349
  const claudeHome =
356
- lookup.claudeHome ??
357
- process.env["CLAUDE_CONFIG_DIR"] ??
358
- process.env["CLAUDE_HOME"] ??
359
- join(homedir(), ".claude");
360
- const fileCredentials = await readCredentialFile(join(claudeHome, ".credentials.json"));
361
- if (fileCredentials) return fileCredentials;
350
+ lookup.claudeHome ?? env.CLAUDE_CONFIG_DIR ?? env.CLAUDE_HOME ?? join(home, ".claude");
351
+ return join(claudeHome, ".credentials.json");
352
+ }
353
+
354
+ async function keychainCredentialRecord(
355
+ lookup: ClaudeCredentialLookup,
356
+ ): Promise<ClaudeCredentialRecord | null> {
362
357
  if ((lookup.platform ?? process.platform) !== "darwin") return null;
363
358
  const parsed = ClaudeCredentialsSchema.safeParse(
364
359
  await (lookup.readKeychainCredentials ?? readClaudeKeychainCredentials)(),
@@ -366,6 +361,61 @@ export async function resolveClaudeCredentials(
366
361
  return parsed.success ? toCredentialRecord(parsed.data) : null;
367
362
  }
368
363
 
364
+ export async function discover(
365
+ lookup: ClaudeCredentialLookup = {},
366
+ fetchApi: typeof fetch = fetch,
367
+ ): Promise<UsageAccount[]> {
368
+ // Claude Code only reads the file when its Keychain login is empty.
369
+ const primary: RoutedInput = (await keychainCredentialRecord(lookup))
370
+ ? { route: { store: "keychain" } }
371
+ : { route: { store: "claude", path: claudeCredentialPath(lookup) } };
372
+ const candidates: RoutedInput[] = [
373
+ primary,
374
+ { route: { store: "pi", path: piAuthPath(lookup) } },
375
+ ...discoverOmp(lookup).map((route) => ({ route })),
376
+ ];
377
+ const accounts: UsageAccount[] = [];
378
+ for (const input of candidates) {
379
+ const credentials = await resolveClaudeCredentials(input, lookup);
380
+ if (!credentials) continue;
381
+ const fallback = { key: hashAccountKey(JSON.stringify(input.route)), input };
382
+ if (credentials.expires !== undefined && credentials.expires <= (lookup.now ?? Date.now)()) {
383
+ accounts.push(fallback);
384
+ continue;
385
+ }
386
+ try {
387
+ const profile = await readProfile(
388
+ credentials.oauth.accessToken,
389
+ fetchApi,
390
+ lookup.now ?? Date.now,
391
+ );
392
+ accounts.push("status" in profile ? fallback : { ...profile, input });
393
+ } catch {
394
+ // Keep the login visible; fetching usage reports the vendor failure.
395
+ accounts.push(fallback);
396
+ }
397
+ }
398
+ return accounts;
399
+ }
400
+
401
+ /** Re-read the selected login; the harness owns token refresh. */
402
+ export async function resolveClaudeCredentials(
403
+ input: UsageInput | Record<string, never>,
404
+ lookup: ClaudeCredentialLookup = {},
405
+ ): Promise<ClaudeCredentialRecord | null> {
406
+ if ("providerId" in input)
407
+ return input.configDir ? readCredentialFile(join(input.configDir, ".credentials.json")) : null;
408
+ if (!("route" in input)) {
409
+ const primary = await keychainCredentialRecord(lookup);
410
+ return primary ?? readCredentialFile(claudeCredentialPath(lookup));
411
+ }
412
+ const route = input.route;
413
+ if (route.store === "claude") return readCredentialFile(route.path);
414
+ if (route.store === "keychain") return keychainCredentialRecord(lookup);
415
+ const oauth = await readHarness(route, lookup);
416
+ return oauth ? { oauth: { accessToken: oauth.access }, expires: oauth.expires } : null;
417
+ }
418
+
369
419
  async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
370
420
  if (!existsSync(path)) return null;
371
421
  try {
@@ -379,7 +429,9 @@ async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord
379
429
 
380
430
  function toCredentialRecord(credentials: ClaudeCredentials): ClaudeCredentialRecord | null {
381
431
  const oauth = credentials.claudeAiOauth;
382
- return oauth?.accessToken ? { oauth: { ...oauth, accessToken: oauth.accessToken } } : null;
432
+ return oauth?.accessToken
433
+ ? { oauth: { ...oauth, accessToken: oauth.accessToken }, expires: oauth.expiresAt }
434
+ : null;
383
435
  }
384
436
 
385
437
  export async function fetchUsage(
@@ -415,9 +467,7 @@ export async function fetchUsage(
415
467
  return parsed;
416
468
  }
417
469
 
418
- async function callClaudeApi(
419
- token: string,
420
- ): Promise<ClaudeUsageResponse | "NEEDS_AUTH" | "FORBIDDEN"> {
470
+ async function callClaudeApi(token: string): Promise<ClaudeUsageResponse | number> {
421
471
  const res = await fetchApi("https://api.anthropic.com/api/oauth/usage", {
422
472
  signal: AbortSignal.timeout(15_000),
423
473
  headers: {
@@ -426,29 +476,38 @@ export async function fetchUsage(
426
476
  "anthropic-beta": CLAUDE_OAUTH_BETA,
427
477
  },
428
478
  });
429
- if (res.status === 401) return "NEEDS_AUTH";
430
- if (res.status === 403) return "FORBIDDEN";
479
+ if (res.status === 401 || res.status === 403) return res.status;
431
480
  if (!res.ok) throw new Error(`Claude usage API returned ${res.status}`);
432
481
  return ClaudeUsageResponseSchema.parse(await res.json());
433
482
  }
434
483
 
435
484
  const credentials = await resolveClaudeCredentials(input, credentialLookup);
436
- if (!credentials) {
437
- return unavailableUsage();
438
- }
485
+ if (!credentials) throw new Error("Claude login store no longer exists");
486
+ const refreshedBy =
487
+ !("route" in input) || input.route.store === "claude" || input.route.store === "keychain"
488
+ ? "claude"
489
+ : input.route.store;
490
+ if (
491
+ credentials.expires !== undefined &&
492
+ credentials.expires <= (credentialLookup.now ?? Date.now)()
493
+ )
494
+ return unavailable({
495
+ kind: "expired",
496
+ expiresAt: new Date(credentials.expires).toISOString(),
497
+ refreshedBy,
498
+ });
499
+ const profile = await cachedProfile(
500
+ credentials.oauth.accessToken,
501
+ (credentialLookup.now ?? Date.now)(),
502
+ );
503
+ if (profile && "status" in profile)
504
+ return unavailable({ kind: "rejected", status: profile.status, refreshedBy });
439
505
 
440
506
  const { oauth } = credentials;
441
507
  const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier);
442
508
  const resp = await callClaudeApi(oauth.accessToken);
443
509
 
444
- if (resp === "FORBIDDEN") {
445
- return { ...unavailableUsage(), error: CLAUDE_USAGE_FORBIDDEN_MESSAGE };
446
- }
447
-
448
- if (resp === "NEEDS_AUTH") {
449
- // Read-only on credentials; the Claude CLI owns refresh. See docs/providers.md.
450
- return unavailableUsage();
451
- }
510
+ if (typeof resp === "number") return unavailable({ kind: "rejected", status: resp, refreshedBy });
452
511
 
453
512
  const scoped = reconcileScopedLimits(
454
513
  legacyScopedLimits(resp),
@@ -483,45 +542,27 @@ export async function fetchUsage(
483
542
  }
484
543
 
485
544
  // The OAuth usage endpoint meters the active organization selected by the token.
486
- const ClaudeAccountSchema = z.object({
487
- accountUuid: z.string().min(1),
488
- organizationUuid: z.string().min(1),
489
- emailAddress: z.string().optional(),
490
- });
491
- type ClaudeIdentity = { key: string; label?: string } | null;
545
+ type ClaudeIdentity = { key: string; label?: string } | { status: number };
492
546
  const PROFILE_TTL_MS = 300_000;
493
547
  const PROFILE_CACHE_LIMIT = 128;
494
548
  const profileCache = new Map<string, { at: number; result: Promise<ClaudeIdentity> }>();
495
549
 
550
+ function cachedProfile(token: string, now: number): Promise<ClaudeIdentity> | undefined {
551
+ pruneProfileCache(now);
552
+ return profileCache.get(hashAccountKey(token))?.result;
553
+ }
554
+
496
555
  function pruneProfileCache(now: number): void {
497
556
  for (const [key, entry] of profileCache) {
498
557
  if (now - entry.at >= PROFILE_TTL_MS) profileCache.delete(key);
499
558
  }
500
559
  }
501
560
 
502
- export async function identify(
503
- input: UsageInput,
504
- fetchApi: typeof fetch = fetch,
505
- now: () => number = Date.now,
506
- credentialLookup: ClaudeCredentialLookup = {},
507
- ) {
508
- if ("providerId" in input) return { key: `provider.${input.providerId}`, label: input.label };
509
- const directory = credentialLookup.accountHome ?? homedir();
510
- try {
511
- const config = z
512
- .object({ oauthAccount: ClaudeAccountSchema })
513
- .parse(JSON.parse(await fs.readFile(join(directory, ".claude.json"), "utf8")));
514
- const account = config.oauthAccount;
515
- return {
516
- key: `${account.accountUuid}.${account.organizationUuid}`,
517
- ...(account.emailAddress ? { label: account.emailAddress } : {}),
518
- };
519
- } catch {
520
- // Credentials may still be present even when account metadata is absent.
521
- }
522
- const credentials = await resolveClaudeCredentials(input, credentialLookup);
523
- if (!credentials) return null;
524
- const token = credentials.oauth.accessToken;
561
+ async function readProfile(
562
+ token: string,
563
+ fetchApi: typeof fetch,
564
+ now: () => number,
565
+ ): Promise<ClaudeIdentity> {
525
566
  const tokenHash = hashAccountKey(token);
526
567
  pruneProfileCache(now());
527
568
  const pending = profileCache.get(tokenHash);
@@ -540,7 +581,7 @@ export async function identify(
540
581
  "anthropic-beta": CLAUDE_OAUTH_BETA,
541
582
  },
542
583
  });
543
- if (response.status === 401 || response.status === 403) return null;
584
+ if (response.status === 401 || response.status === 403) return { status: response.status };
544
585
  if (!response.ok) throw new Error(`Claude profile API returned ${response.status}`);
545
586
  const profile = z
546
587
  .object({
@@ -562,3 +603,15 @@ export async function identify(
562
603
  throw error;
563
604
  }
564
605
  }
606
+
607
+ export async function identify(
608
+ input: UsageInput | Record<string, never>,
609
+ fetchApi: typeof fetch = fetch,
610
+ now: () => number = Date.now,
611
+ credentialLookup: ClaudeCredentialLookup = {},
612
+ ) {
613
+ if ("providerId" in input) return { key: `provider.${input.providerId}`, label: input.label };
614
+ const credentials = await resolveClaudeCredentials(input, credentialLookup);
615
+ if (!credentials) return null;
616
+ return readProfile(credentials.oauth.accessToken, fetchApi, now);
617
+ }
@@ -1,6 +1,18 @@
1
1
  import { z } from "zod";
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
+ ]);
2
14
  export const inputSchema = z.union([
3
- z.strictObject({}),
15
+ z.object({ route: routeSchema }).strict(),
4
16
  z.strictObject({
5
17
  providerId: z.string().min(1),
6
18
  label: z.string(),
@@ -1,6 +1,6 @@
1
1
  import type { PluginServerContext } from "@camerontaylor/paseo-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
+ }