@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.
- package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -2
- package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +132 -0
- package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +45 -29
- 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 -2
- package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +132 -0
- package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +55 -25
- package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +11 -1
- package/dist/server/builtin-plugins/package.json +1 -1
- 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/usage-sources/index.d.ts +1 -1
- package/dist/server/server/plugins/usage-sources/index.js +53 -21
- package/dist/server/web-ui/_expo/static/js/web/{index-87a07caf197cc320e593332ed2d5e20f.js → index-a43718759ebeb27df202e8b9fc2fe58a.js} +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-a43718759ebeb27df202e8b9fc2fe58a.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-a43718759ebeb27df202e8b9fc2fe58a.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 { 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:
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,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
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
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
|