@hanzo/usage 0.1.0
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/host.d.ts +27 -0
- package/dist/host.js +18 -0
- package/dist/hosts/node.d.ts +2 -0
- package/dist/hosts/node.js +50 -0
- package/dist/hosts/tauri.d.ts +26 -0
- package/dist/hosts/tauri.js +67 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +18 -0
- package/dist/provider.d.ts +71 -0
- package/dist/provider.js +45 -0
- package/dist/providers/claude.d.ts +2 -0
- package/dist/providers/claude.js +138 -0
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +91 -0
- package/dist/providers/hanzo.d.ts +2 -0
- package/dist/providers/hanzo.js +168 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +4 -0
- package/dist/store.d.ts +49 -0
- package/dist/store.js +156 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +4 -0
- package/package.json +59 -0
- package/src/host.ts +49 -0
- package/src/hosts/node.ts +50 -0
- package/src/hosts/tauri.ts +77 -0
- package/src/index.ts +22 -0
- package/src/provider.ts +125 -0
- package/src/providers/claude.ts +178 -0
- package/src/providers/codex.ts +126 -0
- package/src/providers/hanzo.ts +203 -0
- package/src/react.ts +12 -0
- package/src/store.ts +209 -0
- package/src/types.ts +160 -0
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface HttpRequest {
|
|
2
|
+
url: string;
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
body?: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface HttpResponse {
|
|
9
|
+
status: number;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
export interface UsageHost {
|
|
14
|
+
/** Read a UTF-8 text file; returns undefined when missing/unreadable. */
|
|
15
|
+
readTextFile(path: string): Promise<string | undefined>;
|
|
16
|
+
/** List directory entries (names, not paths); [] when missing. */
|
|
17
|
+
listDir(path: string): Promise<string[]>;
|
|
18
|
+
/** Write a UTF-8 text file, creating parent directories. */
|
|
19
|
+
writeTextFile(path: string, contents: string): Promise<void>;
|
|
20
|
+
http(req: HttpRequest): Promise<HttpResponse>;
|
|
21
|
+
env(name: string): string | undefined;
|
|
22
|
+
homeDir(): string;
|
|
23
|
+
now(): Date;
|
|
24
|
+
}
|
|
25
|
+
/** Expand a leading `~/` against the host home directory. */
|
|
26
|
+
export declare const expandHome: (host: UsageHost, path: string) => string;
|
|
27
|
+
export declare const readJsonFile: <T>(host: UsageHost, path: string) => Promise<T | undefined>;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
// Host abstraction — the port of CodexBar's "Host APIs" concept. Providers never
|
|
3
|
+
// touch the filesystem, network, or environment directly; they go through a
|
|
4
|
+
// UsageHost so the same provider code runs in Node (chat/console/dev), Tauri
|
|
5
|
+
// (desktop/app), and tests.
|
|
6
|
+
/** Expand a leading `~/` against the host home directory. */
|
|
7
|
+
export const expandHome = (host, path) => path.startsWith('~/') ? `${host.homeDir()}/${path.slice(2)}` : path;
|
|
8
|
+
export const readJsonFile = async (host, path) => {
|
|
9
|
+
const text = await host.readTextFile(expandHome(host, path));
|
|
10
|
+
if (text === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(text);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
// Node host — used by chat's Express API, console/app server routes, and CLIs.
|
|
3
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
export const nodeHost = {
|
|
7
|
+
async readTextFile(path) {
|
|
8
|
+
try {
|
|
9
|
+
return await readFile(path, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
async listDir(path) {
|
|
16
|
+
try {
|
|
17
|
+
return await readdir(path);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
async writeTextFile(path, contents) {
|
|
24
|
+
await mkdir(dirname(path), { recursive: true });
|
|
25
|
+
await writeFile(path, contents, 'utf8');
|
|
26
|
+
},
|
|
27
|
+
async http(req) {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeout = setTimeout(() => controller.abort(), req.timeoutMs ?? 30_000);
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(req.url, {
|
|
32
|
+
method: req.method ?? 'GET',
|
|
33
|
+
headers: req.headers,
|
|
34
|
+
body: req.body,
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
});
|
|
37
|
+
const headers = {};
|
|
38
|
+
res.headers.forEach((v, k) => {
|
|
39
|
+
headers[k] = v;
|
|
40
|
+
});
|
|
41
|
+
return { status: res.status, headers, text: await res.text() };
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
env: (name) => process.env[name],
|
|
48
|
+
homeDir: () => homedir(),
|
|
49
|
+
now: () => new Date(),
|
|
50
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { UsageHost } from '../host.js';
|
|
2
|
+
type TauriFs = {
|
|
3
|
+
readTextFile(path: string): Promise<string>;
|
|
4
|
+
readDir(path: string): Promise<Array<{
|
|
5
|
+
name: string;
|
|
6
|
+
}>>;
|
|
7
|
+
writeTextFile(path: string, contents: string): Promise<void>;
|
|
8
|
+
mkdir(path: string, opts?: {
|
|
9
|
+
recursive?: boolean;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Build a UsageHost from Tauri plugin modules. Modules are passed in (not
|
|
14
|
+
* imported) so this file stays dependency-free for web/Node bundles:
|
|
15
|
+
*
|
|
16
|
+
* import * as fs from '@tauri-apps/plugin-fs'
|
|
17
|
+
* import { fetch } from '@tauri-apps/plugin-http'
|
|
18
|
+
* import { homeDir } from '@tauri-apps/api/path'
|
|
19
|
+
* const host = await createTauriHost({ fs, fetch, homeDir })
|
|
20
|
+
*/
|
|
21
|
+
export declare const createTauriHost: (deps: {
|
|
22
|
+
fs: TauriFs;
|
|
23
|
+
fetch: typeof fetch;
|
|
24
|
+
homeDir: () => Promise<string>;
|
|
25
|
+
}) => Promise<UsageHost>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
// Tauri host — used by hanzo desktop and the hanzo app launcher. Requires the
|
|
3
|
+
// fs + http plugins with scope covering the provider config paths
|
|
4
|
+
// (~/.codex, ~/.claude, ~/.hanzo) and the provider API domains.
|
|
5
|
+
/**
|
|
6
|
+
* Build a UsageHost from Tauri plugin modules. Modules are passed in (not
|
|
7
|
+
* imported) so this file stays dependency-free for web/Node bundles:
|
|
8
|
+
*
|
|
9
|
+
* import * as fs from '@tauri-apps/plugin-fs'
|
|
10
|
+
* import { fetch } from '@tauri-apps/plugin-http'
|
|
11
|
+
* import { homeDir } from '@tauri-apps/api/path'
|
|
12
|
+
* const host = await createTauriHost({ fs, fetch, homeDir })
|
|
13
|
+
*/
|
|
14
|
+
export const createTauriHost = async (deps) => {
|
|
15
|
+
const home = (await deps.homeDir()).replace(/\/$/, '');
|
|
16
|
+
return {
|
|
17
|
+
async readTextFile(path) {
|
|
18
|
+
try {
|
|
19
|
+
return await deps.fs.readTextFile(path);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
async listDir(path) {
|
|
26
|
+
try {
|
|
27
|
+
return (await deps.fs.readDir(path)).map((e) => e.name);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async writeTextFile(path, contents) {
|
|
34
|
+
const dir = path.slice(0, path.lastIndexOf('/'));
|
|
35
|
+
try {
|
|
36
|
+
await deps.fs.mkdir(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// exists
|
|
40
|
+
}
|
|
41
|
+
await deps.fs.writeTextFile(path, contents);
|
|
42
|
+
},
|
|
43
|
+
async http(req) {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const timeout = setTimeout(() => controller.abort(), req.timeoutMs ?? 30_000);
|
|
46
|
+
try {
|
|
47
|
+
const res = await deps.fetch(req.url, {
|
|
48
|
+
method: req.method ?? 'GET',
|
|
49
|
+
headers: req.headers,
|
|
50
|
+
body: req.body,
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
const headers = {};
|
|
54
|
+
res.headers.forEach((v, k) => {
|
|
55
|
+
headers[k] = v;
|
|
56
|
+
});
|
|
57
|
+
return { status: res.status, headers, text: await res.text() };
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
env: () => undefined,
|
|
64
|
+
homeDir: () => home,
|
|
65
|
+
now: () => new Date(),
|
|
66
|
+
};
|
|
67
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './host.js';
|
|
3
|
+
export * from './provider.js';
|
|
4
|
+
export * from './store.js';
|
|
5
|
+
export { codexProvider } from './providers/codex.js';
|
|
6
|
+
export { claudeProvider } from './providers/claude.js';
|
|
7
|
+
export { hanzoProvider } from './providers/hanzo.js';
|
|
8
|
+
import type { ProviderDescriptor } from './provider.js';
|
|
9
|
+
/** All built-in providers, registry-style like CodexBar's descriptor registry. */
|
|
10
|
+
export declare const providerRegistry: Record<string, ProviderDescriptor>;
|
|
11
|
+
export declare const allProviders: ProviderDescriptor[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './host.js';
|
|
4
|
+
export * from './provider.js';
|
|
5
|
+
export * from './store.js';
|
|
6
|
+
export { codexProvider } from './providers/codex.js';
|
|
7
|
+
export { claudeProvider } from './providers/claude.js';
|
|
8
|
+
export { hanzoProvider } from './providers/hanzo.js';
|
|
9
|
+
import { codexProvider } from './providers/codex.js';
|
|
10
|
+
import { claudeProvider } from './providers/claude.js';
|
|
11
|
+
import { hanzoProvider } from './providers/hanzo.js';
|
|
12
|
+
/** All built-in providers, registry-style like CodexBar's descriptor registry. */
|
|
13
|
+
export const providerRegistry = {
|
|
14
|
+
hanzo: hanzoProvider,
|
|
15
|
+
codex: codexProvider,
|
|
16
|
+
claude: claudeProvider,
|
|
17
|
+
};
|
|
18
|
+
export const allProviders = Object.values(providerRegistry);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CreditsSnapshot, UsageSnapshot } from './types.js';
|
|
2
|
+
import type { UsageHost } from './host.js';
|
|
3
|
+
export type ProviderSourceMode = 'auto' | 'web' | 'cli' | 'oauth' | 'api';
|
|
4
|
+
export type ProviderFetchKind = 'cli' | 'web' | 'oauth' | 'apiToken' | 'localProbe';
|
|
5
|
+
export interface ProviderSettings {
|
|
6
|
+
/** API key / secret for apiToken strategies. */
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
/** Base URL override (e.g. self-hosted gateway). */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/** Bearer-token resolver for cloud (IAM) strategies. */
|
|
11
|
+
getToken?: () => Promise<string | undefined>;
|
|
12
|
+
/** Extra provider-specific settings. */
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface ProviderFetchContext {
|
|
16
|
+
host: UsageHost;
|
|
17
|
+
sourceMode: ProviderSourceMode;
|
|
18
|
+
settings?: ProviderSettings;
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ProviderFetchResult {
|
|
22
|
+
usage: UsageSnapshot;
|
|
23
|
+
credits?: CreditsSnapshot;
|
|
24
|
+
/** Human-readable label of the source that produced the data. */
|
|
25
|
+
sourceLabel: string;
|
|
26
|
+
strategyId: string;
|
|
27
|
+
strategyKind: ProviderFetchKind;
|
|
28
|
+
}
|
|
29
|
+
export interface ProviderFetchStrategy {
|
|
30
|
+
/** e.g. "codex.oauth", "claude.web" */
|
|
31
|
+
id: string;
|
|
32
|
+
kind: ProviderFetchKind;
|
|
33
|
+
isAvailable(ctx: ProviderFetchContext): Promise<boolean>;
|
|
34
|
+
fetch(ctx: ProviderFetchContext): Promise<ProviderFetchResult>;
|
|
35
|
+
/** Whether the pipeline should try the next strategy after this error. */
|
|
36
|
+
shouldFallback?(error: unknown, ctx: ProviderFetchContext): boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ProviderFetchAttempt {
|
|
39
|
+
strategyId: string;
|
|
40
|
+
ok: boolean;
|
|
41
|
+
error?: string;
|
|
42
|
+
skipped?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ProviderFetchOutcome {
|
|
45
|
+
result?: ProviderFetchResult;
|
|
46
|
+
error?: unknown;
|
|
47
|
+
attempts: ProviderFetchAttempt[];
|
|
48
|
+
}
|
|
49
|
+
export interface ProviderMetadata {
|
|
50
|
+
displayName: string;
|
|
51
|
+
/** Label for the primary lane, e.g. "5h limit" / "Session". */
|
|
52
|
+
sessionLabel: string;
|
|
53
|
+
/** Label for the secondary lane, e.g. "Weekly limit". */
|
|
54
|
+
weeklyLabel: string;
|
|
55
|
+
supportsCredits?: boolean;
|
|
56
|
+
defaultEnabled?: boolean;
|
|
57
|
+
dashboardUrl?: string;
|
|
58
|
+
statusPageUrl?: string;
|
|
59
|
+
/** Brand accent as CSS color. */
|
|
60
|
+
color?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ProviderDescriptor {
|
|
63
|
+
id: string;
|
|
64
|
+
metadata: ProviderMetadata;
|
|
65
|
+
/** Strategies in priority order for a given source mode. */
|
|
66
|
+
strategies(mode: ProviderSourceMode): ProviderFetchStrategy[];
|
|
67
|
+
}
|
|
68
|
+
/** Filter a full priority list down to the requested source mode. */
|
|
69
|
+
export declare const forMode: (all: ProviderFetchStrategy[], mode: ProviderSourceMode) => ProviderFetchStrategy[];
|
|
70
|
+
/** Run strategies in priority order with availability checks and fallback. */
|
|
71
|
+
export declare const runPipeline: (descriptor: ProviderDescriptor, ctx: ProviderFetchContext) => Promise<ProviderFetchOutcome>;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
// Provider abstraction — TypeScript port of CodexBarCore's ProviderDescriptor /
|
|
3
|
+
// ProviderFetchPlan / ProviderFetchPipeline (priority-ordered strategies with
|
|
4
|
+
// availability checks and fallback).
|
|
5
|
+
const matchesMode = (kind, mode) => {
|
|
6
|
+
if (mode === 'auto')
|
|
7
|
+
return true;
|
|
8
|
+
if (mode === 'web')
|
|
9
|
+
return kind === 'web';
|
|
10
|
+
if (mode === 'cli')
|
|
11
|
+
return kind === 'cli' || kind === 'localProbe';
|
|
12
|
+
if (mode === 'oauth')
|
|
13
|
+
return kind === 'oauth';
|
|
14
|
+
return kind === 'apiToken';
|
|
15
|
+
};
|
|
16
|
+
/** Filter a full priority list down to the requested source mode. */
|
|
17
|
+
export const forMode = (all, mode) => all.filter((s) => matchesMode(s.kind, mode));
|
|
18
|
+
/** Run strategies in priority order with availability checks and fallback. */
|
|
19
|
+
export const runPipeline = async (descriptor, ctx) => {
|
|
20
|
+
const attempts = [];
|
|
21
|
+
let lastError;
|
|
22
|
+
for (const strategy of descriptor.strategies(ctx.sourceMode)) {
|
|
23
|
+
if (!(await strategy.isAvailable(ctx))) {
|
|
24
|
+
attempts.push({ strategyId: strategy.id, ok: false, skipped: true });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = await strategy.fetch(ctx);
|
|
29
|
+
attempts.push({ strategyId: strategy.id, ok: true });
|
|
30
|
+
return { result, attempts };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
attempts.push({
|
|
34
|
+
strategyId: strategy.id,
|
|
35
|
+
ok: false,
|
|
36
|
+
error: error instanceof Error ? error.message : String(error),
|
|
37
|
+
});
|
|
38
|
+
lastError = error;
|
|
39
|
+
const fallback = strategy.shouldFallback?.(error, ctx) ?? true;
|
|
40
|
+
if (!fallback)
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { error: lastError ?? new Error(`${descriptor.id}: no strategy available`), attempts };
|
|
45
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
// Claude (Anthropic) provider — port of CodexBarCore/Providers/Claude.
|
|
3
|
+
// OAuth strategy reads Claude Code credentials (~/.claude/.credentials.json)
|
|
4
|
+
// and calls the Anthropic OAuth usage endpoint; web strategy uses a claude.ai
|
|
5
|
+
// sessionKey cookie supplied via settings.
|
|
6
|
+
import { forMode } from '../provider.js';
|
|
7
|
+
import { readJsonFile } from '../host.js';
|
|
8
|
+
const WINDOW_MINUTES = {
|
|
9
|
+
five_hour: 300,
|
|
10
|
+
seven_day: 10_080,
|
|
11
|
+
seven_day_sonnet: 10_080,
|
|
12
|
+
seven_day_opus: 10_080,
|
|
13
|
+
};
|
|
14
|
+
const toWindow = (key, w) => {
|
|
15
|
+
const used = w?.utilization ?? w?.used_percent;
|
|
16
|
+
if (typeof used !== 'number')
|
|
17
|
+
return undefined;
|
|
18
|
+
return { usedPercent: used, windowMinutes: WINDOW_MINUTES[key], resetsAt: w?.resets_at };
|
|
19
|
+
};
|
|
20
|
+
const mapUsage = (body, now, loginMethod, plan) => {
|
|
21
|
+
const extras = [];
|
|
22
|
+
for (const [key, title] of [
|
|
23
|
+
['seven_day_sonnet', 'Sonnet weekly'],
|
|
24
|
+
['seven_day_opus', 'Opus weekly'],
|
|
25
|
+
]) {
|
|
26
|
+
const window = toWindow(key, body[key]);
|
|
27
|
+
if (window)
|
|
28
|
+
extras.push({ id: key, title, window, usageKnown: true });
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
providerId: 'claude',
|
|
32
|
+
primary: toWindow('five_hour', body.five_hour),
|
|
33
|
+
secondary: toWindow('seven_day', body.seven_day),
|
|
34
|
+
extraRateWindows: extras.length ? extras : undefined,
|
|
35
|
+
identity: {
|
|
36
|
+
providerId: 'claude',
|
|
37
|
+
plan: plan ?? body.subscriptionType ?? body.rate_limit_tier,
|
|
38
|
+
loginMethod,
|
|
39
|
+
},
|
|
40
|
+
providerCost: body.extra_usage && typeof body.extra_usage.used_cents === 'number'
|
|
41
|
+
? {
|
|
42
|
+
used: body.extra_usage.used_cents / 100,
|
|
43
|
+
limit: typeof body.extra_usage.limit_cents === 'number'
|
|
44
|
+
? body.extra_usage.limit_cents / 100
|
|
45
|
+
: undefined,
|
|
46
|
+
currencyCode: 'USD',
|
|
47
|
+
period: 'monthly',
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
}
|
|
50
|
+
: undefined,
|
|
51
|
+
dataConfidence: 'percentOnly',
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const credentialPaths = ['~/.claude/.credentials.json', '~/.config/claude/.credentials.json'];
|
|
56
|
+
const readCredentials = async (ctx) => {
|
|
57
|
+
for (const path of credentialPaths) {
|
|
58
|
+
const file = await readJsonFile(ctx.host, path);
|
|
59
|
+
if (file?.claudeAiOauth?.accessToken)
|
|
60
|
+
return file.claudeAiOauth;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
const oauthStrategy = {
|
|
65
|
+
id: 'claude.oauth',
|
|
66
|
+
kind: 'oauth',
|
|
67
|
+
async isAvailable(ctx) {
|
|
68
|
+
return Boolean(await readCredentials(ctx));
|
|
69
|
+
},
|
|
70
|
+
async fetch(ctx) {
|
|
71
|
+
const creds = await readCredentials(ctx);
|
|
72
|
+
if (!creds?.accessToken)
|
|
73
|
+
throw new Error('claude: no OAuth credentials');
|
|
74
|
+
const res = await ctx.host.http({
|
|
75
|
+
url: 'https://api.anthropic.com/api/oauth/usage',
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
78
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
79
|
+
Accept: 'application/json',
|
|
80
|
+
},
|
|
81
|
+
timeoutMs: 30_000,
|
|
82
|
+
});
|
|
83
|
+
if (res.status !== 200)
|
|
84
|
+
throw new Error(`claude usage HTTP ${res.status}`);
|
|
85
|
+
const body = JSON.parse(res.text);
|
|
86
|
+
return {
|
|
87
|
+
usage: mapUsage(body, ctx.host.now().toISOString(), 'oauth', creds.subscriptionType),
|
|
88
|
+
sourceLabel: 'Claude OAuth',
|
|
89
|
+
strategyId: this.id,
|
|
90
|
+
strategyKind: this.kind,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
/** Web strategy: settings.cookieHeader must hold `sessionKey=sk-ant-…`. */
|
|
95
|
+
const webStrategy = {
|
|
96
|
+
id: 'claude.web',
|
|
97
|
+
kind: 'web',
|
|
98
|
+
async isAvailable(ctx) {
|
|
99
|
+
return typeof ctx.settings?.cookieHeader === 'string';
|
|
100
|
+
},
|
|
101
|
+
async fetch(ctx) {
|
|
102
|
+
const cookie = String(ctx.settings?.cookieHeader);
|
|
103
|
+
const get = async (path) => {
|
|
104
|
+
const res = await ctx.host.http({
|
|
105
|
+
url: `https://claude.ai/api${path}`,
|
|
106
|
+
headers: { Cookie: cookie, Accept: 'application/json' },
|
|
107
|
+
timeoutMs: 30_000,
|
|
108
|
+
});
|
|
109
|
+
if (res.status !== 200)
|
|
110
|
+
throw new Error(`claude.ai${path} HTTP ${res.status}`);
|
|
111
|
+
return JSON.parse(res.text);
|
|
112
|
+
};
|
|
113
|
+
const orgs = (await get('/organizations'));
|
|
114
|
+
const orgId = orgs[0]?.uuid;
|
|
115
|
+
if (!orgId)
|
|
116
|
+
throw new Error('claude: no organization for session');
|
|
117
|
+
const body = (await get(`/organizations/${orgId}/usage`));
|
|
118
|
+
return {
|
|
119
|
+
usage: mapUsage(body, ctx.host.now().toISOString(), 'web'),
|
|
120
|
+
sourceLabel: 'claude.ai session',
|
|
121
|
+
strategyId: this.id,
|
|
122
|
+
strategyKind: this.kind,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
export const claudeProvider = {
|
|
127
|
+
id: 'claude',
|
|
128
|
+
metadata: {
|
|
129
|
+
displayName: 'Claude',
|
|
130
|
+
sessionLabel: 'Session (5h)',
|
|
131
|
+
weeklyLabel: 'Weekly limit',
|
|
132
|
+
defaultEnabled: true,
|
|
133
|
+
dashboardUrl: 'https://claude.ai/settings/usage',
|
|
134
|
+
statusPageUrl: 'https://status.anthropic.com',
|
|
135
|
+
color: '#d97757',
|
|
136
|
+
},
|
|
137
|
+
strategies: (mode) => forMode([oauthStrategy, webStrategy], mode),
|
|
138
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Copyright (c) 2026 Hanzo AI Inc. MIT License.
|
|
2
|
+
// Codex (OpenAI) provider — port of CodexBarCore/Providers/Codex (OAuth strategy).
|
|
3
|
+
// Reads CLI credentials from $CODEX_HOME/auth.json (default ~/.codex/auth.json)
|
|
4
|
+
// and calls the ChatGPT backend usage endpoint.
|
|
5
|
+
import { forMode } from '../provider.js';
|
|
6
|
+
import { expandHome, readJsonFile } from '../host.js';
|
|
7
|
+
const codexHome = (ctx) => ctx.host.env('CODEX_HOME') ?? expandHome(ctx.host, '~/.codex');
|
|
8
|
+
const toWindow = (w) => {
|
|
9
|
+
if (!w || typeof w.used_percent !== 'number')
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
usedPercent: w.used_percent,
|
|
13
|
+
windowMinutes: w.limit_window_seconds ? Math.round(w.limit_window_seconds / 60) : undefined,
|
|
14
|
+
resetsAt: w.reset_at ? new Date(w.reset_at * 1000).toISOString() : undefined,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
const oauthStrategy = {
|
|
18
|
+
id: 'codex.oauth',
|
|
19
|
+
kind: 'oauth',
|
|
20
|
+
async isAvailable(ctx) {
|
|
21
|
+
const auth = await readJsonFile(ctx.host, `${codexHome(ctx)}/auth.json`);
|
|
22
|
+
return Boolean(auth?.tokens?.access_token);
|
|
23
|
+
},
|
|
24
|
+
async fetch(ctx) {
|
|
25
|
+
const auth = await readJsonFile(ctx.host, `${codexHome(ctx)}/auth.json`);
|
|
26
|
+
const token = auth?.tokens?.access_token;
|
|
27
|
+
if (!token)
|
|
28
|
+
throw new Error('codex: no access token in auth.json');
|
|
29
|
+
const headers = {
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
Accept: 'application/json',
|
|
32
|
+
'User-Agent': 'HanzoUsage',
|
|
33
|
+
};
|
|
34
|
+
if (auth?.tokens?.account_id)
|
|
35
|
+
headers['ChatGPT-Account-Id'] = auth.tokens.account_id;
|
|
36
|
+
const res = await ctx.host.http({
|
|
37
|
+
url: 'https://chatgpt.com/backend-api/wham/usage',
|
|
38
|
+
headers,
|
|
39
|
+
timeoutMs: 30_000,
|
|
40
|
+
});
|
|
41
|
+
if (res.status !== 200)
|
|
42
|
+
throw new Error(`codex usage HTTP ${res.status}`);
|
|
43
|
+
const body = JSON.parse(res.text);
|
|
44
|
+
const now = ctx.host.now().toISOString();
|
|
45
|
+
const usage = {
|
|
46
|
+
providerId: 'codex',
|
|
47
|
+
primary: toWindow(body.rate_limit?.primary_window),
|
|
48
|
+
secondary: toWindow(body.rate_limit?.secondary_window),
|
|
49
|
+
extraRateWindows: (body.additional_rate_limits ?? [])
|
|
50
|
+
.map((extra) => {
|
|
51
|
+
const window = toWindow(extra.rate_limit?.primary_window);
|
|
52
|
+
if (!window || !extra.limit_name)
|
|
53
|
+
return undefined;
|
|
54
|
+
return { id: extra.limit_name, title: extra.limit_name, window, usageKnown: true };
|
|
55
|
+
})
|
|
56
|
+
.filter((w) => Boolean(w)),
|
|
57
|
+
identity: { providerId: 'codex', plan: body.plan_type, loginMethod: 'oauth' },
|
|
58
|
+
dataConfidence: 'percentOnly',
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
};
|
|
61
|
+
let credits;
|
|
62
|
+
if (body.credits?.has_credits) {
|
|
63
|
+
credits = {
|
|
64
|
+
remaining: body.credits.balance ?? 0,
|
|
65
|
+
unlimited: body.credits.unlimited,
|
|
66
|
+
updatedAt: now,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
usage,
|
|
71
|
+
credits,
|
|
72
|
+
sourceLabel: 'OpenAI OAuth',
|
|
73
|
+
strategyId: this.id,
|
|
74
|
+
strategyKind: this.kind,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
export const codexProvider = {
|
|
79
|
+
id: 'codex',
|
|
80
|
+
metadata: {
|
|
81
|
+
displayName: 'Codex',
|
|
82
|
+
sessionLabel: '5h limit',
|
|
83
|
+
weeklyLabel: 'Weekly limit',
|
|
84
|
+
supportsCredits: true,
|
|
85
|
+
defaultEnabled: true,
|
|
86
|
+
dashboardUrl: 'https://chatgpt.com/codex/settings/usage',
|
|
87
|
+
statusPageUrl: 'https://status.openai.com',
|
|
88
|
+
color: '#10a37f',
|
|
89
|
+
},
|
|
90
|
+
strategies: (mode) => forMode([oauthStrategy], mode),
|
|
91
|
+
};
|