@getpaseo/server 0.11.0-beta.1 → 0.11.0-beta.2

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 (25) hide show
  1. package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -2
  2. package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +132 -0
  3. package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +45 -29
  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 -2
  6. package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +132 -0
  7. package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +55 -25
  8. package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +11 -1
  9. package/dist/server/builtin-plugins/package.json +1 -1
  10. package/dist/server/server/orchestration-skills/internal/operations.js +19 -5
  11. package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
  12. package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
  13. package/dist/server/server/plugins/builtin/index.js +6 -0
  14. package/dist/server/server/plugins/compiler.js +4 -0
  15. package/dist/server/server/plugins/usage-sources/index.d.ts +1 -1
  16. package/dist/server/server/plugins/usage-sources/index.js +53 -21
  17. package/dist/server/web-ui/_expo/static/js/web/{index-87a07caf197cc320e593332ed2d5e20f.js → index-a43718759ebeb27df202e8b9fc2fe58a.js} +6 -6
  18. package/dist/server/web-ui/_expo/static/js/web/index-a43718759ebeb27df202e8b9fc2fe58a.js.br +0 -0
  19. package/dist/server/web-ui/_expo/static/js/web/index-a43718759ebeb27df202e8b9fc2fe58a.js.gz +0 -0
  20. package/dist/server/web-ui/index.html +1 -1
  21. package/dist/server/web-ui/index.html.br +0 -0
  22. package/dist/server/web-ui/index.html.gz +0 -0
  23. package/package.json +6 -6
  24. package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.br +0 -0
  25. 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, identify } from "./server/usage.js";
4
4
 
5
5
  export default function contribute(server: PluginServerContext) {
6
6
  server.registerUsageSource({
@@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) {
8
8
  label: "Claude",
9
9
  icon: "icon.svg",
10
10
  input: inputSchema,
11
- discover: async () => [{}],
11
+ discover: () => discover(),
12
12
  identify,
13
13
  fetch: fetchUsage,
14
14
  });
@@ -0,0 +1,132 @@
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
+ if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return [];
101
+ return [{ id: row.data.id, oauth }];
102
+ } catch {
103
+ return [];
104
+ }
105
+ });
106
+ } catch {
107
+ return [];
108
+ } finally {
109
+ db?.close();
110
+ }
111
+ }
112
+
113
+ export function discoverOmp(lookup: StoreLookup): Route[] {
114
+ const path = ompDatabasePath(lookup);
115
+ return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id }));
116
+ }
117
+
118
+ export function readOmp(route: Extract<Route, { store: "omp" }>, lookup: StoreLookup) {
119
+ return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null;
120
+ }
121
+
122
+ export async function readHarness(
123
+ route: Extract<Route, { path: string }>,
124
+ lookup: StoreLookup = {},
125
+ ) {
126
+ if (route.store === "omp") return readOmp(route, lookup);
127
+ const provider = "anthropic";
128
+ const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path));
129
+ if (!parsed.success) return null;
130
+ const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]);
131
+ return oauth.success ? oauth.data : null;
132
+ }
@@ -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";
@@ -75,6 +76,7 @@ type ClaudeLimit = z.infer<typeof ClaudeLimitSchema>;
75
76
  const SCOPED_WEEKLY_KIND = "weekly_scoped";
76
77
 
77
78
  interface ClaudeCredentialRecord {
79
+ expires?: number;
78
80
  oauth: { accessToken: string } & NonNullable<ClaudeCredentials["claudeAiOauth"]>;
79
81
  }
80
82
 
@@ -332,21 +334,21 @@ export async function readClaudeKeychainCredentials(
332
334
  return null;
333
335
  }
334
336
 
335
- interface ClaudeCredentialLookup {
336
- platform?: NodeJS.Platform;
337
+ interface ClaudeCredentialLookup extends StoreLookup {
337
338
  readKeychainCredentials?: () => Promise<unknown | null>;
338
339
  claudeHome?: string;
339
- accountHome?: string;
340
340
  }
341
341
 
342
- /** Shared credential lookup for usage fetches and account identification. */
343
- export async function resolveClaudeCredentials(
344
- _input: UsageInput,
345
- lookup: ClaudeCredentialLookup = {},
342
+ function claudeCredentialPath(lookup: ClaudeCredentialLookup): string {
343
+ const home = lookup.home ?? homedir();
344
+ const env = lookup.env ?? process.env;
345
+ const claudeHome = lookup.claudeHome ?? env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
346
+ return join(claudeHome, ".credentials.json");
347
+ }
348
+
349
+ async function keychainCredentialRecord(
350
+ lookup: ClaudeCredentialLookup,
346
351
  ): 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
352
  if ((lookup.platform ?? process.platform) !== "darwin") return null;
351
353
  const parsed = ClaudeCredentialsSchema.safeParse(
352
354
  await (lookup.readKeychainCredentials ?? readClaudeKeychainCredentials)(),
@@ -354,6 +356,34 @@ export async function resolveClaudeCredentials(
354
356
  return parsed.success ? toCredentialRecord(parsed.data) : null;
355
357
  }
356
358
 
359
+ export async function discover(lookup: ClaudeCredentialLookup = {}): Promise<UsageInput[]> {
360
+ const candidates: UsageInput[] = [
361
+ { route: { store: "claude", path: claudeCredentialPath(lookup) } },
362
+ ];
363
+ if ((lookup.platform ?? process.platform) === "darwin")
364
+ candidates.push({ route: { store: "keychain" } });
365
+ candidates.push(
366
+ { route: { store: "pi", path: piAuthPath(lookup) } },
367
+ ...discoverOmp(lookup).map((route) => ({ route })),
368
+ );
369
+ const present: UsageInput[] = [];
370
+ for (const input of candidates)
371
+ if (await resolveClaudeCredentials(input, lookup)) present.push(input);
372
+ return present;
373
+ }
374
+
375
+ /** Re-read the selected login; the harness owns token refresh. */
376
+ export async function resolveClaudeCredentials(
377
+ input: UsageInput,
378
+ lookup: ClaudeCredentialLookup = {},
379
+ ): Promise<ClaudeCredentialRecord | null> {
380
+ const route = input.route;
381
+ if (route.store === "claude") return readCredentialFile(route.path);
382
+ if (route.store === "keychain") return keychainCredentialRecord(lookup);
383
+ const oauth = await readHarness(route, lookup);
384
+ return oauth ? { oauth: { accessToken: oauth.access }, expires: oauth.expires } : null;
385
+ }
386
+
357
387
  async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
358
388
  if (!existsSync(path)) return null;
359
389
  try {
@@ -418,7 +448,11 @@ export async function fetchUsage(
418
448
  }
419
449
 
420
450
  const credentials = await resolveClaudeCredentials(input, credentialLookup);
421
- if (!credentials) {
451
+ if (
452
+ !credentials ||
453
+ (credentials.expires !== undefined &&
454
+ credentials.expires <= (credentialLookup.now ?? Date.now)())
455
+ ) {
422
456
  return unavailableUsage();
423
457
  }
424
458
 
@@ -464,11 +498,6 @@ export async function fetchUsage(
464
498
  }
465
499
 
466
500
  // 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
501
  type ClaudeIdentity = { key: string; label?: string } | null;
473
502
  const PROFILE_TTL_MS = 300_000;
474
503
  const PROFILE_CACHE_LIMIT = 128;
@@ -486,19 +515,6 @@ export async function identify(
486
515
  now: () => number = Date.now,
487
516
  credentialLookup: ClaudeCredentialLookup = {},
488
517
  ) {
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
518
  const credentials = await resolveClaudeCredentials(input, credentialLookup);
503
519
  if (!credentials) return null;
504
520
  const token = credentials.oauth.accessToken;
@@ -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, identify } from "./server/usage.js";
4
4
 
5
5
  export default function contribute(server: PluginServerContext) {
6
6
  server.registerUsageSource({
@@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) {
8
8
  label: "Codex",
9
9
  icon: "icon.svg",
10
10
  input: inputSchema,
11
- discover: async () => [{}],
11
+ discover: () => discover(),
12
12
  identify,
13
13
  fetch: fetchUsage,
14
14
  });
@@ -0,0 +1,132 @@
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
+ if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return [];
101
+ return [{ id: row.data.id, oauth }];
102
+ } catch {
103
+ return [];
104
+ }
105
+ });
106
+ } catch {
107
+ return [];
108
+ } finally {
109
+ db?.close();
110
+ }
111
+ }
112
+
113
+ export function discoverOmp(lookup: StoreLookup): Route[] {
114
+ const path = ompDatabasePath(lookup);
115
+ return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id }));
116
+ }
117
+
118
+ export function readOmp(route: Extract<Route, { store: "omp" }>, lookup: StoreLookup) {
119
+ return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null;
120
+ }
121
+
122
+ export async function readHarness(
123
+ route: Extract<Route, { path: string }>,
124
+ lookup: StoreLookup = {},
125
+ ) {
126
+ if (route.store === "omp") return readOmp(route, lookup);
127
+ const provider = route.store === "opencode" ? "openai" : "openai-codex";
128
+ const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path));
129
+ if (!parsed.success) return null;
130
+ const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]);
131
+ return oauth.success ? oauth.data : null;
132
+ }
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { discoverOmp, piAuthPath, readHarness, readJson, type StoreLookup } from "./stores.js";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import {
@@ -32,28 +32,56 @@ const responseSchema = z.object({
32
32
  credits: z.object({ balance: number.optional() }).nullish(),
33
33
  });
34
34
 
35
- export async function readAuth(
36
- _input: CodexUsageInput,
37
- ): Promise<{ token: string; accountId?: string; idToken?: string } | null> {
38
- const candidates = [
39
- ...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []),
40
- join(homedir(), ".config", "codex", "auth.json"),
41
- join(homedir(), ".codex", "auth.json"),
35
+ interface Auth {
36
+ token: string;
37
+ accountId?: string;
38
+ idToken?: string;
39
+ expires?: number;
40
+ }
41
+
42
+ export async function discover(lookup: StoreLookup = {}): Promise<CodexUsageInput[]> {
43
+ const env = lookup.env ?? process.env;
44
+ const home = lookup.home ?? homedir();
45
+ const paths = [
46
+ ...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []),
47
+ join(home, ".codex", "auth.json"),
42
48
  ];
43
- for (const path of candidates) {
44
- try {
45
- const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8")));
46
- if (auth.tokens?.access_token)
47
- return {
48
- token: auth.tokens.access_token,
49
- accountId: auth.tokens.account_id,
50
- idToken: auth.tokens.id_token,
51
- };
52
- } catch {
53
- continue;
54
- }
49
+ const candidates: CodexUsageInput[] = [...new Set(paths)].map((path) => ({
50
+ route: { store: "codex", path },
51
+ }));
52
+ candidates.push(
53
+ {
54
+ route: {
55
+ store: "opencode",
56
+ path: join(env.XDG_DATA_HOME || join(home, ".local", "share"), "opencode", "auth.json"),
57
+ },
58
+ },
59
+ { route: { store: "pi", path: piAuthPath(lookup) } },
60
+ ...discoverOmp(lookup).map((route) => ({ route })),
61
+ );
62
+ const present: CodexUsageInput[] = [];
63
+ for (const input of candidates) if (await readAuth(input, lookup)) present.push(input);
64
+ return present;
65
+ }
66
+
67
+ export async function readAuth(
68
+ input: CodexUsageInput,
69
+ lookup: StoreLookup = {},
70
+ ): Promise<Auth | null> {
71
+ const route = input.route;
72
+ if (route.store !== "codex") {
73
+ const oauth = await readHarness(route, lookup);
74
+ return oauth
75
+ ? { token: oauth.access, accountId: oauth.accountId, expires: oauth.expires }
76
+ : null;
55
77
  }
56
- return null;
78
+ const auth = authSchema.safeParse(await readJson(route.path));
79
+ if (!auth.success || !auth.data.tokens?.access_token) return null;
80
+ return {
81
+ token: auth.data.tokens.access_token,
82
+ accountId: auth.data.tokens.account_id,
83
+ idToken: auth.data.tokens.id_token,
84
+ };
57
85
  }
58
86
 
59
87
  function usageWindow(
@@ -73,9 +101,11 @@ function usageWindow(
73
101
  export async function fetchUsage(
74
102
  input: CodexUsageInput,
75
103
  fetchApi: typeof fetch = fetch,
104
+ lookup: StoreLookup = {},
76
105
  ): Promise<UsageReport> {
77
- const auth = await readAuth(input);
78
- if (!auth) return { status: "unavailable", windows: [] };
106
+ const auth = await readAuth(input, lookup);
107
+ if (!auth || (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)()))
108
+ return { status: "unavailable", windows: [] };
79
109
  const headers: Record<string, string> = {
80
110
  Authorization: `Bearer ${auth.token}`,
81
111
  Accept: "application/json",
@@ -151,8 +181,8 @@ function claimString(value: unknown): string | null {
151
181
  return typeof value === "string" && value.length > 0 ? value : null;
152
182
  }
153
183
 
154
- export async function identify(input: CodexUsageInput) {
155
- const auth = await readAuth(input);
184
+ export async function identify(input: CodexUsageInput, lookup: StoreLookup = {}) {
185
+ const auth = await readAuth(input, lookup);
156
186
  if (!auth) return null;
157
187
  const access = jwtClaims(auth.token);
158
188
  const id = jwtClaims(auth.idToken);
@@ -1,4 +1,14 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const inputSchema = z.object({}).strict();
3
+ export const routeSchema = z.discriminatedUnion("store", [
4
+ z.object({ store: z.enum(["codex", "opencode", "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
+ ]);
13
+ export const inputSchema = z.object({ route: routeSchema }).strict();
4
14
  export type CodexUsageInput = z.infer<typeof inputSchema>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpaseo/builtin-plugins",
3
- "version": "0.11.0-beta.1",
3
+ "version": "0.11.0-beta.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { promises as fs } from "node:fs";
3
3
  import path from "node:path";
4
- import { listFilesRecursive, removeSkill, syncSkills } from "./sync.js";
4
+ import { isLegacyCodexCopy, listFilesRecursive, readManagedFilesManifest, removeSkill, syncSkills, } from "./sync.js";
5
5
  // Names the bundle used to ship. They are never selectable, but every scan still
6
6
  // covers them so an older install's copies get cleaned up.
7
7
  export const LEGACY_SKILL_NAMES = [
@@ -110,10 +110,24 @@ export async function getSkillsStatus(targets, selection) {
110
110
  hashSkills(targets.claudeDir, names),
111
111
  hashSkills(targets.codexDir, names),
112
112
  ]);
113
- const disks = [agentsDisk, claudeDisk, codexDisk];
114
- const ops = diff(bundle, disks, names, resolveDesiredSkills(selection, available));
115
- const installed = installedSkillNames(disks, names);
116
- if (!hasInstalledPaseoSkill(disks))
113
+ const disks = [agentsDisk, claudeDisk];
114
+ const desired = resolveDesiredSkills(selection, available);
115
+ const ops = diff(bundle, disks, names, desired);
116
+ for (const name of codexDisk.keys()) {
117
+ if (ops.some((op) => op.name === name))
118
+ continue;
119
+ if (!(await isLegacyCodexCopy(name, targets)))
120
+ continue;
121
+ if (!desired.has(name))
122
+ ops.push({ kind: "delete", name });
123
+ else if (await readManagedFilesManifest(path.join(targets.codexDir, name))) {
124
+ ops.push({ kind: "update", name });
125
+ }
126
+ }
127
+ ops.sort((a, b) => compareStrings(a.name, b.name));
128
+ const allDisks = [...disks, codexDisk];
129
+ const installed = installedSkillNames(allDisks, names);
130
+ if (!hasInstalledPaseoSkill(allDisks))
117
131
  return { state: "not-installed", ops, available, installed };
118
132
  if (ops.length === 0)
119
133
  return { state: "up-to-date", ops, available, installed };
@@ -10,12 +10,24 @@ export interface SkillSyncResult {
10
10
  changedFiles: number;
11
11
  processedSkills: number;
12
12
  }
13
+ interface ManagedFilesManifest {
14
+ version: 1;
15
+ files: Record<string, string>;
16
+ }
13
17
  export declare function listFilesRecursive(rootDir: string): Promise<string[]>;
18
+ export declare function readManagedFilesManifest(dstDir: string): Promise<ManagedFilesManifest | null>;
14
19
  export interface RemoveSkillTargets {
15
20
  agentsDir: string;
16
21
  claudeDir: string;
17
22
  codexDir: string;
18
23
  }
19
24
  export declare function removeSkill(skillName: string, targets: RemoveSkillTargets): Promise<void>;
25
+ /**
26
+ * Codex also discovers .agents/skills, so its dedicated copy is a leftover. When
27
+ * the Codex skill directory resolves to the shared or Claude copy, it is that
28
+ * copy, and retiring it would delete the install.
29
+ */
30
+ export declare function isLegacyCodexCopy(skillName: string, targets: RemoveSkillTargets): Promise<boolean>;
20
31
  export declare function syncSkills(options: SkillSyncOptions): Promise<SkillSyncResult>;
32
+ export {};
21
33
  //# sourceMappingURL=sync.d.ts.map