tokenmaxxing 1.35.0 → 1.39.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/package.json +1 -1
- package/src/cli/check.ts +2 -0
- package/src/entries/codexstophook.ts +2 -8
- package/src/entries/sessionstart.ts +2 -1
- package/src/entries/statusline.ts +2 -6
- package/src/entries/stopfailurehook.ts +2 -2
- package/src/entries/stophook.ts +2 -2
- package/src/entries/subagentstatusline.ts +2 -7
- package/src/lib/atomic.ts +2 -8
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +2 -10
- package/src/lib/codexauth.ts +3 -7
- package/src/lib/codexoauth.ts +5 -11
- package/src/lib/codexpresence.ts +3 -9
- package/src/lib/codexusage.ts +4 -10
- package/src/lib/credstore.ts +3 -7
- package/src/lib/http.ts +3 -6
- package/src/lib/install.ts +4 -7
- package/src/lib/oauth.ts +7 -12
- package/src/lib/paths.ts +1 -1
- package/src/lib/proc.ts +4 -4
- package/src/lib/state.ts +2 -2
- package/src/lib/types.ts +14 -0
- package/src/lib/update.ts +133 -0
- package/src/lib/usage.ts +12 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/check.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { claude } from "../lib/claude.ts";
|
|
|
2
2
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
3
|
import { sampleOldestParked } from "../lib/sample.ts";
|
|
4
4
|
import { loadConfig } from "../lib/state.ts";
|
|
5
|
+
import { maybeAutoUpdate } from "../lib/update.ts";
|
|
5
6
|
import { log } from "../lib/log.ts";
|
|
6
7
|
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ export async function cmdCheck(json = false): Promise<number> {
|
|
|
11
12
|
try {
|
|
12
13
|
d = await evaluateAndMaybeSwap(claude, now);
|
|
13
14
|
await sampleOldestParked(loadConfig());
|
|
15
|
+
await maybeAutoUpdate();
|
|
14
16
|
} catch (e) {
|
|
15
17
|
const detail = e instanceof Error ? e.message : String(e);
|
|
16
18
|
log("check.error", { err: detail });
|
|
@@ -11,7 +11,7 @@ import { isExhausted } from "../lib/picker.ts";
|
|
|
11
11
|
import { livingCodexPresences } from "../lib/codexpresence.ts";
|
|
12
12
|
import { loadAccounts } from "../lib/state.ts";
|
|
13
13
|
import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
|
|
14
|
-
import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema } from "../lib/types.ts";
|
|
14
|
+
import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, JsonTextSchema } from "../lib/types.ts";
|
|
15
15
|
import { log } from "../lib/log.ts";
|
|
16
16
|
|
|
17
17
|
const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
|
|
@@ -85,13 +85,7 @@ async function readStdin(): Promise<string> {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export async function handleCodexStop(input: { rawStdin: string }): Promise<void> {
|
|
88
|
-
const parsed = CodexStopStdinSchema.safeParse((
|
|
89
|
-
try {
|
|
90
|
-
return JSON.parse(input.rawStdin);
|
|
91
|
-
} catch {
|
|
92
|
-
return {};
|
|
93
|
-
}
|
|
94
|
-
})());
|
|
88
|
+
const parsed = CodexStopStdinSchema.safeParse(JsonTextSchema.safeParse(input.rawStdin).data);
|
|
95
89
|
const sessionId = parsed.success ? (parsed.data.session_id ?? null) : null;
|
|
96
90
|
|
|
97
91
|
try {
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
4
|
import { log } from "../lib/log.ts";
|
|
5
|
+
import { JsonTextSchema } from "../lib/types.ts";
|
|
5
6
|
|
|
6
7
|
const SessionStartStdin = z.looseObject({
|
|
7
8
|
source: z.string().optional(),
|
|
@@ -18,7 +19,7 @@ export async function runSessionStart(): Promise<number> {
|
|
|
18
19
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
19
20
|
|
|
20
21
|
const raw = await readStdin();
|
|
21
|
-
const parsed = SessionStartStdin.safeParse(
|
|
22
|
+
const parsed = SessionStartStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
22
23
|
const source = parsed.success ? parsed.data.source : undefined;
|
|
23
24
|
|
|
24
25
|
try {
|
|
@@ -10,6 +10,7 @@ import { makeColors, makeUsagePaint } from "../cli/render.ts";
|
|
|
10
10
|
import { fmtResetShort } from "../lib/usage.ts";
|
|
11
11
|
import {
|
|
12
12
|
AccountsIndexSchema,
|
|
13
|
+
JsonTextSchema,
|
|
13
14
|
StatusLineStdinSchema,
|
|
14
15
|
WindowSchema,
|
|
15
16
|
type Account,
|
|
@@ -118,12 +119,7 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
export async function runStatusline(): Promise<number> {
|
|
121
|
-
const
|
|
122
|
-
let obj: unknown = null;
|
|
123
|
-
try {
|
|
124
|
-
obj = JSON.parse(raw);
|
|
125
|
-
} catch {
|
|
126
|
-
}
|
|
122
|
+
const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
|
|
127
123
|
const now = Date.now();
|
|
128
124
|
|
|
129
125
|
let account: string | null = null;
|
|
@@ -9,7 +9,7 @@ import { withLock } from "../lib/lock.ts";
|
|
|
9
9
|
import { claudePool } from "../lib/paths.ts";
|
|
10
10
|
import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
|
|
11
11
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
12
|
-
import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
12
|
+
import { JsonTextSchema, RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
13
13
|
import { log } from "../lib/log.ts";
|
|
14
14
|
|
|
15
15
|
const StopFailureStdin = z.looseObject({
|
|
@@ -35,7 +35,7 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
35
35
|
const account = entry.account;
|
|
36
36
|
const now = Date.now();
|
|
37
37
|
const raw = await readStdin();
|
|
38
|
-
const parsed = StopFailureStdin.safeParse(
|
|
38
|
+
const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
39
39
|
const stdin = parsed.success ? parsed.data : {};
|
|
40
40
|
if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
|
|
41
41
|
|
package/src/entries/stophook.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { paths } from "../lib/paths.ts";
|
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
5
|
import { claude } from "../lib/claude.ts";
|
|
6
6
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
7
|
-
import { RespawnMarkerSchema } from "../lib/types.ts";
|
|
7
|
+
import { JsonTextSchema, RespawnMarkerSchema } from "../lib/types.ts";
|
|
8
8
|
import { log } from "../lib/log.ts";
|
|
9
9
|
|
|
10
10
|
const StopStdin = z.looseObject({ session_id: z.uuid().optional().catch(undefined) });
|
|
@@ -19,7 +19,7 @@ export async function runStopHook(): Promise<number> {
|
|
|
19
19
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
20
20
|
|
|
21
21
|
const raw = await readStdin();
|
|
22
|
-
const parsed = StopStdin.safeParse(
|
|
22
|
+
const parsed = StopStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
23
23
|
const stdinSid = parsed.success ? parsed.data.session_id : undefined;
|
|
24
24
|
const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
|
|
25
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { makeColors, makeUsagePaint } from "../cli/render.ts";
|
|
3
3
|
import { readStdin } from "./statusline.ts";
|
|
4
|
-
import { SubagentStatusLineStdinSchema } from "../lib/types.ts";
|
|
4
|
+
import { JsonTextSchema, SubagentStatusLineStdinSchema } from "../lib/types.ts";
|
|
5
5
|
|
|
6
6
|
const RowCtxSchema = z.object({ color: z.boolean(), truecolor: z.boolean() });
|
|
7
7
|
export type RowCtx = z.infer<typeof RowCtxSchema>;
|
|
@@ -43,12 +43,7 @@ export function renderSubagentRows(stdinObj: unknown, ctx: RowCtx): string[] {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function runSubagentStatusline(): Promise<number> {
|
|
46
|
-
const
|
|
47
|
-
let obj: unknown = null;
|
|
48
|
-
try {
|
|
49
|
-
obj = JSON.parse(raw);
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
46
|
+
const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
|
|
52
47
|
const colorterm = z.string().optional().parse(process.env.COLORTERM);
|
|
53
48
|
const rows = renderSubagentRows(obj, {
|
|
54
49
|
color: !process.env.NO_COLOR,
|
package/src/lib/atomic.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import { closeSync, mkdirSync, openSync, renameSync, rmSync,
|
|
1
|
+
import { closeSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync, fsyncSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
export function writeFileAtomic(file: string, data: string | Uint8Array, mode = 0o600): void {
|
|
5
5
|
const dir = dirname(file);
|
|
6
6
|
mkdirSync(dir, { recursive: true });
|
|
7
7
|
const tmp = `${file}.tmp.${process.pid}.${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
8
|
-
const bytes = data instanceof Uint8Array ? data : new TextEncoder().encode(data);
|
|
9
8
|
const fd = openSync(tmp, "wx", mode);
|
|
10
9
|
try {
|
|
11
10
|
try {
|
|
12
|
-
|
|
13
|
-
while (offset < bytes.length) {
|
|
14
|
-
const written = writeSync(fd, bytes, offset);
|
|
15
|
-
if (written <= 0) throw new Error(`short write on ${tmp}: ${offset}/${bytes.length} bytes (disk full?)`);
|
|
16
|
-
offset += written;
|
|
17
|
-
}
|
|
11
|
+
writeFileSync(fd, data);
|
|
18
12
|
fsyncSync(fd);
|
|
19
13
|
} finally {
|
|
20
14
|
closeSync(fd);
|
package/src/lib/claudelock.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mkdirSync, realpathSync, rmdirSync, statSync, utimesSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { delay } from "es-toolkit";
|
|
4
|
-
import { z } from "zod";
|
|
5
4
|
import { credDir } from "./paths.ts";
|
|
6
5
|
import { log } from "./log.ts";
|
|
6
|
+
import { ErrnoSchema } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
const STALE_MS = 60_000;
|
|
9
9
|
const HEARTBEAT_MS = 5_000;
|
|
@@ -15,8 +15,7 @@ function tryAcquire(lockDir: string): boolean {
|
|
|
15
15
|
mkdirSync(lockDir);
|
|
16
16
|
return true;
|
|
17
17
|
} catch (e) {
|
|
18
|
-
|
|
19
|
-
if (!errno.success || errno.data.code !== "EEXIST") throw e;
|
|
18
|
+
if (ErrnoSchema.safeParse(e).data?.code !== "EEXIST") throw e;
|
|
20
19
|
}
|
|
21
20
|
try {
|
|
22
21
|
if (Date.now() - statSync(lockDir).mtimeMs > STALE_MS) {
|
package/src/lib/codex.ts
CHANGED
|
@@ -259,16 +259,8 @@ async function importLive(): Promise<Harvest | null> {
|
|
|
259
259
|
function storePinnedAwayFromFile(): boolean {
|
|
260
260
|
const configToml = `${codexPaths.home}/config.toml`;
|
|
261
261
|
if (!existsSync(configToml)) return false;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!line.startsWith("cli_auth_credentials_store")) continue;
|
|
265
|
-
const rest = line.slice("cli_auth_credentials_store".length).trimStart();
|
|
266
|
-
if (!rest.startsWith("=")) continue;
|
|
267
|
-
const beforeComment = rest.slice(1).split("#", 1)[0]!;
|
|
268
|
-
const value = beforeComment.replaceAll('"', "").replaceAll("'", "").trim();
|
|
269
|
-
return value !== "file";
|
|
270
|
-
}
|
|
271
|
-
return false;
|
|
262
|
+
const config = Bun.TOML.parse(readFileSync(configToml, "utf8"));
|
|
263
|
+
return "cli_auth_credentials_store" in config && config.cli_auth_credentials_store !== "file";
|
|
272
264
|
}
|
|
273
265
|
|
|
274
266
|
function preflight(): void {
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -3,18 +3,14 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import { codexPaths } from "./paths.ts";
|
|
6
|
-
import { CodexAuthJsonSchema, type CodexAuthJson } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
function isEnoent(e: unknown): boolean {
|
|
9
|
-
return e instanceof Error && "code" in e && e.code === "ENOENT";
|
|
10
|
-
}
|
|
6
|
+
import { CodexAuthJsonSchema, ErrnoSchema, type CodexAuthJson } from "./types.ts";
|
|
11
7
|
|
|
12
8
|
export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
|
|
13
9
|
let raw: string;
|
|
14
10
|
try {
|
|
15
11
|
raw = readFileSync(input.path, "utf8");
|
|
16
12
|
} catch (e) {
|
|
17
|
-
if (
|
|
13
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
18
14
|
throw e;
|
|
19
15
|
}
|
|
20
16
|
const parsed = JSON.parse(raw);
|
|
@@ -46,7 +42,7 @@ export function readParkedCodexAuth(input: { credFile: string }): CodexAuthJson
|
|
|
46
42
|
try {
|
|
47
43
|
raw = readFileSync(parkedPath(input), "utf8");
|
|
48
44
|
} catch (e) {
|
|
49
|
-
if (
|
|
45
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
50
46
|
throw e;
|
|
51
47
|
}
|
|
52
48
|
return CodexAuthJsonSchema.parse(JSON.parse(raw));
|
package/src/lib/codexoauth.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { env } from "./paths.ts";
|
|
4
|
+
import { CodexAuthJsonSchema, JsonTextSchema, type CodexAuthJson } from "./types.ts";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const CLIENT_ID = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_CLIENT_ID) ?? "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
|
+
const TOKEN_URL = env("TOKENMAXXING_CODEX_TOKEN_URL", "https://auth.openai.com/oauth/token");
|
|
7
|
+
const CLIENT_ID = env("TOKENMAXXING_CODEX_CLIENT_ID", "app_EMoamEEZ73f0CkXaXp7hrann");
|
|
8
8
|
|
|
9
9
|
export class CodexInvalidGrantError extends Error {
|
|
10
10
|
constructor(detail: string) {
|
|
@@ -60,13 +60,7 @@ export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: numbe
|
|
|
60
60
|
throw new CodexRefreshFailedError(`HTTP ${res.status}: ${detail}`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const parsed = CodexRefreshResponseSchema.safeParse((
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(text);
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
})());
|
|
63
|
+
const parsed = CodexRefreshResponseSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
70
64
|
if (!parsed.success) {
|
|
71
65
|
throw new CodexRefreshFailedError("endpoint returned an unexpected body shape (withheld: may carry tokens)");
|
|
72
66
|
}
|
package/src/lib/codexpresence.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { codexPaths } from "./paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
6
|
import { pidExists, pidStartTime } from "./proc.ts";
|
|
7
|
+
import { ErrnoSchema, JsonTextSchema } from "./types.ts";
|
|
7
8
|
|
|
8
9
|
const PresenceSchema = z.object({
|
|
9
10
|
accountId: z.string(),
|
|
@@ -38,17 +39,10 @@ export function livingCodexPresences(): LivingPresence[] {
|
|
|
38
39
|
try {
|
|
39
40
|
raw = readFileSync(file, "utf8");
|
|
40
41
|
} catch (e) {
|
|
41
|
-
|
|
42
|
-
if (errno.success && errno.data.code === "ENOENT") continue;
|
|
42
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
|
|
43
43
|
throw e;
|
|
44
44
|
}
|
|
45
|
-
const parsed = PresenceSchema.safeParse((
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(raw);
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
})());
|
|
45
|
+
const parsed = PresenceSchema.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
52
46
|
if (!parsed.success) {
|
|
53
47
|
throw new Error(`${file} is not a readable presence record - it may belong to a RUNNING codex session, refusing to treat it as absent; remove the file (or respawn that session) to proceed`);
|
|
54
48
|
}
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, safeErrorDetail } from "./http.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { env } from "./paths.ts";
|
|
4
|
+
import { CodexUsageSchema, JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
|
|
4
5
|
import { codexIdentityOf } from "./codexauth.ts";
|
|
5
6
|
import { familyTokens } from "./usage.ts";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const USAGE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_USAGE_URL) ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
8
|
+
const USAGE_URL = env("TOKENMAXXING_CODEX_USAGE_URL", "https://chatgpt.com/backend-api/wham/usage");
|
|
9
9
|
|
|
10
10
|
export class CodexUsageReadError extends Error {
|
|
11
11
|
constructor(detail: string) {
|
|
@@ -70,13 +70,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
70
70
|
if (!res.ok) {
|
|
71
71
|
throw new CodexUsageReadError(`HTTP ${res.status}: ${safeErrorDetail({ text })}`);
|
|
72
72
|
}
|
|
73
|
-
const parsed = WireUsageSchema.safeParse((
|
|
74
|
-
try {
|
|
75
|
-
return JSON.parse(text);
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
})());
|
|
73
|
+
const parsed = WireUsageSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
80
74
|
if (!parsed.success) {
|
|
81
75
|
throw new CodexUsageReadError("endpoint returned an unexpected body shape (withheld)");
|
|
82
76
|
}
|
package/src/lib/credstore.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import * as kc from "./keychain.ts";
|
|
6
6
|
import { credDir, keychain as kcNames, namespacedCredService, paths } from "./paths.ts";
|
|
7
|
-
import { CredentialBlobSchema } from "./types.ts";
|
|
7
|
+
import { CredentialBlobSchema, ErrnoSchema } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
const CredTargetSchema = z.discriminatedUnion("kind", [
|
|
10
10
|
z.object({ kind: z.literal("keychain"), service: z.string(), account: z.string() }),
|
|
@@ -16,16 +16,12 @@ const darwin = process.platform === "darwin";
|
|
|
16
16
|
|
|
17
17
|
const BlobRecordSchema = z.record(z.string(), z.unknown());
|
|
18
18
|
|
|
19
|
-
function isEnoent(e: unknown): boolean {
|
|
20
|
-
return e instanceof Error && "code" in e && e.code === "ENOENT";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
export async function readItem(t: CredTarget): Promise<string | null> {
|
|
24
20
|
if (t.kind === "keychain") return kc.readItem(t);
|
|
25
21
|
try {
|
|
26
22
|
return readFileSync(t.path, "utf8");
|
|
27
23
|
} catch (e) {
|
|
28
|
-
if (
|
|
24
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
29
25
|
throw e;
|
|
30
26
|
}
|
|
31
27
|
}
|
|
@@ -42,7 +38,7 @@ export async function deleteItem(t: CredTarget): Promise<boolean> {
|
|
|
42
38
|
unlinkSync(t.path);
|
|
43
39
|
return true;
|
|
44
40
|
} catch (e) {
|
|
45
|
-
if (
|
|
41
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return false;
|
|
46
42
|
throw e;
|
|
47
43
|
}
|
|
48
44
|
}
|
package/src/lib/http.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ky from "ky";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { JsonTextSchema } from "./types.ts";
|
|
3
4
|
|
|
4
5
|
const OAuthErrorBodySchema = z.looseObject({
|
|
5
6
|
error: z.string(),
|
|
@@ -20,12 +21,8 @@ const ErrorDetailSchema = z.object({ code: z.string().nullable(), fields: z.arra
|
|
|
20
21
|
type ErrorDetail = z.infer<typeof ErrorDetailSchema>;
|
|
21
22
|
|
|
22
23
|
function parseErrorBody(text: string): ErrorDetail | null {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
json = JSON.parse(text);
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
24
|
+
const json = JsonTextSchema.safeParse(text).data;
|
|
25
|
+
if (json === undefined) return null;
|
|
29
26
|
const oauth = OAuthErrorBodySchema.safeParse(json);
|
|
30
27
|
if (oauth.success) return { code: oauth.data.error, fields: [oauth.data.error, oauth.data.error_description ?? ""] };
|
|
31
28
|
const nested = NestedErrorBodySchema.safeParse(json);
|
package/src/lib/install.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { writeFileAtomic } from "./atomic.ts";
|
|
|
7
7
|
import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
|
|
8
8
|
import { resolveRealClaude } from "./claudebin.ts";
|
|
9
9
|
import { loadConfig } from "./state.ts";
|
|
10
|
+
import { ErrnoSchema } from "./types.ts";
|
|
10
11
|
|
|
11
12
|
const InstallOutcomeSchema = z.object({
|
|
12
13
|
claudeWrapper: z.string(),
|
|
@@ -49,10 +50,6 @@ function isNixStorePath(path: string): boolean {
|
|
|
49
50
|
return path === "/nix/store" || path.startsWith("/nix/store/");
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function isEacces(e: unknown): boolean {
|
|
53
|
-
return typeof e === "object" && e != null && "code" in e && e.code === "EACCES";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
53
|
function cannotWriteRcTarget(target: string): boolean {
|
|
57
54
|
if (EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_SHELL_RC) != null) return true;
|
|
58
55
|
if (isNixStorePath(target)) return true;
|
|
@@ -355,7 +352,7 @@ export function ensurePathInRc(rc: string): "added" | "present" | "skipped" {
|
|
|
355
352
|
try {
|
|
356
353
|
writeFileAtomic(target, `${body}${sep0}${addition}`, statSync(target).mode & 0o777);
|
|
357
354
|
} catch (e) {
|
|
358
|
-
if (
|
|
355
|
+
if (ErrnoSchema.safeParse(e).data?.code === "EACCES") return "skipped";
|
|
359
356
|
throw e;
|
|
360
357
|
}
|
|
361
358
|
return "added";
|
|
@@ -366,7 +363,7 @@ export function ensurePathInRc(rc: string): "added" | "present" | "skipped" {
|
|
|
366
363
|
try {
|
|
367
364
|
appendFileSync(target, `${sep}export PATH="${dir}:$PATH" ${PATH_LINE_MARK}\n`);
|
|
368
365
|
} catch (e) {
|
|
369
|
-
if (
|
|
366
|
+
if (ErrnoSchema.safeParse(e).data?.code === "EACCES") return "skipped";
|
|
370
367
|
throw e;
|
|
371
368
|
}
|
|
372
369
|
return "added";
|
|
@@ -411,7 +408,7 @@ export function removePathFromRc(rc: string): boolean {
|
|
|
411
408
|
try {
|
|
412
409
|
writeFileAtomic(target, kept.join("\n"), statSync(target).mode & 0o777);
|
|
413
410
|
} catch (e) {
|
|
414
|
-
if (
|
|
411
|
+
if (ErrnoSchema.safeParse(e).data?.code === "EACCES") return false;
|
|
415
412
|
throw e;
|
|
416
413
|
}
|
|
417
414
|
return true;
|
package/src/lib/oauth.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
|
|
3
|
-
import {
|
|
2
|
+
import { env } from "./paths.ts";
|
|
3
|
+
import { JsonTextSchema, ProfileResponseSchema, RefreshResponseSchema, TokenIdentitySchema, type OAuthCreds, type TokenIdentity } from "./types.ts";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const PROFILE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_OAUTH_PROFILE_URL) ?? "https://api.anthropic.com/api/oauth/profile";
|
|
5
|
+
const TOKEN_URL = env("TOKENMAXXING_OAUTH_TOKEN_URL", "https://platform.claude.com/v1/oauth/token");
|
|
6
|
+
const CLIENT_ID = env("TOKENMAXXING_OAUTH_CLIENT_ID", "9d1c250a-e61b-44d9-88ed-5944d1962f5e");
|
|
7
|
+
const PROFILE_URL = env("TOKENMAXXING_OAUTH_PROFILE_URL", "https://api.anthropic.com/api/oauth/profile");
|
|
9
8
|
|
|
10
9
|
const DEFAULT_SCOPES = [
|
|
11
10
|
"user:profile",
|
|
@@ -76,9 +75,7 @@ export async function refreshCredential(creds: OAuthCreds, now = Date.now(), sig
|
|
|
76
75
|
throw new Error(`token refresh failed (HTTP ${res.status}): ${detail}`);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
const parsed = RefreshResponseSchema.safeParse((
|
|
80
|
-
try { return JSON.parse(text); } catch { return null; }
|
|
81
|
-
})());
|
|
78
|
+
const parsed = RefreshResponseSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
82
79
|
if (!parsed.success) {
|
|
83
80
|
throw new Error(`token endpoint returned an unrecognized body (${text.length} bytes, withheld)`);
|
|
84
81
|
}
|
|
@@ -119,9 +116,7 @@ export async function fetchTokenIdentity(accessToken: string, signal?: AbortSign
|
|
|
119
116
|
throw new IdentityUnavailableError(res.status, `profile response body unreadable: ${e instanceof Error ? e.message : String(e)}`);
|
|
120
117
|
}
|
|
121
118
|
if (!res.ok) throw new IdentityUnavailableError(res.status, safeErrorDetail({ text }));
|
|
122
|
-
const parsed = ProfileResponseSchema.safeParse((
|
|
123
|
-
try { return JSON.parse(text); } catch { return null; }
|
|
124
|
-
})());
|
|
119
|
+
const parsed = ProfileResponseSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
125
120
|
if (!parsed.success) throw new IdentityUnavailableError(res.status, `profile endpoint returned an unrecognized body (${text.length} bytes, withheld)`);
|
|
126
121
|
return TokenIdentitySchema.parse({
|
|
127
122
|
accountUuid: parsed.data.account.uuid,
|
package/src/lib/paths.ts
CHANGED
|
@@ -6,7 +6,7 @@ const HOME = homedir();
|
|
|
6
6
|
|
|
7
7
|
const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
8
8
|
|
|
9
|
-
function env(name: string, fallback: string): string {
|
|
9
|
+
export function env(name: string, fallback: string): string {
|
|
10
10
|
return EnvOverrideSchema.parse(process.env[name]) ?? fallback;
|
|
11
11
|
}
|
|
12
12
|
|
package/src/lib/proc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ErrnoSchema } from "./types.ts";
|
|
2
2
|
|
|
3
3
|
export function pidStartTime(pid: number): string | null {
|
|
4
4
|
const res = Bun.spawnSync(["ps", "-p", String(pid), "-o", "lstart="], { env: { ...process.env, LC_ALL: "C" } });
|
|
@@ -12,9 +12,9 @@ export function pidExists(pid: number): boolean {
|
|
|
12
12
|
process.kill(pid, 0);
|
|
13
13
|
return true;
|
|
14
14
|
} catch (e) {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
if (
|
|
15
|
+
const code = ErrnoSchema.safeParse(e).data?.code;
|
|
16
|
+
if (code === "ESRCH") return false;
|
|
17
|
+
if (code === "EPERM") return true;
|
|
18
18
|
throw e;
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/lib/state.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { writeFileAtomic } from "./atomic.ts";
|
|
|
6
6
|
import {
|
|
7
7
|
AccountsIndexSchema,
|
|
8
8
|
ConfigSchema,
|
|
9
|
+
ErrnoSchema,
|
|
9
10
|
LastSwapSchema,
|
|
10
11
|
UsageStateSchema,
|
|
11
12
|
type Account,
|
|
@@ -181,8 +182,7 @@ export function writeUsage(input: UsageState, opts: { stamp?: boolean } = {}): b
|
|
|
181
182
|
try {
|
|
182
183
|
utimesSync(paths.usageJson, new Date(next.ts), new Date(next.ts));
|
|
183
184
|
} catch (e) {
|
|
184
|
-
|
|
185
|
-
if (!errno.success || errno.data.code !== "ENOENT") throw e;
|
|
185
|
+
if (ErrnoSchema.safeParse(e).data?.code !== "ENOENT") throw e;
|
|
186
186
|
}
|
|
187
187
|
return false;
|
|
188
188
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -244,3 +244,17 @@ export const CodexReconcileMarkerSchema = z.object({
|
|
|
244
244
|
accountId: z.string(),
|
|
245
245
|
ts: z.number(),
|
|
246
246
|
});
|
|
247
|
+
|
|
248
|
+
export const ErrnoSchema = z.object({ code: z.string() });
|
|
249
|
+
|
|
250
|
+
export const JsonTextSchema = z.codec(z.string(), z.unknown(), {
|
|
251
|
+
decode: (text, ctx) => {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(text);
|
|
254
|
+
} catch {
|
|
255
|
+
ctx.issues.push({ code: "invalid_format", format: "json", input: text });
|
|
256
|
+
return z.NEVER;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
encode: (value) => JSON.stringify(value),
|
|
260
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { writeFileAtomic } from "./atomic.ts";
|
|
5
|
+
import { http } from "./http.ts";
|
|
6
|
+
import { isNixPackaged } from "./install.ts";
|
|
7
|
+
import { withLock } from "./lock.ts";
|
|
8
|
+
import { log } from "./log.ts";
|
|
9
|
+
import { HOME, paths } from "./paths.ts";
|
|
10
|
+
|
|
11
|
+
const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const INSTALL_DEADLINE_MS = 60_000;
|
|
13
|
+
const REGISTRY_DEADLINE_MS = 15_000;
|
|
14
|
+
|
|
15
|
+
const OverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
16
|
+
const AttemptSchema = z.object({ attemptedAt: z.number() });
|
|
17
|
+
const VersionSchema = z.object({ version: z.string().min(1) });
|
|
18
|
+
const LatestSchema = z.object({ version: z.string().min(1), engines: z.looseObject({ bun: z.string().min(1).optional() }).optional() });
|
|
19
|
+
|
|
20
|
+
const updateJson = join(paths.home, "update.json");
|
|
21
|
+
const updateLock = join(paths.home, "update.lock");
|
|
22
|
+
|
|
23
|
+
function registry(): { base: URL; authorization: string | null } {
|
|
24
|
+
const raw = OverrideSchema.parse(process.env.TOKENMAXXING_NPM_REGISTRY) ?? "https://registry.npmjs.org";
|
|
25
|
+
const base = new URL(raw.endsWith("/") ? raw : `${raw}/`);
|
|
26
|
+
const authorization =
|
|
27
|
+
base.username !== "" || base.password !== "" ? `Basic ${btoa(`${decodeURIComponent(base.username)}:${decodeURIComponent(base.password)}`)}` : null;
|
|
28
|
+
base.username = "";
|
|
29
|
+
base.password = "";
|
|
30
|
+
return { base, authorization };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function realOrNull(path: string): string | null {
|
|
34
|
+
try {
|
|
35
|
+
return realpathSync(path);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function globalRootCandidates(): string[] {
|
|
42
|
+
const globalDir = OverrideSchema.parse(process.env.BUN_INSTALL_GLOBAL_DIR);
|
|
43
|
+
const bunInstall = OverrideSchema.parse(process.env.BUN_INSTALL);
|
|
44
|
+
return [
|
|
45
|
+
...(globalDir != null ? [globalDir] : []),
|
|
46
|
+
...(bunInstall != null ? [join(bunInstall, "install", "global")] : []),
|
|
47
|
+
join(dirname(dirname(process.execPath)), "install", "global"),
|
|
48
|
+
join(HOME, ".bun", "install", "global"),
|
|
49
|
+
].map((candidate) => resolve(candidate));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function packageDir(root: string): string {
|
|
53
|
+
return join(root, "node_modules", "tokenmaxxing");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function registryPackageDir(root: string): string | null {
|
|
57
|
+
const dir = packageDir(root);
|
|
58
|
+
try {
|
|
59
|
+
return lstatSync(dir).isSymbolicLink() ? null : dir;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function detectGlobalRoot(): string | null {
|
|
66
|
+
if (isNixPackaged()) return null;
|
|
67
|
+
const entry = realOrNull(Bun.main);
|
|
68
|
+
if (entry == null) return null;
|
|
69
|
+
return (
|
|
70
|
+
globalRootCandidates().find((root) => {
|
|
71
|
+
const dir = registryPackageDir(root);
|
|
72
|
+
return dir != null && entry === realOrNull(join(dir, "src", "main.ts"));
|
|
73
|
+
}) ?? null
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function installedVersion(root: string): string {
|
|
78
|
+
return VersionSchema.parse(JSON.parse(readFileSync(join(packageDir(root), "package.json"), "utf8"))).version;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDue(now: number): boolean {
|
|
82
|
+
if (!existsSync(updateJson)) return true;
|
|
83
|
+
let json: unknown;
|
|
84
|
+
try {
|
|
85
|
+
json = JSON.parse(readFileSync(updateJson, "utf8"));
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`${updateJson} is corrupt (unparsable JSON) - repair or remove the file`);
|
|
88
|
+
}
|
|
89
|
+
return now - AttemptSchema.parse(json).attemptedAt >= UPDATE_INTERVAL_MS;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function updateToLatest(root: string): Promise<void> {
|
|
93
|
+
const current = installedVersion(root);
|
|
94
|
+
const { base, authorization } = registry();
|
|
95
|
+
const latestUrl = new URL("tokenmaxxing/latest", base).href;
|
|
96
|
+
const signal = AbortSignal.timeout(REGISTRY_DEADLINE_MS);
|
|
97
|
+
const response = await http.get(latestUrl, { signal, headers: authorization ? { authorization } : undefined });
|
|
98
|
+
if (!response.ok) throw new Error(`${latestUrl} answered HTTP ${response.status}`);
|
|
99
|
+
const latest = LatestSchema.parse(await response.json());
|
|
100
|
+
if (Bun.semver.order(latest.version, current) <= 0) return;
|
|
101
|
+
const floor = latest.engines?.bun;
|
|
102
|
+
if (floor != null && !Bun.semver.satisfies(Bun.version, floor)) {
|
|
103
|
+
throw new Error(`tokenmaxxing@${latest.version} needs bun ${floor}, this is bun ${Bun.version}`);
|
|
104
|
+
}
|
|
105
|
+
const child = Bun.spawn([process.execPath, "add", "-g", "--registry", base.href, `tokenmaxxing@${latest.version}`], {
|
|
106
|
+
cwd: paths.home,
|
|
107
|
+
env: { ...process.env, BUN_INSTALL_GLOBAL_DIR: root },
|
|
108
|
+
stdout: "ignore",
|
|
109
|
+
stderr: "inherit",
|
|
110
|
+
timeout: INSTALL_DEADLINE_MS,
|
|
111
|
+
killSignal: "SIGKILL",
|
|
112
|
+
});
|
|
113
|
+
await child.exited;
|
|
114
|
+
if (child.exitCode !== 0) throw new Error(`bun add -g tokenmaxxing@${latest.version} exited ${child.signalCode ?? child.exitCode}`);
|
|
115
|
+
const installed = installedVersion(root);
|
|
116
|
+
if (installed !== latest.version) throw new Error(`bun add -g tokenmaxxing@${latest.version} left ${installed} installed`);
|
|
117
|
+
log("update.done", { from: current, to: latest.version });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function maybeAutoUpdate(): Promise<void> {
|
|
121
|
+
const root = detectGlobalRoot();
|
|
122
|
+
if (root == null) return;
|
|
123
|
+
await withLock(updateLock, async () => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (!isDue(now)) return;
|
|
126
|
+
writeFileAtomic(updateJson, JSON.stringify({ attemptedAt: now }) + "\n");
|
|
127
|
+
try {
|
|
128
|
+
await updateToLatest(root);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(`self-update from npm failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
package/src/lib/usage.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { delay } from "es-toolkit";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./claudebin.ts";
|
|
5
5
|
import { log } from "./log.ts";
|
|
6
|
-
import { RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
|
|
6
|
+
import { JsonTextSchema, RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
export function normalizeResetsAt(v: unknown): number | null {
|
|
9
9
|
const num = z.number().finite().safeParse(v);
|
|
@@ -279,21 +279,16 @@ const SpawnResultSchema = z.object({
|
|
|
279
279
|
type SpawnResult = z.infer<typeof SpawnResultSchema>;
|
|
280
280
|
|
|
281
281
|
async function spawnClaudeBounded(cmd: string[], env: Record<string, string>): Promise<SpawnResult | null> {
|
|
282
|
-
const p = Bun.spawn(cmd, { env, stdout: "pipe", stderr: "pipe" });
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await p.exited;
|
|
293
|
-
return { exitCode: p.exitCode, stdout, stderr };
|
|
294
|
-
} finally {
|
|
295
|
-
clearTimeout(killer);
|
|
296
|
-
}
|
|
282
|
+
const p = Bun.spawn(cmd, { env, stdout: "pipe", stderr: "pipe", timeout: PROBE_KILL_MS, killSignal: "SIGKILL" });
|
|
283
|
+
const reads = Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text()]);
|
|
284
|
+
const settled = await Promise.race([
|
|
285
|
+
reads,
|
|
286
|
+
p.exited.then(() => delay(PIPE_GRACE_MS)).then(() => null),
|
|
287
|
+
]);
|
|
288
|
+
if (settled === null) return null;
|
|
289
|
+
const [stdout, stderr] = settled;
|
|
290
|
+
await p.exited;
|
|
291
|
+
return { exitCode: p.exitCode, stdout, stderr };
|
|
297
292
|
}
|
|
298
293
|
|
|
299
294
|
async function probeUsageOnce(env: Record<string, string>, now: number): Promise<UsageWindows | null> {
|
|
@@ -314,7 +309,7 @@ async function probeUsageOnce(env: Record<string, string>, now: number): Promise
|
|
|
314
309
|
return null;
|
|
315
310
|
}
|
|
316
311
|
|
|
317
|
-
const j = z.object({ result: z.string() }).safeParse(
|
|
312
|
+
const j = z.object({ result: z.string() }).safeParse(JsonTextSchema.safeParse(out).data);
|
|
318
313
|
const text = j.success ? j.data.result : out;
|
|
319
314
|
const full = parseUsageText(text, now);
|
|
320
315
|
if (!full) {
|