tokenmaxxing 1.72.0 → 1.74.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/DESIGN.md +1 -1
- package/package.json +1 -1
- package/src/entries/codexstophook.ts +0 -2
- package/src/lib/claudebin.ts +1 -2
- package/src/lib/codex.ts +1 -2
- package/src/lib/codexauth.ts +0 -1
- package/src/lib/grok.ts +1 -2
- package/src/lib/install.ts +1 -4
- package/src/lib/opencodego.ts +1 -2
- package/src/lib/presence.ts +1 -2
- package/src/lib/sessions.ts +3 -31
package/DESIGN.md
CHANGED
|
@@ -42,7 +42,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
|
|
|
42
42
|
- `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` and `opencode-go-*` state of the status-only pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
|
|
43
43
|
- Per-account **credential stores** follow the platform's Claude Code store: macOS = a login-keychain item keyed by the store path (never plaintext on disk); Linux = a 0600 `.credentials.json` inside the 0700 store directory (the same plaintext model claude itself uses - its Linux build has no keyring path at all, binary-verified 2.1.205). One `credstore` facade dispatches on a `{kind: keychain|file}` target; call sites never branch on platform. tokenmaxxing writes a store once, at onboarding; Claude Code's own refresh is the only writer afterwards.
|
|
44
44
|
|
|
45
|
-
- A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): it runs the decision with no seat (nothing moves), folds fresh tees into the index, samples up to three accounts whose last sample attempt is oldest (the direct no-spend usage GET), deletes leftover
|
|
45
|
+
- A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): it runs the decision with no seat (nothing moves), folds fresh tees into the index, samples up to three accounts whose last sample attempt is oldest (the direct no-spend usage GET), deletes leftover atomic-write temp files under the state root, and on a Bun global install checks npm for a newer version once a day, so the cached figures every placement reads stay fresh.
|
|
46
46
|
- A usage hub service (`com.tokenmaxxing.hub` launchd agent on macOS, `tokenmaxxing-hub.service` systemd user service on Linux) running `tokenmaxxing serve`, so the [usage hub](docs/content/docs/hub.mdx) is up in the background beside the check timer.
|
|
47
47
|
|
|
48
48
|
The switching path runs no long-lived daemon. The statusline pushes usage. Hooks and the supervisor react. The periodic check job above is the only recurring process in the switching path. The [usage hub](docs/content/docs/hub.mdx) that answers the CLIProxyAPI management subset for a dashboard from the cached figures runs outside the switching path, as a background service beside the check timer (`tokenmaxxing serve` runs the same server in the foreground). The `claude` binary, `~/.claude.json`, and Claude Code's default credential store are untouched; inside `~/.claude` only `settings.json` gets the five entries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { mkdirSync } from "node:fs";
|
|
3
2
|
import type { z } from "zod";
|
|
4
3
|
import { codexPaths, optionalEnv } from "../lib/paths.ts";
|
|
5
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
@@ -50,7 +49,6 @@ async function handleCodexStop(rawStdin: string): Promise<void> {
|
|
|
50
49
|
await compactBeforeMove({ sessionId, now });
|
|
51
50
|
const decision = await evaluateAndMaybeSwap(codex, now);
|
|
52
51
|
if (decision.swapped && decision.account) {
|
|
53
|
-
mkdirSync(codexPaths.respawnDir, { recursive: true });
|
|
54
52
|
const payload: z.infer<typeof CodexRespawnMarkerSchema> = { accountId: decision.account.id, sessionId, ts: Date.now() };
|
|
55
53
|
writeFileAtomic(join(codexPaths.respawnDir, supervisorId), JSON.stringify(payload));
|
|
56
54
|
log("codexstop.marker", { supervisorId: supervisorId.slice(0, 8) });
|
package/src/lib/claudebin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { uniq } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -30,7 +30,6 @@ export function wrapperEntryRateTripped(now: number): boolean {
|
|
|
30
30
|
entries = entries.filter((t) => now - t < WRAP_RATE_WINDOW_MS);
|
|
31
31
|
entries.push(now);
|
|
32
32
|
try {
|
|
33
|
-
mkdirSync(paths.home, { recursive: true });
|
|
34
33
|
writeFileAtomic(file, JSON.stringify({ entries }));
|
|
35
34
|
} catch { }
|
|
36
35
|
return entries.length > WRAP_RATE_MAX;
|
package/src/lib/codex.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { writeFileAtomic } from "./atomic.ts";
|
|
4
4
|
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "./claudebin.ts";
|
|
@@ -116,7 +116,6 @@ async function login(): Promise<Harvest | null> {
|
|
|
116
116
|
const real = resolveRealCodex();
|
|
117
117
|
const onboardDir = codexPaths.onboardDir;
|
|
118
118
|
rmSync(onboardDir, { recursive: true, force: true });
|
|
119
|
-
mkdirSync(onboardDir, { recursive: true });
|
|
120
119
|
writeFileAtomic(join(onboardDir, "config.toml"), 'cli_auth_credentials_store = "file"\n');
|
|
121
120
|
|
|
122
121
|
const savedTermios = saveTermios();
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -32,7 +32,6 @@ export function codexStoreUsable(accountId: string): boolean {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function writeCodexStoreAuth(accountId: string, auth: CodexAuthJson): void {
|
|
35
|
-
mkdirSync(codexStoreDirFor(accountId), { recursive: true });
|
|
36
35
|
writeFileAtomic(codexAuthJsonFor(accountId), JSON.stringify(CodexAuthJsonSchema.parse(auth), null, 2), 0o600);
|
|
37
36
|
}
|
|
38
37
|
|
package/src/lib/grok.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
@@ -31,7 +31,6 @@ function harvestOf(id: string, key: string, entry: GrokAuthEntry): Harvest {
|
|
|
31
31
|
tier: null,
|
|
32
32
|
sample: null,
|
|
33
33
|
park: async () => {
|
|
34
|
-
mkdirSync(grokStoreDirFor(id), { recursive: true });
|
|
35
34
|
writeFileAtomic(grokAuthJsonFor(id), JSON.stringify({ [key]: entry }, null, 2), 0o600);
|
|
36
35
|
},
|
|
37
36
|
};
|
package/src/lib/install.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accessSync, appendFileSync, constants, existsSync,
|
|
1
|
+
import { accessSync, appendFileSync, constants, existsSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { userInfo } from "node:os";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { escape } from "es-toolkit";
|
|
@@ -89,7 +89,6 @@ exec ${JSON.stringify(bun)} --no-env-file run ${JSON.stringify(entry)} "$@"
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export function installSupervisor(): InstallOutcome {
|
|
92
|
-
mkdirSync(paths.binDir, { recursive: true });
|
|
93
92
|
const target = installedBin();
|
|
94
93
|
const entry = realpathSync(Bun.main);
|
|
95
94
|
if (isNixPackaged()) {
|
|
@@ -147,7 +146,6 @@ export function installCodexStopHook(): void {
|
|
|
147
146
|
],
|
|
148
147
|
},
|
|
149
148
|
};
|
|
150
|
-
mkdirSync(codexPaths.home, { recursive: true });
|
|
151
149
|
writeFileAtomic(codexPaths.hooksJson, JSON.stringify(next, null, 2) + "\n");
|
|
152
150
|
}
|
|
153
151
|
|
|
@@ -198,7 +196,6 @@ export function codexSupervisorLink(): string {
|
|
|
198
196
|
}
|
|
199
197
|
|
|
200
198
|
export function installCodexSupervisor(): void {
|
|
201
|
-
mkdirSync(paths.binDir, { recursive: true });
|
|
202
199
|
writeFileAtomic(codexSupervisorLink(), `#!/bin/sh\nexec ${JSON.stringify(installedBin())} __supervise-codex "$@"\n`, 0o755);
|
|
203
200
|
installCodexStopHook();
|
|
204
201
|
}
|
package/src/lib/opencodego.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
@@ -29,7 +29,6 @@ function harvestOf(key: string): Harvest {
|
|
|
29
29
|
tier: "go",
|
|
30
30
|
sample: null,
|
|
31
31
|
park: async () => {
|
|
32
|
-
mkdirSync(opencodeGoStoreDirFor(id), { recursive: true });
|
|
33
32
|
writeFileAtomic(opencodeGoAuthJsonFor(id), JSON.stringify({ "opencode-go": { type: "api", key } }, null, 2), 0o600);
|
|
34
33
|
},
|
|
35
34
|
};
|
package/src/lib/presence.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
@@ -14,7 +14,6 @@ const PresenceSchema = z.object({
|
|
|
14
14
|
export function writePresence(input: { dir: string; id: string; accountId: string; pid: number }): void {
|
|
15
15
|
const startedAt = pidStartTime(input.pid);
|
|
16
16
|
if (startedAt == null) throw new Error(`could not read pid ${input.pid}'s start time (ps lstart) - refusing to write an unverifiable presence file`);
|
|
17
|
-
mkdirSync(input.dir, { recursive: true });
|
|
18
17
|
writeFileAtomic(join(input.dir, input.id), JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid: input.pid, startedAt })));
|
|
19
18
|
}
|
|
20
19
|
|
package/src/lib/sessions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, type Dirent } from "node:fs";
|
|
2
2
|
import { basename, join, sep } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { errorMessage, log } from "./log.ts";
|
|
@@ -19,7 +19,6 @@ function sessionFile(sid: string): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function saveSessionFlags(sid: string, flags: string[], cwd: string): void {
|
|
22
|
-
mkdirSync(sessionsDir(), { recursive: true });
|
|
23
22
|
writeFileAtomic(sessionFile(sid), JSON.stringify({ flags, cwd }));
|
|
24
23
|
}
|
|
25
24
|
|
|
@@ -39,28 +38,9 @@ export function liveSessionId(sid: string): string {
|
|
|
39
38
|
|
|
40
39
|
function recordLiveSession(sid: string, current: string): void {
|
|
41
40
|
const session = loadSession(sid);
|
|
42
|
-
mkdirSync(sessionsDir(), { recursive: true });
|
|
43
41
|
writeFileAtomic(sessionFile(sid), JSON.stringify({ flags: session?.flags ?? [], cwd: session?.cwd ?? process.cwd(), current }));
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
const DEAD_STATE_ENTRIES = [
|
|
47
|
-
"model-usage.json",
|
|
48
|
-
"accounts.json.v1-backup",
|
|
49
|
-
"codex-accounts.json.v1-backup",
|
|
50
|
-
"nextcheck.json",
|
|
51
|
-
"usage.json",
|
|
52
|
-
"lastswap.json",
|
|
53
|
-
"depleted.json",
|
|
54
|
-
"codex-lastswap.json",
|
|
55
|
-
"creds",
|
|
56
|
-
"codex-creds",
|
|
57
|
-
"codex-reconcile",
|
|
58
|
-
"sample",
|
|
59
|
-
"setup-tokens.json",
|
|
60
|
-
"setup-token",
|
|
61
|
-
"cloud",
|
|
62
|
-
];
|
|
63
|
-
|
|
64
44
|
const TMP_MARKER = ".tmp.";
|
|
65
45
|
const TMP_GRACE_MS = 3600 * 1000;
|
|
66
46
|
|
|
@@ -88,14 +68,7 @@ function tmpSweepDirs(root: string): string[] {
|
|
|
88
68
|
return dirs;
|
|
89
69
|
}
|
|
90
70
|
|
|
91
|
-
function
|
|
92
|
-
for (const name of DEAD_STATE_ENTRIES) {
|
|
93
|
-
try {
|
|
94
|
-
rmSync(join(paths.home, name), { recursive: true, force: true });
|
|
95
|
-
} catch (e) {
|
|
96
|
-
log("state.dead_entry_failed", { name, err: errorMessage(e) });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
71
|
+
function pruneTmpFiles(now: number, root: string): void {
|
|
99
72
|
for (const dir of tmpSweepDirs(root)) {
|
|
100
73
|
for (const f of listDir(dir, root)) {
|
|
101
74
|
if (!f.isFile() || !f.name.includes(TMP_MARKER)) continue;
|
|
@@ -115,7 +88,7 @@ export function pruneStaleSessions(now: number): void {
|
|
|
115
88
|
} catch {
|
|
116
89
|
return;
|
|
117
90
|
}
|
|
118
|
-
|
|
91
|
+
pruneTmpFiles(now, root);
|
|
119
92
|
const dir = sessionsDir();
|
|
120
93
|
for (const f of listDir(dir, root)) {
|
|
121
94
|
const p = join(dir, f.name);
|
|
@@ -151,7 +124,6 @@ export function adoptLiveSession(session: SupervisedSession, stdinSid: string |
|
|
|
151
124
|
}
|
|
152
125
|
|
|
153
126
|
export function writeRespawnMarker(input: { session: SupervisedSession; accountId: string; waitUntil: number; compact: boolean }): void {
|
|
154
|
-
mkdirSync(paths.respawnDir, { recursive: true });
|
|
155
127
|
const payload: z.infer<typeof RespawnMarkerSchema> = {
|
|
156
128
|
accountId: input.accountId,
|
|
157
129
|
ts: Date.now(),
|