tokenmaxxing 1.168.0 → 1.170.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/README.md +2 -29
- package/package.json +2 -3
- package/src/lib/install.ts +4 -42
- package/src/lib/muse.ts +69 -6
- package/src/lib/update.ts +0 -2
package/README.md
CHANGED
|
@@ -25,33 +25,6 @@ bun add -g tokenmaxxing
|
|
|
25
25
|
tokenmaxxing init
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Or with Nix (same source-run-by-Bun package; `init` still owns the stores, the `claude` shim, and settings merges). Install onto PATH first, then init: `nix run ... -- init` alone leaves supervisor shims without a stable `tokenmaxxing` on PATH after the ephemeral run exits.
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
nix profile install github:anaclumos/tokenmaxxing
|
|
32
|
-
tokenmaxxing init
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
nix-darwin:
|
|
36
|
-
|
|
37
|
-
```nix
|
|
38
|
-
inputs.tokenmaxxing.url = "github:anaclumos/tokenmaxxing";
|
|
39
|
-
modules = [
|
|
40
|
-
inputs.tokenmaxxing.darwinModules.withOverlay
|
|
41
|
-
{ programs.tokenmaxxing.enable = true; }
|
|
42
|
-
];
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Home Manager:
|
|
46
|
-
|
|
47
|
-
```nix
|
|
48
|
-
imports = [ inputs.tokenmaxxing.homeManagerModules.default ];
|
|
49
|
-
programs.tokenmaxxing.enable = true;
|
|
50
|
-
programs.tokenmaxxing.package = inputs.tokenmaxxing.packages.${pkgs.system}.default;
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
With either module, run `tokenmaxxing init` afterwards.
|
|
54
|
-
|
|
55
28
|
`init` verifies and pins the real `claude` binary, opens an isolated `claude` session for your first pooled account (run `/login` there, even if you are already signed in; the login you already have stays for sessions started outside the supervisor), installs the `claude` supervisor and the `xx` entry point, five `settings.json` entries (the tokenmaxxing statusLine, a subagentStatusLine, a Stop hook, a StopFailure hook, a SessionStart hook), the periodic check timer, and the usage hub service, and adds the supervisor's bin dir to PATH in your shell rc (idempotent; it must sit ahead of the real `claude` to intercept it). Restart your shell, then add more accounts and go:
|
|
56
29
|
|
|
57
30
|
```sh
|
|
@@ -67,7 +40,7 @@ claude # use claude as always; each session gets its own accoun
|
|
|
67
40
|
| `tokenmaxxing init --codex` | same for codex: log in the first account (isolated), install codex supervisor + Stop hook |
|
|
68
41
|
| `tokenmaxxing init --pi` | verify and pin the real `pi` and install the `pi` supervisor, which runs pi sessions on pooled Claude and ChatGPT accounts that have a pi login |
|
|
69
42
|
| `tokenmaxxing init --grok` | pool the first grok login, status-only: no supervisor, no switching |
|
|
70
|
-
| `tokenmaxxing init --muse` | pool the first Muse Code login, status-only: no supervisor, no switching
|
|
43
|
+
| `tokenmaxxing init --muse` | pool the first Muse Code login, status-only: no supervisor, no switching |
|
|
71
44
|
| `tokenmaxxing add [--codex \| --grok \| --muse]` | register an additional account in that pool (isolated login, harvested once into its own store) |
|
|
72
45
|
| `tokenmaxxing auth [--codex \| --grok \| --muse] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every account that is flagged or has no usable credential in its store, one by one |
|
|
73
46
|
| `tokenmaxxing auth --pi [--codex] [sel \| --all]` | log a pooled Claude account (or a codex account with `--codex`) into pi, isolated, and keep that login as the account's pi credential after an identity check; `--all` walks every account without a pi login |
|
|
@@ -153,7 +126,7 @@ pi keeps its own credential and refreshes it itself, so each pooled account gets
|
|
|
153
126
|
- **Keychain (macOS).** The first keychain access for a new store happens inside the interactive `init`, `add`, or `auth` run, where a prompt can be answered, not inside a headless hook.
|
|
154
127
|
- **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
|
|
155
128
|
- **The status-only grok pool does not switch.** grok `status` shows the weekly subscription credit window, and no grok session moves.
|
|
156
|
-
- **The status-only Muse Code pool does not switch and shows no usage
|
|
129
|
+
- **The status-only Muse Code pool does not switch, and shows no usage before the first model request of a window.** Its usage read is the key mint, which reports no usage until a model request opens the 5-hour window.
|
|
157
130
|
|
|
158
131
|
## How it's built
|
|
159
132
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.170.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",
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"dev": "bun run src/main.ts",
|
|
29
|
-
"typecheck": "tsc --noEmit"
|
|
30
|
-
"nix:bun": "bunx bun2nix@2.1.2 -o bun.nix"
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
31
30
|
},
|
|
32
31
|
"devDependencies": {
|
|
33
32
|
"@types/bun": "1.3.14",
|
package/src/lib/install.ts
CHANGED
|
@@ -28,15 +28,6 @@ export function isBinDirAhead(bin: { name: string; key: BinKey } = CLAUDE_BIN):
|
|
|
28
28
|
|
|
29
29
|
const envFlag = (name: string): boolean => z.stringbool().optional().parse(optionalEnv(name)) === true;
|
|
30
30
|
|
|
31
|
-
export function isNixPackaged(): boolean {
|
|
32
|
-
if (envFlag("TOKENMAXXING_NIX")) return true;
|
|
33
|
-
try {
|
|
34
|
-
return realpathSync(Bun.main).startsWith("/nix/store/");
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
function skipImperativeTimer(): boolean {
|
|
41
32
|
return envFlag("TOKENMAXXING_SKIP_TIMER");
|
|
42
33
|
}
|
|
@@ -45,13 +36,8 @@ function skipImperativeHub(): boolean {
|
|
|
45
36
|
return envFlag("TOKENMAXXING_SKIP_HUB");
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
function isNixStorePath(path: string): boolean {
|
|
49
|
-
return path === "/nix/store" || path.startsWith("/nix/store/");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
function cannotWriteRcTarget(target: string): boolean {
|
|
53
40
|
if (envFlag("TOKENMAXXING_SKIP_SHELL_RC")) return true;
|
|
54
|
-
if (isNixStorePath(target)) return true;
|
|
55
41
|
if (!existsSync(target)) return false;
|
|
56
42
|
try {
|
|
57
43
|
accessSync(target, constants.W_OK);
|
|
@@ -63,40 +49,16 @@ function cannotWriteRcTarget(target: string): boolean {
|
|
|
63
49
|
|
|
64
50
|
export function managedShellRcSkipLines(): { headline: string; detail: string; exportLine: string } {
|
|
65
51
|
return {
|
|
66
|
-
headline: "shell rc is
|
|
67
|
-
detail: `put ${paths.binDir} on PATH
|
|
68
|
-
exportLine: `
|
|
52
|
+
headline: "shell rc is not writable - PATH was not auto-edited",
|
|
53
|
+
detail: `put ${paths.binDir} on PATH ahead of the real claude, e.g.`,
|
|
54
|
+
exportLine: `export PATH="${paths.binDir}:$PATH"`,
|
|
69
55
|
};
|
|
70
56
|
}
|
|
71
57
|
|
|
72
|
-
function nixSupervisorShim(bun: string, entry: string): string {
|
|
73
|
-
return `#!/bin/sh
|
|
74
|
-
dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
|
75
|
-
old_ifs=$IFS
|
|
76
|
-
IFS=:
|
|
77
|
-
new_path=
|
|
78
|
-
for p in $PATH; do
|
|
79
|
-
[ "$p" = "$dir" ] && continue
|
|
80
|
-
if [ -n "$new_path" ]; then new_path="$new_path:$p"; else new_path="$p"; fi
|
|
81
|
-
done
|
|
82
|
-
IFS=$old_ifs
|
|
83
|
-
PATH=$new_path
|
|
84
|
-
export PATH
|
|
85
|
-
if command -v tokenmaxxing >/dev/null 2>&1; then
|
|
86
|
-
exec tokenmaxxing "$@"
|
|
87
|
-
fi
|
|
88
|
-
exec ${shellQuote(bun)} --no-env-file run ${shellQuote(entry)} "$@"
|
|
89
|
-
`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
58
|
function installEntryPoints(): string {
|
|
93
59
|
const target = installedBin();
|
|
94
60
|
const entry = realpathSync(Bun.main);
|
|
95
|
-
|
|
96
|
-
writeFileAtomic(target, nixSupervisorShim(process.execPath, entry), 0o755);
|
|
97
|
-
} else {
|
|
98
|
-
writeFileAtomic(target, `#!/bin/sh\nexec ${shellQuote(process.execPath)} --no-env-file run ${shellQuote(entry)} "$@"\n`, 0o755);
|
|
99
|
-
}
|
|
61
|
+
writeFileAtomic(target, `#!/bin/sh\nexec ${shellQuote(process.execPath)} --no-env-file run ${shellQuote(entry)} "$@"\n`, 0o755);
|
|
100
62
|
writeFileAtomic(join(paths.binDir, "xx"), `#!/bin/sh\nexec ${shellQuote(target)} "$@"\n`, 0o755);
|
|
101
63
|
return target;
|
|
102
64
|
}
|
package/src/lib/muse.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { writeFileAtomic } from "./atomic.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { http, safeErrorDetail } from "./http.ts";
|
|
4
|
+
import { errorMessage } from "./log.ts";
|
|
5
|
+
import { env, museAuthJsonFor, musePaths, musePool, museStoreDirFor } from "./paths.ts";
|
|
4
6
|
import type { SampleReport } from "./provider.ts";
|
|
5
7
|
import { readJsonFile, type Harvest } from "./state.ts";
|
|
6
8
|
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
7
|
-
import { ErrnoSchema, type Account } from "./types.ts";
|
|
9
|
+
import { EpochSecondsSchema, ErrnoSchema, JsonTextSchema, type Account } from "./types.ts";
|
|
8
10
|
import { c } from "../cli/render.ts";
|
|
9
11
|
|
|
10
12
|
const MuseProviderSchema = z.looseObject({
|
|
11
13
|
access_token: z.string().optional(),
|
|
12
|
-
api_key: z.string().optional(),
|
|
13
14
|
user_email: z.string().nullish(),
|
|
14
15
|
});
|
|
15
16
|
|
|
@@ -48,13 +49,75 @@ function readAuth(path: string): AuthEntry[] {
|
|
|
48
49
|
writeFileAtomic(museAuthJsonFor(id), JSON.stringify({ ...file, providers: { [key]: value } }, null, 2), 0o600);
|
|
49
50
|
},
|
|
50
51
|
};
|
|
51
|
-
out.push({ id, usable: (parsed.data.access_token ?? "") !== ""
|
|
52
|
+
out.push({ id, usable: (parsed.data.access_token ?? "") !== "", harvest });
|
|
52
53
|
}
|
|
53
54
|
return out;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const SubscriptionWindowSchema = z.looseObject({ used_percent: z.number(), resets_at: EpochSecondsSchema });
|
|
58
|
+
|
|
59
|
+
const MintSchema = z.looseObject({
|
|
60
|
+
subs_usage: z
|
|
61
|
+
.looseObject({
|
|
62
|
+
window: SubscriptionWindowSchema.extend({ window_duration_mins: z.number() }),
|
|
63
|
+
weekly: SubscriptionWindowSchema,
|
|
64
|
+
})
|
|
65
|
+
.nullable(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const WEEK_S = 7 * 86400;
|
|
69
|
+
|
|
70
|
+
function usageUrl(): string {
|
|
71
|
+
return env("TOKENMAXXING_MUSE_USAGE_URL", "https://api.meta.ai/muse-code/key");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function storedAccessToken(id: string): string | null {
|
|
75
|
+
const file = readFile(museAuthJsonFor(id));
|
|
76
|
+
if (file == null) return null;
|
|
77
|
+
for (const value of Object.values(file.providers)) {
|
|
78
|
+
const token = MuseProviderSchema.safeParse(value).data?.access_token ?? "";
|
|
79
|
+
if (token !== "") return token;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readMuseUsage(account: Account, now: number): Promise<SampleReport> {
|
|
85
|
+
let token: string | null;
|
|
86
|
+
try {
|
|
87
|
+
token = storedAccessToken(account.id);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { ok: false, reason: `this account's store is unreadable (${errorMessage(e)}) - run \`tokenmaxxing auth --muse\`` };
|
|
90
|
+
}
|
|
91
|
+
if (token == null) return { ok: false, reason: "no access_token in this account's store - run `tokenmaxxing auth --muse`" };
|
|
92
|
+
let res: Response;
|
|
93
|
+
let text: string;
|
|
94
|
+
try {
|
|
95
|
+
res = await http.post(usageUrl(), { headers: { Authorization: `Bearer ${token}` }, json: {} });
|
|
96
|
+
text = await res.text();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { ok: false, reason: `Muse Code usage read failed: endpoint unreachable: ${errorMessage(e)}` };
|
|
99
|
+
}
|
|
100
|
+
if (res.status === 401 || res.status === 403) {
|
|
101
|
+
account.needsReauth = true;
|
|
102
|
+
return { ok: false, reason: `Muse Code rejected the stored login (HTTP ${res.status}) - run \`tokenmaxxing auth --muse\`` };
|
|
103
|
+
}
|
|
104
|
+
if (!res.ok) return { ok: false, reason: `Muse Code usage read failed: HTTP ${res.status}: ${safeErrorDetail({ text })}` };
|
|
105
|
+
const parsed = MintSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
106
|
+
if (!parsed.success) return { ok: false, reason: "Muse Code usage read failed: endpoint returned an unexpected body shape (withheld)" };
|
|
107
|
+
if (parsed.data.subs_usage == null) return { ok: false, reason: "Muse Code reports no usage until a model request opens the 5-hour window" };
|
|
108
|
+
const { window, weekly } = parsed.data.subs_usage;
|
|
109
|
+
account.windows = [
|
|
110
|
+
{ name: null, usedPercentage: window.used_percent, resetsAt: window.resets_at, windowSeconds: window.window_duration_mins * 60, sampledAt: now },
|
|
111
|
+
{ name: null, usedPercentage: weekly.used_percent, resetsAt: weekly.resets_at, windowSeconds: WEEK_S, sampledAt: now },
|
|
112
|
+
];
|
|
113
|
+
account.lastUsageAt = now;
|
|
114
|
+
return { ok: true, source: "probe" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function samplePool(accounts: Account[], now: number): Promise<Map<string, SampleReport>> {
|
|
118
|
+
const reports = new Map<string, SampleReport>();
|
|
119
|
+
await Promise.all(accounts.map(async (account) => reports.set(account.id, await readMuseUsage(account, now))));
|
|
120
|
+
return reports;
|
|
58
121
|
}
|
|
59
122
|
|
|
60
123
|
export const muse = statusOnlyProvider({
|
package/src/lib/update.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { z } from "zod";
|
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import { realpathOrNull } from "./claudebin.ts";
|
|
6
6
|
import { http } from "./http.ts";
|
|
7
|
-
import { isNixPackaged } from "./install.ts";
|
|
8
7
|
import { withLock } from "./lock.ts";
|
|
9
8
|
import { errorMessage, log } from "./log.ts";
|
|
10
9
|
import { env, HOME, optionalEnv, paths } from "./paths.ts";
|
|
@@ -56,7 +55,6 @@ function registryPackageDir(root: string): string | null {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
function detectGlobalRoot(): string | null {
|
|
59
|
-
if (isNixPackaged()) return null;
|
|
60
58
|
const entry = realpathOrNull(Bun.main);
|
|
61
59
|
if (entry == null) return null;
|
|
62
60
|
return (
|