axusage 2.0.0 → 2.2.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.
@@ -1,5 +1,5 @@
1
- import { extractRawCredentials, getAccessToken } from "axauth";
2
1
  import { ApiError } from "../types/domain.js";
2
+ import { getServiceAccessToken } from "../services/get-service-access-token.js";
3
3
  import { ChatGPTUsageResponse as ChatGPTUsageResponseSchema } from "../types/chatgpt.js";
4
4
  import { toServiceUsageData } from "./parse-chatgpt-usage.js";
5
5
  const API_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -12,24 +12,11 @@ const API_URL = "https://chatgpt.com/backend-api/wham/usage";
12
12
  export const chatGPTAdapter = {
13
13
  name: "ChatGPT",
14
14
  async fetchUsage() {
15
- const credentials = extractRawCredentials("codex");
16
- if (!credentials) {
17
- return {
18
- ok: false,
19
- error: new ApiError("No Codex credentials found. Run 'codex' to authenticate."),
20
- };
21
- }
22
- if (credentials.type !== "oauth") {
23
- return {
24
- ok: false,
25
- error: new ApiError("ChatGPT usage API requires OAuth authentication. API key authentication is not supported for usage data."),
26
- };
27
- }
28
- const accessToken = getAccessToken(credentials);
15
+ const accessToken = await getServiceAccessToken("chatgpt");
29
16
  if (!accessToken) {
30
17
  return {
31
18
  ok: false,
32
- error: new ApiError("Invalid OAuth credentials: missing access token."),
19
+ error: new ApiError("No Codex credentials found. Run 'codex' to authenticate."),
33
20
  };
34
21
  }
35
22
  try {
@@ -1,6 +1,6 @@
1
- import { extractRawCredentials, getAccessToken } from "axauth";
2
1
  import { z } from "zod";
3
2
  import { ApiError } from "../types/domain.js";
3
+ import { getServiceAccessToken } from "../services/get-service-access-token.js";
4
4
  import { UsageResponse as UsageResponseSchema } from "../types/usage.js";
5
5
  import { coalesceClaudeUsageResponse } from "./coalesce-claude-usage-response.js";
6
6
  import { toServiceUsageData } from "./parse-claude-usage.js";
@@ -43,24 +43,11 @@ async function fetchPlanType(accessToken) {
43
43
  export const claudeAdapter = {
44
44
  name: "Claude",
45
45
  async fetchUsage() {
46
- const credentials = extractRawCredentials("claude");
47
- if (!credentials) {
48
- return {
49
- ok: false,
50
- error: new ApiError("No Claude Code credentials found. Ensure Claude Code is installed and authenticated."),
51
- };
52
- }
53
- if (credentials.type !== "oauth") {
54
- return {
55
- ok: false,
56
- error: new ApiError("Claude Code usage API requires OAuth authentication. API key authentication is not supported for usage data."),
57
- };
58
- }
59
- const accessToken = getAccessToken(credentials);
46
+ const accessToken = await getServiceAccessToken("claude");
60
47
  if (!accessToken) {
61
48
  return {
62
49
  ok: false,
63
- error: new ApiError("Invalid OAuth credentials: missing access token."),
50
+ error: new ApiError("No Claude credentials found. Run 'claude' to authenticate."),
64
51
  };
65
52
  }
66
53
  try {
@@ -1,6 +1,6 @@
1
- import { getAgentAccessToken } from "axauth";
2
1
  import { ApiError } from "../types/domain.js";
3
2
  import { fetchGeminiQuota, fetchGeminiProject, } from "../services/gemini-api.js";
3
+ import { getServiceAccessToken } from "../services/get-service-access-token.js";
4
4
  import { toServiceUsageData } from "./parse-gemini-usage.js";
5
5
  /**
6
6
  * Gemini service adapter using direct API access.
@@ -11,7 +11,7 @@ import { toServiceUsageData } from "./parse-gemini-usage.js";
11
11
  export const geminiAdapter = {
12
12
  name: "Gemini",
13
13
  async fetchUsage() {
14
- const accessToken = getAgentAccessToken("gemini");
14
+ const accessToken = await getServiceAccessToken("gemini");
15
15
  if (!accessToken) {
16
16
  return {
17
17
  ok: false,
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Configuration for credential sources per service.
3
+ *
4
+ * Supports three modes:
5
+ * - "local": Use local credentials from axauth (default behavior)
6
+ * - "vault": Fetch credentials from axvault server
7
+ * - "auto": Try vault first if configured and credential name provided, fallback to local
8
+ */
9
+ import { z } from "zod";
10
+ /** Credential source type */
11
+ declare const CredentialSourceType: z.ZodEnum<{
12
+ auto: "auto";
13
+ local: "local";
14
+ vault: "vault";
15
+ }>;
16
+ type CredentialSourceType = z.infer<typeof CredentialSourceType>;
17
+ /** Resolved source config with normalized fields */
18
+ interface ResolvedSourceConfig {
19
+ source: CredentialSourceType;
20
+ name: string | undefined;
21
+ }
22
+ /** Service IDs that support vault credentials (API-based services) */
23
+ type VaultSupportedServiceId = "claude" | "chatgpt" | "gemini";
24
+ /**
25
+ * All service IDs.
26
+ * Note: github-copilot uses GitHub token auth, not vault credentials,
27
+ * so it's excluded from VaultSupportedServiceId.
28
+ */
29
+ type ServiceId = VaultSupportedServiceId | "github-copilot";
30
+ /**
31
+ * Get the resolved source config for a specific service.
32
+ *
33
+ * @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
34
+ * @returns Resolved config with source type and optional credential name
35
+ */
36
+ declare function getServiceSourceConfig(service: ServiceId): ResolvedSourceConfig;
37
+ export type { ServiceId, VaultSupportedServiceId };
38
+ export { getServiceSourceConfig };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Configuration for credential sources per service.
3
+ *
4
+ * Supports three modes:
5
+ * - "local": Use local credentials from axauth (default behavior)
6
+ * - "vault": Fetch credentials from axvault server
7
+ * - "auto": Try vault first if configured and credential name provided, fallback to local
8
+ */
9
+ import Conf from "conf";
10
+ import { z } from "zod";
11
+ /** Credential source type */
12
+ const CredentialSourceType = z.enum(["auto", "local", "vault"]);
13
+ /** Service source config - either a string shorthand or object with name */
14
+ const ServiceSourceConfig = z.union([
15
+ CredentialSourceType,
16
+ z.object({
17
+ source: CredentialSourceType,
18
+ name: z.string().optional(),
19
+ }),
20
+ ]);
21
+ /** Full sources config - map of service ID to source config */
22
+ const SourcesConfig = z.record(z.string(), ServiceSourceConfig);
23
+ // Lazy-initialized config instance
24
+ let configInstance;
25
+ function getConfig() {
26
+ if (!configInstance) {
27
+ configInstance = new Conf({
28
+ projectName: "axusage",
29
+ schema: {
30
+ sources: {
31
+ type: "object",
32
+ additionalProperties: true,
33
+ },
34
+ },
35
+ });
36
+ }
37
+ return configInstance;
38
+ }
39
+ /**
40
+ * Get the full credential source configuration.
41
+ *
42
+ * Priority:
43
+ * 1. AXUSAGE_SOURCES environment variable (flat JSON)
44
+ * 2. Config file sources key
45
+ * 3. Empty object (defaults apply per-service)
46
+ */
47
+ function getCredentialSourceConfig() {
48
+ // Priority 1: Environment variable
49
+ const environmentVariable = process.env.AXUSAGE_SOURCES;
50
+ if (environmentVariable) {
51
+ try {
52
+ const parsed = SourcesConfig.parse(JSON.parse(environmentVariable));
53
+ return parsed;
54
+ }
55
+ catch (error) {
56
+ const reason = error instanceof SyntaxError
57
+ ? "invalid JSON syntax"
58
+ : "schema validation failed";
59
+ console.error(`Warning: AXUSAGE_SOURCES ${reason}, falling back to config file`);
60
+ }
61
+ }
62
+ // Priority 2: Config file
63
+ const config = getConfig();
64
+ const fileConfig = config.get("sources");
65
+ if (fileConfig) {
66
+ const parsed = SourcesConfig.safeParse(fileConfig);
67
+ if (parsed.success) {
68
+ return parsed.data;
69
+ }
70
+ console.error("Warning: Config file contains invalid sources, using defaults");
71
+ }
72
+ // Priority 3: Empty (defaults apply)
73
+ return {};
74
+ }
75
+ /**
76
+ * Get the resolved source config for a specific service.
77
+ *
78
+ * @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
79
+ * @returns Resolved config with source type and optional credential name
80
+ */
81
+ function getServiceSourceConfig(service) {
82
+ const config = getCredentialSourceConfig();
83
+ const serviceConfig = config[service];
84
+ // Default: auto mode with no credential name
85
+ if (serviceConfig === undefined) {
86
+ return { source: "auto", name: undefined };
87
+ }
88
+ // String shorthand: just the source type
89
+ if (typeof serviceConfig === "string") {
90
+ return { source: serviceConfig, name: undefined };
91
+ }
92
+ // Object: source and name
93
+ return { source: serviceConfig.source, name: serviceConfig.name };
94
+ }
95
+ export { getServiceSourceConfig };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Unified credential fetcher for services.
3
+ *
4
+ * Fetches access tokens based on per-service configuration:
5
+ * - "local": From local axauth credential store
6
+ * - "vault": From axvault server
7
+ * - "auto": Try vault first if configured, fallback to local
8
+ */
9
+ import { type VaultSupportedServiceId } from "../config/credential-sources.js";
10
+ /**
11
+ * Get access token for a service.
12
+ *
13
+ * Uses the configured credential source for the service:
14
+ * - "local": Fetch from local axauth credential store
15
+ * - "vault": Fetch from axvault server (requires credential name)
16
+ * - "auto": Try vault if configured and name provided, fallback to local
17
+ *
18
+ * @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
19
+ * @returns Access token string or undefined if not available
20
+ *
21
+ * @example
22
+ * const token = await getServiceAccessToken("claude");
23
+ * if (!token) {
24
+ * console.error("No credentials found for Claude");
25
+ * }
26
+ */
27
+ declare function getServiceAccessToken(service: VaultSupportedServiceId): Promise<string | undefined>;
28
+ export { getServiceAccessToken };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Unified credential fetcher for services.
3
+ *
4
+ * Fetches access tokens based on per-service configuration:
5
+ * - "local": From local axauth credential store
6
+ * - "vault": From axvault server
7
+ * - "auto": Try vault first if configured, fallback to local
8
+ */
9
+ import { fetchVaultCredentials, getAgentAccessToken, isVaultConfigured, } from "axauth";
10
+ import { getServiceSourceConfig, } from "../config/credential-sources.js";
11
+ /** Map service IDs to agent IDs for axauth/vault */
12
+ const SERVICE_TO_AGENT = {
13
+ claude: "claude",
14
+ chatgpt: "codex", // ChatGPT and Codex both use OpenAI API credentials
15
+ gemini: "gemini",
16
+ };
17
+ /**
18
+ * Extract access token from vault credentials.
19
+ *
20
+ * Different credential types store tokens differently:
21
+ * - oauth-credentials: access_token (Gemini style) or tokens.access_token (Codex/OpenAI style)
22
+ * - oauth-token: accessToken field (Claude style)
23
+ * - api-key: apiKey field
24
+ */
25
+ function extractAccessToken(credentials) {
26
+ if (!credentials)
27
+ return undefined;
28
+ const { data } = credentials;
29
+ // Try accessToken first (Claude oauth-token style, camelCase)
30
+ if (typeof data.accessToken === "string") {
31
+ return data.accessToken;
32
+ }
33
+ // Try access_token at top level (Gemini oauth-credentials style, snake_case)
34
+ if (typeof data.access_token === "string") {
35
+ return data.access_token;
36
+ }
37
+ // Try tokens.access_token (Codex/OpenAI oauth-credentials style)
38
+ if (data.tokens &&
39
+ typeof data.tokens === "object" &&
40
+ "access_token" in data.tokens &&
41
+ typeof data.tokens.access_token === "string") {
42
+ return data.tokens.access_token;
43
+ }
44
+ // Try apiKey as fallback (api-key type)
45
+ if (typeof data.apiKey === "string") {
46
+ return data.apiKey;
47
+ }
48
+ return undefined;
49
+ }
50
+ /**
51
+ * Fetch access token from vault.
52
+ *
53
+ * @returns Access token string or undefined if not available
54
+ */
55
+ async function fetchFromVault(agentId, credentialName) {
56
+ try {
57
+ const result = await fetchVaultCredentials({
58
+ agentId,
59
+ name: credentialName,
60
+ });
61
+ if (!result.ok) {
62
+ // Log warning for debugging, but don't fail hard
63
+ if (result.reason !== "not-configured" && result.reason !== "not-found") {
64
+ console.error(`[axusage] Vault fetch failed for ${agentId}/${credentialName}: ${result.reason}`);
65
+ }
66
+ return undefined;
67
+ }
68
+ const token = extractAccessToken(result.credentials);
69
+ if (!token) {
70
+ console.error(`[axusage] Vault credentials for ${agentId}/${credentialName} missing access token. ` +
71
+ `Credential type: ${result.credentials.type}`);
72
+ }
73
+ return token;
74
+ }
75
+ catch (error) {
76
+ console.error(`[axusage] Vault fetch error for ${agentId}/${credentialName}: ${error instanceof Error ? error.message : String(error)}`);
77
+ return undefined;
78
+ }
79
+ }
80
+ /**
81
+ * Fetch access token from local credential store.
82
+ *
83
+ * @returns Access token string or undefined if not available
84
+ */
85
+ async function fetchFromLocal(agentId) {
86
+ try {
87
+ return await getAgentAccessToken(agentId);
88
+ }
89
+ catch (error) {
90
+ console.error(`[axusage] Local credential fetch error for ${agentId}: ${error instanceof Error ? error.message : String(error)}`);
91
+ return undefined;
92
+ }
93
+ }
94
+ /**
95
+ * Get access token for a service.
96
+ *
97
+ * Uses the configured credential source for the service:
98
+ * - "local": Fetch from local axauth credential store
99
+ * - "vault": Fetch from axvault server (requires credential name)
100
+ * - "auto": Try vault if configured and name provided, fallback to local
101
+ *
102
+ * @param service - Service ID (e.g., "claude", "chatgpt", "gemini")
103
+ * @returns Access token string or undefined if not available
104
+ *
105
+ * @example
106
+ * const token = await getServiceAccessToken("claude");
107
+ * if (!token) {
108
+ * console.error("No credentials found for Claude");
109
+ * }
110
+ */
111
+ async function getServiceAccessToken(service) {
112
+ const config = getServiceSourceConfig(service);
113
+ const agentId = SERVICE_TO_AGENT[service];
114
+ switch (config.source) {
115
+ case "local": {
116
+ return fetchFromLocal(agentId);
117
+ }
118
+ case "vault": {
119
+ if (!config.name) {
120
+ console.error(`[axusage] Vault source requires credential name for ${service}. ` +
121
+ `Set {"${service}": {"source": "vault", "name": "your-name"}} in config.`);
122
+ return undefined;
123
+ }
124
+ const token = await fetchFromVault(agentId, config.name);
125
+ if (!token) {
126
+ // User explicitly selected vault but it failed - provide clear feedback
127
+ console.error(`[axusage] Vault credential fetch failed for ${service}. ` +
128
+ `Check that vault is configured (AXVAULT env) and credential "${config.name}" exists.`);
129
+ }
130
+ return token;
131
+ }
132
+ case "auto": {
133
+ // Auto mode: try vault first if configured and name provided
134
+ if (config.name && isVaultConfigured()) {
135
+ const vaultToken = await fetchFromVault(agentId, config.name);
136
+ if (vaultToken) {
137
+ return vaultToken;
138
+ }
139
+ // Fallback to local if vault failed
140
+ }
141
+ // No credential name or vault not configured: use local only
142
+ return fetchFromLocal(agentId);
143
+ }
144
+ }
145
+ }
146
+ export { getServiceAccessToken };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "axusage",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "2.0.0",
5
+ "version": "2.2.0",
6
6
  "description": "Monitor API usage across Claude, ChatGPT, GitHub Copilot, and Gemini from a single CLI",
7
7
  "repository": {
8
8
  "type": "git",
@@ -18,6 +18,24 @@
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
+ "scripts": {
22
+ "postinstall": "playwright install chromium",
23
+ "prepare": "git config core.hooksPath .githooks",
24
+ "prepublishOnly": "pnpm run rebuild",
25
+ "build": "tsc -p tsconfig.app.json",
26
+ "clean": "rm -rf dist *.tsbuildinfo",
27
+ "format": "prettier --write .",
28
+ "format:check": "prettier --check .",
29
+ "fta": "fta-check",
30
+ "knip": "knip",
31
+ "lint": "eslint",
32
+ "rebuild": "pnpm run clean && pnpm run build",
33
+ "start": "pnpm run rebuild && node bin/axusage",
34
+ "test": "vitest run",
35
+ "test:coverage": "vitest run --coverage",
36
+ "test:watch": "vitest",
37
+ "typecheck": "tsc -b --noEmit"
38
+ },
21
39
  "keywords": [
22
40
  "ai",
23
41
  "usage",
@@ -30,54 +48,34 @@
30
48
  "llm",
31
49
  "monitoring"
32
50
  ],
51
+ "packageManager": "pnpm@10.25.0",
33
52
  "engines": {
34
53
  "node": ">=22.14.0"
35
54
  },
36
55
  "dependencies": {
37
56
  "@commander-js/extra-typings": "^14.0.0",
38
- "axauth": "^1.0.0",
57
+ "axauth": "^1.11.2",
39
58
  "chalk": "^5.6.2",
40
59
  "commander": "^14.0.2",
60
+ "conf": "^15.0.2",
41
61
  "env-paths": "^3.0.0",
42
62
  "playwright": "^1.57.0",
43
63
  "prom-client": "^15.1.3",
44
64
  "trash": "^10.0.1",
45
- "zod": "^4.2.0"
65
+ "zod": "^4.3.5"
46
66
  },
47
67
  "devDependencies": {
48
- "@eslint/compat": "^2.0.0",
49
- "@eslint/js": "^9.39.2",
50
68
  "@total-typescript/ts-reset": "^0.6.1",
51
- "@types/node": "^25.0.2",
52
- "@vitest/coverage-v8": "^4.0.15",
53
- "@vitest/eslint-plugin": "^1.5.2",
69
+ "@types/node": "^25.0.8",
70
+ "@vitest/coverage-v8": "^4.0.17",
54
71
  "eslint": "^9.39.2",
55
- "eslint-config-prettier": "^10.1.8",
56
- "eslint-plugin-unicorn": "^62.0.0",
57
- "fta-check": "^1.5.0",
72
+ "eslint-config-axkit": "^1.0.0",
73
+ "fta-check": "^1.5.1",
58
74
  "fta-cli": "^3.0.0",
59
- "globals": "^16.5.0",
60
- "knip": "^5.73.4",
75
+ "knip": "^5.81.0",
61
76
  "prettier": "3.7.4",
62
77
  "semantic-release": "^25.0.2",
63
78
  "typescript": "^5.9.3",
64
- "typescript-eslint": "^8.49.0",
65
- "vitest": "^4.0.15"
66
- },
67
- "scripts": {
68
- "postinstall": "playwright install chromium",
69
- "build": "tsc -p tsconfig.app.json",
70
- "clean": "rm -rf dist *.tsbuildinfo",
71
- "format": "prettier --write .",
72
- "format:check": "prettier --check .",
73
- "fta": "fta-check",
74
- "knip": "knip",
75
- "lint": "eslint",
76
- "rebuild": "pnpm run clean && pnpm run build",
77
- "start": "pnpm run rebuild && node bin/axusage",
78
- "test": "vitest run",
79
- "test:coverage": "vitest run --coverage",
80
- "test:watch": "vitest",
81
- "typecheck": "tsc -b --noEmit"
79
+ "vitest": "^4.0.17"
82
80
  }
83
- }
81
+ }