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 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, no usage read |
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.** Muse Code exposes no read-only quota endpoint, so every Muse Code account stays unmeasured.
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.168.0",
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",
@@ -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 managed (Home Manager / nix-store) - PATH was not auto-edited",
67
- detail: `put ${paths.binDir} on PATH via home.sessionPath (programs.tokenmaxxing Home Manager module sets this), e.g.`,
68
- exportLine: `home.sessionPath = [ "${paths.binDir}" ];`,
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
- if (isNixPackaged()) {
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 { museAuthJsonFor, musePaths, musePool, museStoreDirFor } from "./paths.ts";
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 ?? "") !== "" || (parsed.data.api_key ?? "") !== "", harvest });
52
+ out.push({ id, usable: (parsed.data.access_token ?? "") !== "", harvest });
52
53
  }
53
54
  return out;
54
55
  }
55
56
 
56
- async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
57
- return new Map(accounts.map((a) => [a.id, { ok: false, reason: "Muse Code exposes no read-only quota endpoint" }]));
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 (