tokenmaxxing 1.5.0 → 1.6.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
@@ -23,6 +23,33 @@ bun add -g tokenmaxxing
23
23
  tokenmaxxing init
24
24
  ```
25
25
 
26
+ Or with Nix (same source-run-by-Bun package; `init` still owns credentials, 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:
27
+
28
+ ```sh
29
+ nix profile install github:anaclumos/tokenmaxxing
30
+ tokenmaxxing init
31
+ ```
32
+
33
+ nix-darwin:
34
+
35
+ ```nix
36
+ # flake inputs: tokenmaxxing.url = "github:anaclumos/tokenmaxxing";
37
+ modules = [
38
+ inputs.tokenmaxxing.darwinModules.withOverlay
39
+ { programs.tokenmaxxing.enable = true; }
40
+ ];
41
+ # then: tokenmaxxing init
42
+ ```
43
+
44
+ Home Manager:
45
+
46
+ ```nix
47
+ imports = [ inputs.tokenmaxxing.homeManagerModules.default ];
48
+ programs.tokenmaxxing.enable = true;
49
+ programs.tokenmaxxing.package = inputs.tokenmaxxing.packages.${pkgs.system}.default;
50
+ # then: tokenmaxxing init
51
+ ```
52
+
26
53
  `init` imports the account you're already on, installs the `claude` supervisor + four `settings.json` entries (the tokenmaxxing statusLine, a subagentStatusLine, a Stop hook, a SessionStart hook), 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:
27
54
 
28
55
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.5.0",
3
+ "version": "1.6.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",
@@ -33,10 +33,11 @@
33
33
  "dev": "bun run src/main.ts",
34
34
  "test": "bun test",
35
35
  "typecheck": "tsc --noEmit",
36
- "prepublishOnly": "bun run typecheck && bun run test"
36
+ "prepublishOnly": "bun run typecheck && bun run test",
37
+ "nix:bun": "bunx bun2nix@2.1.2 -o bun.nix"
37
38
  },
38
39
  "devDependencies": {
39
- "@types/bun": "latest",
40
+ "@types/bun": "1.3.14",
40
41
  "typescript": "^5.6.0"
41
42
  },
42
43
  "dependencies": {
@@ -33,14 +33,65 @@ export function isBinDirAhead(): boolean {
33
33
  }
34
34
  }
35
35
 
36
+ // Optional "1"/"true"/"yes" flag; unset/empty → undefined (feature off).
37
+ const EnvFlagSchema = z.enum(["1", "true", "yes"]).optional().catch(undefined);
38
+
39
+ /** True when this process is the Nix-packaged CLI (flake startScript sets
40
+ * TOKENMAXXING_NIX=1; store-path Bun.main is the fallback for wraps that
41
+ * forget the env). Env overrides parse at the read site. */
42
+ export function isNixPackaged(): boolean {
43
+ if (EnvFlagSchema.parse(process.env.TOKENMAXXING_NIX) != null) return true;
44
+ try {
45
+ return realpathSync(Bun.main).startsWith("/nix/store/");
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /** True when a Nix module owns the periodic check timer; init must not write
52
+ * a second imperative unit. */
53
+ export function skipImperativeTimer(): boolean {
54
+ return EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_TIMER) != null;
55
+ }
56
+
57
+ /** Nix supervisor shim: prefer a PATH-stable `tokenmaxxing` (profile /
58
+ * current-system, excluding this binDir) so upgrades/GC of an old store
59
+ * generation stay reachable; fall back to bun+entry for the rare
60
+ * `nix run ... -- init` case where nothing is on PATH yet (works until that
61
+ * generation is GC'd — docs steer users to `nix profile install` first). */
62
+ function nixSupervisorShim(bun: string, entry: string): string {
63
+ return `#!/bin/sh
64
+ dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
65
+ old_ifs=$IFS
66
+ IFS=:
67
+ new_path=
68
+ for p in $PATH; do
69
+ [ "$p" = "$dir" ] && continue
70
+ if [ -n "$new_path" ]; then new_path="$new_path:$p"; else new_path="$p"; fi
71
+ done
72
+ IFS=$old_ifs
73
+ PATH=$new_path
74
+ export PATH
75
+ if command -v tokenmaxxing >/dev/null 2>&1; then
76
+ exec tokenmaxxing "$@"
77
+ fi
78
+ exec ${JSON.stringify(bun)} run ${JSON.stringify(entry)} "$@"
79
+ `;
80
+ }
81
+
36
82
  export function installSupervisor(): InstallOutcome {
37
83
  mkdirSync(paths.binDir, { recursive: true });
38
84
  const target = installedBin(); // binDir/tokenmaxxing
39
85
  // Resolve the entry through the global-bin symlink (bun add -g links
40
86
  // ~/.bun/bin/tokenmaxxing → the package's src/main.ts) so the shim points
41
- // into the installed package tree, where its imports resolve.
87
+ // into the installed package tree, where its imports resolve. Nix shims
88
+ // prefer PATH first (see nixSupervisorShim).
42
89
  const entry = realpathSync(Bun.main);
43
- writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} run ${JSON.stringify(entry)} "$@"\n`, 0o755);
90
+ if (isNixPackaged()) {
91
+ writeFileAtomic(target, nixSupervisorShim(process.execPath, entry), 0o755);
92
+ } else {
93
+ writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} run ${JSON.stringify(entry)} "$@"\n`, 0o755);
94
+ }
44
95
 
45
96
  // the on-PATH `claude` wrapper
46
97
  writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${JSON.stringify(target)} __supervise "$@"\n`, 0o755);
@@ -174,6 +225,10 @@ function run(cmd: string[]): boolean {
174
225
  * place but activation failed (e.g. systemd user session absent over ssh) -
175
226
  * the caller prints the manual activation step. */
176
227
  function installCheckTimer(): boolean {
228
+ // Nix module owns the timer (TOKENMAXXING_SKIP_TIMER): do not write a second
229
+ // unit that would double-fire or clobber the declarative one.
230
+ if (skipImperativeTimer()) return true;
231
+
177
232
  if (process.platform === "darwin") {
178
233
  const plist = launchdPlist();
179
234
  writeFileAtomic(
@@ -243,6 +298,9 @@ export function timerActivationHint(): string {
243
298
 
244
299
  /** True when the timer unit exists AND the service manager reports it loaded. */
245
300
  export function checkTimerHealthy(): boolean {
301
+ // Declarative Nix timer: init wrote nothing; doctor must not demand the
302
+ // imperative unit.
303
+ if (skipImperativeTimer()) return true;
246
304
  if (process.platform === "darwin") {
247
305
  const domain = launchdDomain();
248
306
  return existsSync(launchdPlist()) && domain != null && run(["launchctl", "print", `${domain}/${LAUNCHD_LABEL}`]);
@@ -305,6 +363,8 @@ function systemdTimerActive(): "active" | "not-active" | "unavailable" {
305
363
  * loaded must deactivate successfully, and an unanswerable probe (service
306
364
  * manager unusable) reports false rather than pretending it is gone. */
307
365
  function uninstallCheckTimer(): boolean {
366
+ // Nix owns the timer: do not bootout/disable the declarative unit.
367
+ if (skipImperativeTimer()) return true;
308
368
  if (process.platform === "darwin") {
309
369
  const domain = launchdDomain();
310
370
  const loaded = launchdJobLoaded();