@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.
- package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +131 -0
- package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +102 -50
- package/dist/server/builtin-plugins/claude-usage-source/shared/input.ts +13 -1
- package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +131 -0
- package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +70 -30
- package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +11 -1
- package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +28 -39
- package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
- package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
- package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
- package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
- package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
- package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
- package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
- package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
- package/dist/server/builtin-plugins/package.json +1 -1
- package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
- package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
- package/dist/server/server/orchestration-skills/internal/operations.js +19 -5
- package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
- package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
- package/dist/server/server/plugins/builtin/index.js +6 -0
- package/dist/server/server/plugins/compiler.js +4 -0
- package/dist/server/server/plugins/index.d.ts +18 -3
- package/dist/server/server/plugins/index.js +1 -2
- package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
- package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
- package/dist/server/server/plugins/plugin-process.js +2 -7
- package/dist/server/server/plugins/runtime.d.ts +0 -1
- package/dist/server/server/plugins/runtime.js +0 -11
- package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
- package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
- package/dist/server/server/plugins/usage-sources/index.d.ts +7 -9
- package/dist/server/server/plugins/usage-sources/index.js +98 -71
- package/dist/server/web-ui/_expo/static/js/web/{index-87a07caf197cc320e593332ed2d5e20f.js → index-1a3199cb049d45e47bfd4c216ce98838.js} +13 -13
- package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.gz +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.br +0 -0
- 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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
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 |
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
fetchApi: typeof fetch
|
|
486
|
-
now: () => number
|
|
487
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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
|
+
}
|