tokenmaxxing 1.20.0 → 1.23.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.
Files changed (38) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +3 -30
  3. package/package.json +1 -7
  4. package/src/cli/check.ts +0 -3
  5. package/src/cli/config.ts +0 -1
  6. package/src/cli/status.ts +2 -12
  7. package/src/entries/codexstophook.ts +0 -1
  8. package/src/entries/stopfailurehook.ts +6 -9
  9. package/src/entries/stophook.ts +0 -1
  10. package/src/entries/supervisor.ts +1 -2
  11. package/src/lib/codexdecide.ts +1 -25
  12. package/src/lib/codexpick.ts +3 -3
  13. package/src/lib/codexusage.ts +0 -9
  14. package/src/lib/decide.ts +4 -69
  15. package/src/lib/paths.ts +0 -1
  16. package/src/lib/state.ts +2 -82
  17. package/src/lib/types.ts +0 -53
  18. package/agent-plugin/bin/tokenmaxxing-mcp +0 -7
  19. package/agent-plugin/mcp.json +0 -10
  20. package/agent-plugin/plugin.json +0 -20
  21. package/agent-plugin/skills/codex-pool/SKILL.md +0 -23
  22. package/agent-plugin/skills/codex-pool/references/codex.md +0 -5
  23. package/agent-plugin/skills/credentials-hygiene/SKILL.md +0 -27
  24. package/agent-plugin/skills/credentials-hygiene/references/credentials.md +0 -6
  25. package/agent-plugin/skills/doctor-diagnostics/SKILL.md +0 -26
  26. package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +0 -5
  27. package/agent-plugin/skills/pool-status/SKILL.md +0 -27
  28. package/agent-plugin/skills/pool-status/references/commands.md +0 -9
  29. package/agent-plugin/skills/safe-contribution/SKILL.md +0 -27
  30. package/agent-plugin/skills/safe-contribution/references/ship.md +0 -5
  31. package/agent-plugin/skills/sdk-pairing/SKILL.md +0 -34
  32. package/agent-plugin/skills/sdk-pairing/references/sdk.md +0 -6
  33. package/agent-plugin/skills/switching-policy/SKILL.md +0 -35
  34. package/agent-plugin/skills/switching-policy/references/policy.md +0 -10
  35. package/src/entries/mcp.ts +0 -277
  36. package/src/lib/bankedreset.ts +0 -234
  37. package/src/lib/codexreset.ts +0 -122
  38. package/src/sdk.ts +0 -55
@@ -1,277 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { z } from "zod";
4
- import { readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { cmdLs } from "../cli/ls.ts";
7
- import { cmdStatus } from "../cli/status.ts";
8
- import { cmdDoctor } from "../cli/doctor.ts";
9
- import { cmdConfig } from "../cli/config.ts";
10
- import { cmdSwitch } from "../cli/switch.ts";
11
- import { cmdCodexSwitch } from "../cli/codexswitch.ts";
12
- import { cmdCheck } from "../cli/check.ts";
13
-
14
- const MUTATIONS_ENV = "TOKENMAXXING_AGENT_MUTATIONS";
15
- const PACKAGE_ROOT = join(import.meta.dir, "../..");
16
-
17
- function packageVersion(): string {
18
- try {
19
- const raw = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")) as { version?: string };
20
- return raw.version ?? "0.0.0";
21
- } catch {
22
- return "0.0.0";
23
- }
24
- }
25
-
26
- export function refuseAmbientStoreEnv(): string | null {
27
- const nonEmpty = (v: string | undefined) => (v != null && v !== "" ? v : null);
28
- return nonEmpty(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR) ?? nonEmpty(process.env.CLAUDE_CONFIG_DIR);
29
- }
30
-
31
- export function scrubSecrets(text: string): string {
32
- return text
33
- .replace(/\b(Bearer\s+)[A-Za-z0-9._\-+/=]+/gi, "$1[redacted]")
34
- .replace(/\b(sk-ant-[A-Za-z0-9_-]+)\b/g, "[redacted]")
35
- .replace(/\b(accessToken|refreshToken|claudeAiOauth)\b\s*[:=]\s*["']?[^"'}\s,]+/gi, "$1=[redacted]");
36
- }
37
-
38
- type CaptureResult = { code: number; stdout: string; stderr: string };
39
-
40
- let captureChain: Promise<unknown> = Promise.resolve();
41
-
42
- export async function captureCli(run: () => number | Promise<number>): Promise<CaptureResult> {
43
- const job = async (): Promise<CaptureResult> => {
44
- const out: string[] = [];
45
- const err: string[] = [];
46
- const joinArgs = (args: unknown[]) => args.map((a) => (typeof a === "string" ? a : String(a))).join(" ");
47
- const log = console.log;
48
- const error = console.error;
49
- console.log = (...args: unknown[]) => { out.push(joinArgs(args)); };
50
- console.error = (...args: unknown[]) => { err.push(joinArgs(args)); };
51
- try {
52
- const code = await run();
53
- return { code, stdout: out.join("\n"), stderr: err.join("\n") };
54
- } finally {
55
- console.log = log;
56
- console.error = error;
57
- }
58
- };
59
- const next = captureChain.then(job, job);
60
- captureChain = next.then(
61
- () => undefined,
62
- () => undefined,
63
- );
64
- return next;
65
- }
66
-
67
- function textResult(input: { text: string; isError?: boolean }) {
68
- return {
69
- content: [{ type: "text" as const, text: scrubSecrets(input.text) }],
70
- ...(input.isError ? { isError: true } : {}),
71
- };
72
- }
73
-
74
- function formatCapture(cap: CaptureResult): string {
75
- const parts: string[] = [];
76
- if (cap.stdout) parts.push(cap.stdout);
77
- if (cap.stderr) parts.push(cap.stderr);
78
- parts.push(`exit ${cap.code}`);
79
- return parts.join("\n").trimEnd();
80
- }
81
-
82
- export function mutationsEnabled(): boolean {
83
- return process.env[MUTATIONS_ENV] === "1";
84
- }
85
-
86
- function mutationDenied(confirm: boolean): string | null {
87
- if (!confirm) {
88
- return `Mutating tools require confirm=true. Also set ${MUTATIONS_ENV}=1 in the MCP server environment after the user approves.`;
89
- }
90
- if (!mutationsEnabled()) {
91
- return `Mutations are disabled. Set ${MUTATIONS_ENV}=1 in the MCP server environment only after the user explicitly approves a pool mutation.`;
92
- }
93
- return null;
94
- }
95
-
96
- const HELP_TEXT = `tokenmaxxing agent MCP
97
-
98
- Read tools (always available):
99
- pool_ls list pooled Claude and Codex accounts (labels/status only)
100
- pool_status sample usage bars (free /usage path; never --ping)
101
- doctor verify install health (labels/status only)
102
- config_get read effective config, or one key when provided
103
- help this catalog
104
-
105
- Mutating tools (confirm=true AND ${MUTATIONS_ENV}=1):
106
- pool_switch Claude greedy/forced switch, or Codex when codex=true
107
- pool_check one evaluate-and-maybe-swap pass
108
- config_set write a config.json override
109
- config_unset remove a config.json override
110
-
111
- Hard deny (no tools):
112
- status --ping / metered pings
113
- init / add / auth / rm / uninstall
114
- credential blobs or token values
115
- killing sessions or supervisors
116
-
117
- Prefer these tools over raw shell for pool ops. Honor TOKENMAXXING_HOME for hermetic use.
118
- `;
119
-
120
- export function createTokenmaxxingMcpServer(): McpServer {
121
- const server = new McpServer({
122
- name: "tokenmaxxing",
123
- version: packageVersion(),
124
- });
125
-
126
- server.registerTool(
127
- "help",
128
- {
129
- description: "Catalog of tokenmaxxing MCP tools and hard safety rules. Use when deciding which pool tool to call.",
130
- inputSchema: {},
131
- },
132
- async () => textResult({ text: HELP_TEXT }),
133
- );
134
-
135
- server.registerTool(
136
- "pool_ls",
137
- {
138
- description: "List pooled Claude and Codex accounts with active and needs-reauth flags. Labels and status only; never credential material.",
139
- inputSchema: {},
140
- },
141
- async () => {
142
- const cap = await captureCli(() => cmdLs());
143
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
144
- },
145
- );
146
-
147
- server.registerTool(
148
- "pool_status",
149
- {
150
- description: "Show pool usage bars via the free /usage path. Never opens 5h windows. Do not request --ping; that tool does not exist.",
151
- inputSchema: {},
152
- },
153
- async () => {
154
- const cap = await captureCli(() => cmdStatus());
155
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
156
- },
157
- );
158
-
159
- server.registerTool(
160
- "doctor",
161
- {
162
- description: "Verify supervisor, hooks, timer, and credential identity health. Reports labels and pass/fail only; never returns credential blobs.",
163
- inputSchema: {},
164
- },
165
- async () => {
166
- const cap = await captureCli(() => cmdDoctor());
167
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
168
- },
169
- );
170
-
171
- server.registerTool(
172
- "config_get",
173
- {
174
- description: "Read effective config with sources, or one dotted key when key is set (e.g. thresholds.session).",
175
- inputSchema: {
176
- key: z.string().optional().describe("Optional dotted config key; omit for the full effective table"),
177
- },
178
- },
179
- async ({ key }) => {
180
- const args = key ? ["get", key] : [];
181
- const cap = await captureCli(() => cmdConfig(args));
182
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
183
- },
184
- );
185
-
186
- server.registerTool(
187
- "pool_switch",
188
- {
189
- description: "Switch the Claude pool (or Codex when codex=true). Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1. Hot-swaps live Claude; Codex takes effect on next start.",
190
- inputSchema: {
191
- confirm: z.boolean().describe("Must be true after the user approves the mutation"),
192
- selector: z.string().optional().describe("Optional account selector; omit for greedy best"),
193
- codex: z.boolean().optional().describe("When true, run the Codex pool switch instead"),
194
- },
195
- },
196
- async ({ confirm, selector, codex }) => {
197
- const denied = mutationDenied(confirm);
198
- if (denied) return textResult({ text: denied, isError: true });
199
- const cap = await captureCli(() => (codex ? cmdCodexSwitch(selector) : cmdSwitch(selector)));
200
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
201
- },
202
- );
203
-
204
- server.registerTool(
205
- "pool_check",
206
- {
207
- description: "Run one evaluate-and-maybe-swap pass (the periodic timer path). Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
208
- inputSchema: {
209
- confirm: z.boolean().describe("Must be true after the user approves the mutation"),
210
- },
211
- },
212
- async ({ confirm }) => {
213
- const denied = mutationDenied(confirm);
214
- if (denied) return textResult({ text: denied, isError: true });
215
- const cap = await captureCli(() => cmdCheck());
216
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
217
- },
218
- );
219
-
220
- server.registerTool(
221
- "config_set",
222
- {
223
- description: "Write a config.json override. Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
224
- inputSchema: {
225
- confirm: z.boolean().describe("Must be true after the user approves the mutation"),
226
- key: z.string().describe("Dotted config key"),
227
- value: z.string().describe("JSON or literal string value"),
228
- },
229
- },
230
- async ({ confirm, key, value }) => {
231
- const denied = mutationDenied(confirm);
232
- if (denied) return textResult({ text: denied, isError: true });
233
- const cap = await captureCli(() => cmdConfig(["set", key, value]));
234
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
235
- },
236
- );
237
-
238
- server.registerTool(
239
- "config_unset",
240
- {
241
- description: "Remove a config.json override. Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
242
- inputSchema: {
243
- confirm: z.boolean().describe("Must be true after the user approves the mutation"),
244
- key: z.string().describe("Dotted config key"),
245
- },
246
- },
247
- async ({ confirm, key }) => {
248
- const denied = mutationDenied(confirm);
249
- if (denied) return textResult({ text: denied, isError: true });
250
- const cap = await captureCli(() => cmdConfig(["unset", key]));
251
- return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
252
- },
253
- );
254
-
255
- return server;
256
- }
257
-
258
- export async function main(): Promise<void> {
259
- const ambient = refuseAmbientStoreEnv();
260
- if (ambient != null) {
261
- console.error(
262
- `CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set (${ambient}): the pooled MCP surface requires the default Claude Code credential store. Unset it and retry.`,
263
- );
264
- process.exit(1);
265
- }
266
- const server = createTokenmaxxingMcpServer();
267
- const transport = new StdioServerTransport();
268
- await server.connect(transport);
269
- console.error("tokenmaxxing MCP server running on stdio");
270
- }
271
-
272
- if (import.meta.main) {
273
- main().catch((e) => {
274
- console.error(e instanceof Error ? e.message : String(e));
275
- process.exit(1);
276
- });
277
- }
@@ -1,234 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { z } from "zod";
4
- import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./claudebin.ts";
5
- import { withClaudeRefreshLock } from "./claudelock.ts";
6
- import { liveTarget, mergeIntoLive, readItem, writeItem } from "./credstore.ts";
7
- import { http, safeErrorDetail } from "./http.ts";
8
- import { log } from "./log.ts";
9
- import { InvalidGrantError, fetchTokenIdentity, isAccessTokenExpiring, isDeadCredential, refreshCredential } from "./oauth.ts";
10
- import { clearDepletedWait, clearNextCheck, loadAccounts, loadUsage, saveAccounts, saveLastResetAt, writeUsage } from "./state.ts";
11
- import { keepRotatedPair } from "./swap.ts";
12
- import { normalizeResetsAt } from "./usage.ts";
13
- import { CredentialBlobSchema, type Account, type BankedResetOutcome, type BankedResetRecord, type OAuthCreds } from "./types.ts";
14
-
15
- const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
16
- const API_BASE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_OAUTH_API_BASE_URL) ?? "https://api.anthropic.com";
17
- const OAUTH_BETA = "oauth-2025-04-20";
18
- const RESET_PROGRAM = "juniper_tide";
19
- const CLAIM_TIMEOUT_MS = 25_000;
20
- const VERSION_PROBE_TIMEOUT_MS = 5_000;
21
- const REFRESH_SKEW_MS = 300_000;
22
- const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
23
- const INELIGIBLE_TTL_MS = 24 * 60 * 60 * 1000;
24
-
25
- const ClaimResponseSchema = z.looseObject({
26
- result: z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable"]).catch("unavailable"),
27
- next_available_at: z.union([z.string(), z.number()]).nullable().optional().catch(null),
28
- });
29
-
30
- const ClaimSchema = z.object({ outcome: z.enum(["reset", "pass"]) });
31
- export type Claim = z.infer<typeof ClaimSchema>["outcome"];
32
-
33
- export function bankedResetBelievedAvailable(account: Account, now: number): boolean {
34
- const rec = account.bankedReset;
35
- if (!rec) return true;
36
- if (rec.nextAvailableAt != null) return rec.nextAvailableAt <= now;
37
- if (rec.outcome === "reset" || rec.outcome === "already_used") return now - rec.at >= WEEK_MS;
38
- if (rec.outcome === "ineligible") return now - rec.at >= INELIGIBLE_TTL_MS;
39
- return true;
40
- }
41
-
42
- const VersionSchema = z.string().regex(/^\d+\.\d+\.\d+\S*$/);
43
- const PackageJsonSchema = z.looseObject({ version: VersionSchema });
44
-
45
- function claudeVersion(): string | undefined {
46
- const bin = resolveRealClaude();
47
- const fromPath = VersionSchema.safeParse(bin.match(/\/versions\/(\d+\.\d+\.\d+[^/]*)$/)?.[1]);
48
- if (fromPath.success) return fromPath.data;
49
- try {
50
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync(join(dirname(bin), "package.json"), "utf8")));
51
- if (pkg.success) return pkg.data.version;
52
- } catch {
53
- }
54
- try {
55
- const r = Bun.spawnSync([bin, "--version"], {
56
- env: { ...process.env, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) },
57
- stdout: "pipe",
58
- stderr: "pipe",
59
- timeout: VERSION_PROBE_TIMEOUT_MS,
60
- });
61
- const spawned = VersionSchema.safeParse(r.stdout.toString("utf8").trim().match(/^(\d+\.\d+\.\d+\S*)/)?.[1]);
62
- if (spawned.success) return spawned.data;
63
- } catch {
64
- }
65
- return undefined;
66
- }
67
-
68
- function claudeUserAgent(): string {
69
- const version = claudeVersion();
70
- return version ? `claude-cli/${version} (external, cli)` : "claude-cli (external, cli)";
71
- }
72
-
73
- const LiveTokenSchema = z.discriminatedUnion("ok", [
74
- z.object({ ok: z.literal(true), accessToken: z.string(), organizationUuid: z.string() }),
75
- z.object({ ok: z.literal(false), outcome: z.enum(["auth_error", "error"]), detail: z.string() }),
76
- ]);
77
- type LiveToken = z.infer<typeof LiveTokenSchema>;
78
-
79
- function liveAccessToken(raw: string | null): string | null {
80
- if (raw == null) return null;
81
- const parsed = CredentialBlobSchema.safeParse((() => {
82
- try { return JSON.parse(raw); } catch { return null; }
83
- })());
84
- return parsed.success ? parsed.data.claudeAiOauth.accessToken : null;
85
- }
86
-
87
- async function liveTokenLocked(account: Account, lock: { compromised: () => boolean }): Promise<LiveToken> {
88
- const raw = await readItem(liveTarget());
89
- if (!raw) return { ok: false, outcome: "auth_error", detail: "no live credential" };
90
- let creds: OAuthCreds;
91
- try {
92
- creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
93
- } catch (e) {
94
- return { ok: false, outcome: "error", detail: `live credential blob unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)})` };
95
- }
96
- if (isDeadCredential(creds)) return { ok: false, outcome: "auth_error", detail: "live credential was cleared after a failed refresh" };
97
- if (isAccessTokenExpiring(creds, REFRESH_SKEW_MS)) {
98
- let next: OAuthCreds;
99
- try {
100
- next = await refreshCredential(creds);
101
- } catch (e) {
102
- const detail = e instanceof Error ? e.message : String(e);
103
- return { ok: false, outcome: e instanceof InvalidGrantError ? "auth_error" : "error", detail };
104
- }
105
- if (lock.compromised()) {
106
- let owner: Account | null = null;
107
- try {
108
- const id = await fetchTokenIdentity(next.accessToken);
109
- owner = loadAccounts().accounts.find((a) => a.accountUuid === id.accountUuid) ?? null;
110
- } catch {
111
- }
112
- const kept = await keepRotatedPair({ fresh: next, owner, fallback: account, liveOwnerUuid: account.accountUuid, expectedLiveToken: creds.accessToken, lock });
113
- return { ok: false, outcome: "error", detail: `refresh lock compromised mid-refresh - the rotated token ${kept}` };
114
- }
115
- await writeItem(liveTarget(), mergeIntoLive(raw, next));
116
- creds = next;
117
- }
118
- let identity;
119
- try {
120
- identity = await fetchTokenIdentity(creds.accessToken);
121
- } catch (e) {
122
- return { ok: false, outcome: "error", detail: e instanceof Error ? e.message : String(e) };
123
- }
124
- if (identity.accountUuid !== account.accountUuid) {
125
- return { ok: false, outcome: "error", detail: `live credential belongs to account ${identity.accountUuid.slice(0, 8)}, not the seat` };
126
- }
127
- return { ok: true, accessToken: creds.accessToken, organizationUuid: identity.organizationUuid };
128
- }
129
-
130
- const PostResultSchema = z.object({
131
- outcome: z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "rate_limited", "auth_error", "error"]),
132
- nextAvailableAt: z.number().nullable(),
133
- detail: z.string(),
134
- ambiguous: z.boolean().optional(),
135
- });
136
- type PostResult = z.infer<typeof PostResultSchema>;
137
-
138
- async function postClaim(input: { accessToken: string; organizationUuid: string; userAgent: string }): Promise<PostResult> {
139
- const url = `${API_BASE_URL}/api/organizations/${input.organizationUuid}/reset_rate_limits`;
140
- let res: Response;
141
- try {
142
- res = await http.post(url, {
143
- headers: {
144
- Authorization: `Bearer ${input.accessToken}`,
145
- "anthropic-beta": OAUTH_BETA,
146
- "Content-Type": "application/json",
147
- "User-Agent": input.userAgent,
148
- },
149
- body: JSON.stringify({ program: RESET_PROGRAM }),
150
- timeout: CLAIM_TIMEOUT_MS,
151
- });
152
- } catch (e) {
153
- return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint unreachable: ${e instanceof Error ? e.message : String(e)}` };
154
- }
155
- let text: string;
156
- try {
157
- text = await res.text();
158
- } catch (e) {
159
- return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint answered HTTP ${res.status} but the body could not be read: ${e instanceof Error ? e.message : String(e)}`, ambiguous: true };
160
- }
161
- if (!res.ok) {
162
- const detail = `HTTP ${res.status}: ${safeErrorDetail({ text })}`;
163
- if (res.status === 429) return { outcome: "rate_limited", nextAvailableAt: null, detail };
164
- if (res.status === 401 || res.status === 403) return { outcome: "auth_error", nextAvailableAt: null, detail };
165
- return { outcome: "error", nextAvailableAt: null, detail };
166
- }
167
- const parsed = ClaimResponseSchema.safeParse((() => {
168
- try { return JSON.parse(text); } catch { return null; }
169
- })());
170
- if (!parsed.success) {
171
- return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint answered HTTP ${res.status} with an unrecognized body (${text.length} bytes, withheld)`, ambiguous: true };
172
- }
173
- return { outcome: parsed.data.result, nextAvailableAt: normalizeResetsAt(parsed.data.next_available_at), detail: parsed.data.result };
174
- }
175
-
176
- function record(accountUuid: string, rec: BankedResetRecord, reset: boolean, now: number): void {
177
- const idx = loadAccounts();
178
- const account = idx.accounts.find((a) => a.accountUuid === accountUuid);
179
- if (!account) return;
180
- account.bankedReset = rec;
181
- if (reset) {
182
- delete account.enforcedUntil;
183
- if (account.lastUsage) {
184
- account.lastUsage = { ...account.lastUsage, fiveHour: { usedPercentage: 0, resetsAt: null } };
185
- }
186
- }
187
- saveAccounts(idx);
188
- if (!reset) return;
189
- const u = loadUsage();
190
- if (u && u.account === accountUuid) {
191
- writeUsage({ ...u, fiveHour: { usedPercentage: 0, resetsAt: null }, ts: now }, { stamp: true });
192
- }
193
- saveLastResetAt(now);
194
- clearDepletedWait();
195
- clearNextCheck();
196
- }
197
-
198
- export async function claimBankedReset(input: { account: Account }): Promise<Claim> {
199
- const { account } = input;
200
- const short = account.accountUuid.slice(0, 8);
201
- let result: PostResult;
202
- let moved = false;
203
- try {
204
- const userAgent = claudeUserAgent();
205
- ({ result, moved } = await withClaudeRefreshLock(async (lock) => {
206
- const token = await liveTokenLocked(account, lock);
207
- if (!token.ok) return { result: { outcome: token.outcome, nextAvailableAt: null, detail: token.detail }, moved: false };
208
- let posted = await postClaim({ accessToken: token.accessToken, organizationUuid: token.organizationUuid, userAgent });
209
- if (posted.ambiguous) {
210
- const again = await postClaim({ accessToken: token.accessToken, organizationUuid: token.organizationUuid, userAgent });
211
- posted =
212
- again.outcome === "already_used"
213
- ? { outcome: "reset", nextAvailableAt: again.nextAvailableAt, detail: "already_used on the confirming claim, so the ambiguous claim was the reset", ambiguous: true }
214
- : again;
215
- }
216
- return { result: posted, moved: liveAccessToken(await readItem(liveTarget())) !== token.accessToken };
217
- }));
218
- } catch (e) {
219
- result = { outcome: "error", nextAvailableAt: null, detail: `claim aborted: ${e instanceof Error ? e.message : String(e)}` };
220
- }
221
- const outcome: BankedResetOutcome = result.outcome;
222
- const reset = outcome === "reset";
223
- const completedAt = Date.now();
224
- record(account.accountUuid, { outcome, at: completedAt, nextAvailableAt: result.nextAvailableAt }, reset, completedAt);
225
- log(reset ? "bankedreset.claimed" : "bankedreset.refused", {
226
- account: short,
227
- outcome,
228
- nextAvailableAt: result.nextAvailableAt,
229
- moved,
230
- ambiguous: result.ambiguous === true ? true : undefined,
231
- detail: reset && result.ambiguous !== true ? undefined : result.detail.slice(0, 200),
232
- });
233
- return reset && !moved ? "reset" : "pass";
234
- }
@@ -1,122 +0,0 @@
1
- import { z } from "zod";
2
- import { codexIdentityOf, readLiveCodexAuth } from "./codexauth.ts";
3
- import { allWindows, barFor, liveUsed } from "./codexpick.ts";
4
- import { loadCodexAccounts, saveCodexAccounts } from "./codexstate.ts";
5
- import { http, safeErrorDetail } from "./http.ts";
6
- import { log } from "./log.ts";
7
- import type { CodexAccount, CodexAuthJson, CodexBankedResetOutcome, Thresholds } from "./types.ts";
8
-
9
- const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
10
- const CONSUME_URL =
11
- EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_RESET_URL) ?? "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
12
- const CONSUME_TIMEOUT_MS = 30_000;
13
- const RETRY_KEY_TTL_MS = 10 * 60_000;
14
- const WALL_PERCENT = 100;
15
- const LIMIT_REACHED = "rate_limit_reached";
16
-
17
- const VerdictSchema = z.enum(["hold", "claim", "pass"]);
18
- export type CodexBankedVerdict = z.infer<typeof VerdictSchema>;
19
- const ClaimSchema = z.enum(["reset", "pass"]);
20
- export type CodexBankedClaim = z.infer<typeof ClaimSchema>;
21
-
22
- export function codexBankedResetVerdict(input: { account: CodexAccount; thresholds: Thresholds; now: number; pollTtlMs: number }): CodexBankedVerdict {
23
- const { account, thresholds, now, pollTtlMs } = input;
24
- const usage = account.lastUsage;
25
- if (!usage) return "pass";
26
- const credits = usage.resetCredits ?? null;
27
- if (credits == null || credits <= 0) return "pass";
28
- const rec = account.bankedReset;
29
- if (rec != null && rec.outcome !== "reset" && now - rec.at < pollTtlMs) return "pass";
30
- const sampledAt = account.lastUsageAt ?? null;
31
- const windows = allWindows(account);
32
- const over = windows.some((window) => liveUsed({ window, now, sampledAt }) >= barFor({ window, thresholds }));
33
- if (!over) return "pass";
34
- const atWall = usage.reachedType === LIMIT_REACHED || windows.some((window) => liveUsed({ window, now, sampledAt }) >= WALL_PERCENT);
35
- return atWall ? "claim" : "hold";
36
- }
37
-
38
- const ConsumeResponseSchema = z.looseObject({
39
- code: z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed"]),
40
- windows_reset: z.number().int().nullish(),
41
- });
42
-
43
- const ConsumeResultSchema = z.object({ outcome: z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed", "auth_error", "error"]), detail: z.string() });
44
- type ConsumeResult = z.infer<typeof ConsumeResultSchema>;
45
-
46
- export async function consumeCodexBankedReset(input: { auth: CodexAuthJson; idempotencyKey: string }): Promise<ConsumeResult> {
47
- const { auth, idempotencyKey } = input;
48
- const identity = codexIdentityOf({ auth });
49
- let res: Response;
50
- let text: string;
51
- try {
52
- res = await http.post(CONSUME_URL, {
53
- headers: {
54
- Authorization: `Bearer ${auth.tokens.access_token}`,
55
- "ChatGPT-Account-Id": identity.accountId,
56
- "User-Agent": "codex-cli",
57
- "Content-Type": "application/json",
58
- },
59
- body: JSON.stringify({ redeem_request_id: idempotencyKey }),
60
- timeout: CONSUME_TIMEOUT_MS,
61
- });
62
- text = await res.text();
63
- } catch (e) {
64
- return { outcome: "error", detail: `reset endpoint unreachable: ${e instanceof Error ? e.message : String(e)}` };
65
- }
66
- if (!res.ok) {
67
- const detail = `HTTP ${res.status}: ${safeErrorDetail({ text })}`;
68
- if (res.status === 401 || res.status === 403) return { outcome: "auth_error", detail };
69
- return { outcome: "error", detail };
70
- }
71
- const parsed = ConsumeResponseSchema.safeParse((() => {
72
- try {
73
- return JSON.parse(text);
74
- } catch {
75
- return null;
76
- }
77
- })());
78
- if (!parsed.success) return { outcome: "error", detail: `reset endpoint returned an unrecognized body (${text.length} bytes, withheld)` };
79
- return { outcome: parsed.data.code, detail: `${parsed.data.code} (windows reset: ${parsed.data.windows_reset ?? "unknown"})` };
80
- }
81
-
82
- function rememberPendingKey(accountId: string, key: string, now: number): void {
83
- const index = loadCodexAccounts();
84
- const entry = index.accounts.find((a) => a.accountId === accountId);
85
- if (!entry) return;
86
- entry.bankedReset = { outcome: "error", at: now, key };
87
- saveCodexAccounts({ index });
88
- }
89
-
90
- export async function consumeLiveCodexBankedReset(input: { account: CodexAccount; now: number }): Promise<CodexBankedClaim> {
91
- const { account, now } = input;
92
- const short = account.accountId.slice(0, 8);
93
- const live = readLiveCodexAuth();
94
- let result: ConsumeResult;
95
- let idempotencyKey: string | null = null;
96
- if (!live) {
97
- result = { outcome: "auth_error", detail: "live auth.json vanished" };
98
- } else if (codexIdentityOf({ auth: live }).accountId !== account.accountId) {
99
- result = { outcome: "error", detail: "live codex credential belongs to another account" };
100
- } else {
101
- const prior = account.bankedReset;
102
- const retrying = prior != null && prior.outcome === "error" && prior.key != null && now - prior.at < RETRY_KEY_TTL_MS;
103
- idempotencyKey = retrying ? prior.key! : crypto.randomUUID();
104
- if (!retrying) rememberPendingKey(account.accountId, idempotencyKey, now);
105
- result = await consumeCodexBankedReset({ auth: live, idempotencyKey });
106
- if (result.outcome === "already_redeemed" && retrying) result = { outcome: "reset", detail: `${result.detail}; the retried key had already completed` };
107
- }
108
- const outcome: CodexBankedResetOutcome = result.outcome;
109
- const reset = outcome === "reset";
110
- const index = loadCodexAccounts();
111
- const entry = index.accounts.find((a) => a.accountId === account.accountId);
112
- if (entry) {
113
- entry.bankedReset = { outcome, at: now, ...(outcome === "error" && idempotencyKey != null ? { key: idempotencyKey } : {}) };
114
- if (reset) {
115
- delete entry.lastUsage;
116
- delete entry.lastUsageAt;
117
- }
118
- saveCodexAccounts({ index });
119
- }
120
- log(reset ? "codexreset.consumed" : "codexreset.refused", { account: short, outcome, detail: result.detail.slice(0, 200) });
121
- return reset ? "reset" : "pass";
122
- }
package/src/sdk.ts DELETED
@@ -1,55 +0,0 @@
1
- import { z } from "zod";
2
- import { UNMANAGED_ENV, resolveRealClaude } from "./lib/claudebin.ts";
3
- import { evaluateAndMaybeSwap, type SwapDecision } from "./lib/decide.ts";
4
- import { CRED_ENV_OVERRIDES } from "./lib/usage.ts";
5
- import { log } from "./lib/log.ts";
6
-
7
- export { evaluateAndMaybeSwap };
8
- export type { SwapDecision };
9
-
10
- export async function ensureBestAccount(now = Date.now()): Promise<SwapDecision> {
11
- return evaluateAndMaybeSwap(now, false);
12
- }
13
-
14
- export function claudeExecutablePath(): string {
15
- return resolveRealClaude();
16
- }
17
-
18
- const AMBIENT_STORE_VARS = ["CLAUDE_SECURESTORAGE_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
19
-
20
- export function pooledSpawnEnv(): Record<string, string> {
21
- for (const k of AMBIENT_STORE_VARS) {
22
- if (process.env[k]) {
23
- throw new Error(
24
- `${k} is set: the pooled SDK surface requires the default Claude Code credential store (a swap writes the live credential where ${k} points, while the spawned subprocess reads the default store). Unset it in the process running tokenmaxxing.`,
25
- );
26
- }
27
- }
28
- const env: Record<string, string> = { ...process.env, [UNMANAGED_ENV]: "1" };
29
- for (const k of CRED_ENV_OVERRIDES) delete env[k];
30
- return env;
31
- }
32
-
33
- const PooledOptionsSchema = z.object({
34
- pathToClaudeCodeExecutable: z.string(),
35
- env: z.record(z.string(), z.string()),
36
- });
37
- export type PooledOptions = z.infer<typeof PooledOptionsSchema>;
38
-
39
- export function pooledOptions(): PooledOptions {
40
- return PooledOptionsSchema.parse({
41
- pathToClaudeCodeExecutable: claudeExecutablePath(),
42
- env: pooledSpawnEnv(),
43
- });
44
- }
45
-
46
- export async function stopHookCheck(): Promise<Record<string, never>> {
47
- try {
48
- await evaluateAndMaybeSwap(Date.now(), false);
49
- } catch (e) {
50
- const err = e instanceof Error ? e.message : String(e);
51
- console.error(`tokenmaxxing: switch check failed at turn boundary: ${err}`);
52
- log("sdk.stop_error", { err });
53
- }
54
- return {};
55
- }