tokenmaxxing 1.8.0 → 1.9.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.
Files changed (85) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +0 -40
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +1 -33
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -73
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -34
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +0 -28
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +6 -127
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -48
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -40
  67. package/src/sdk.ts +0 -59
  68. package/agent-plugin/agents/tokenmaxxing-claude.md +0 -43
  69. package/agent-plugin/agents/tokenmaxxing-codex.md +0 -40
  70. package/agent-plugin/hooks/cursor-relay.json +0 -14
  71. package/agent-plugin/skills/relay-session/SKILL.md +0 -118
  72. package/agent-plugin/skills/relay-session/references/ipc.md +0 -23
  73. package/src/cli/relay.ts +0 -323
  74. package/src/entries/relaypermission.ts +0 -105
  75. package/src/lib/relay/config.ts +0 -84
  76. package/src/lib/relay/decide.ts +0 -75
  77. package/src/lib/relay/gc.ts +0 -80
  78. package/src/lib/relay/install.ts +0 -143
  79. package/src/lib/relay/markers.ts +0 -148
  80. package/src/lib/relay/modes.ts +0 -82
  81. package/src/lib/relay/protocol.ts +0 -61
  82. package/src/lib/relay/registry.ts +0 -175
  83. package/src/lib/relay/tmux.ts +0 -109
  84. package/src/lib/relay/turn.ts +0 -137
  85. package/src/lib/relay/worker.ts +0 -141
@@ -1,7 +1,3 @@
1
- // Install/uninstall the on-PATH `claude` supervisor wrapper + settings entries.
2
- // The wrapper is a 2-line `exec ... __supervise "$@"` shim so dispatch never
3
- // depends on argv0 semantics.
4
-
5
1
  import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
6
2
  import { basename, dirname, join } from "node:path";
7
3
  import { escape } from "es-toolkit";
@@ -19,7 +15,6 @@ const InstallOutcomeSchema = z.object({
19
15
  });
20
16
  export type InstallOutcome = z.infer<typeof InstallOutcomeSchema>;
21
17
 
22
- /** True if our binDir comes before the real claude's dir on PATH. */
23
18
  export function isBinDirAhead(): boolean {
24
19
  const dirs = (process.env.PATH ?? "").split(":");
25
20
  const ourIdx = dirs.indexOf(paths.binDir);
@@ -33,12 +28,8 @@ export function isBinDirAhead(): boolean {
33
28
  }
34
29
  }
35
30
 
36
- // Optional "1"/"true"/"yes" flag; unset/empty → undefined (feature off).
37
31
  const EnvFlagSchema = z.enum(["1", "true", "yes"]).optional().catch(undefined);
38
32
 
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
33
  export function isNixPackaged(): boolean {
43
34
  if (EnvFlagSchema.parse(process.env.TOKENMAXXING_NIX) != null) return true;
44
35
  try {
@@ -48,8 +39,6 @@ export function isNixPackaged(): boolean {
48
39
  }
49
40
  }
50
41
 
51
- /** True when a Nix module owns the periodic check timer; init must not write
52
- * a second imperative unit. */
53
42
  export function skipImperativeTimer(): boolean {
54
43
  return EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_TIMER) != null;
55
44
  }
@@ -62,9 +51,6 @@ function isEacces(e: unknown): boolean {
62
51
  return typeof e === "object" && e != null && "code" in e && e.code === "EACCES";
63
52
  }
64
53
 
65
- /** Home Manager (and similar) point ~/.zshrc at a nix-store file. Writing
66
- * through that symlink throws EACCES; soft-skip instead. Still write through
67
- * ordinary writable symlink targets (PR #36). */
68
54
  function cannotWriteRcTarget(target: string): boolean {
69
55
  if (EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_SHELL_RC) != null) return true;
70
56
  if (isNixStorePath(target)) return true;
@@ -77,7 +63,6 @@ function cannotWriteRcTarget(target: string): boolean {
77
63
  }
78
64
  }
79
65
 
80
- /** User-facing lines when ensurePathInRc soft-skips a managed shell rc. */
81
66
  export function managedShellRcSkipLines(): { headline: string; detail: string; exportLine: string } {
82
67
  return {
83
68
  headline: "shell rc is managed (Home Manager / nix-store) - PATH was not auto-edited",
@@ -86,11 +71,6 @@ export function managedShellRcSkipLines(): { headline: string; detail: string; e
86
71
  };
87
72
  }
88
73
 
89
- /** Nix supervisor shim: prefer a PATH-stable `tokenmaxxing` (profile /
90
- * current-system, excluding this binDir) so upgrades/GC of an old store
91
- * generation stay reachable; fall back to bun+entry for the rare
92
- * `nix run ... -- init` case where nothing is on PATH yet (works until that
93
- * generation is GC'd — docs steer users to `nix profile install` first). */
94
74
  function nixSupervisorShim(bun: string, entry: string): string {
95
75
  return `#!/bin/sh
96
76
  dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
@@ -113,11 +93,7 @@ exec ${JSON.stringify(bun)} run ${JSON.stringify(entry)} "$@"
113
93
 
114
94
  export function installSupervisor(): InstallOutcome {
115
95
  mkdirSync(paths.binDir, { recursive: true });
116
- const target = installedBin(); // binDir/tokenmaxxing
117
- // Resolve the entry through the global-bin symlink (bun add -g links
118
- // ~/.bun/bin/tokenmaxxing → the package's src/main.ts) so the shim points
119
- // into the installed package tree, where its imports resolve. Nix shims
120
- // prefer PATH first (see nixSupervisorShim).
96
+ const target = installedBin();
121
97
  const entry = realpathSync(Bun.main);
122
98
  if (isNixPackaged()) {
123
99
  writeFileAtomic(target, nixSupervisorShim(process.execPath, entry), 0o755);
@@ -125,9 +101,7 @@ export function installSupervisor(): InstallOutcome {
125
101
  writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} run ${JSON.stringify(entry)} "$@"\n`, 0o755);
126
102
  }
127
103
 
128
- // the on-PATH `claude` wrapper
129
104
  writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${JSON.stringify(target)} __supervise "$@"\n`, 0o755);
130
- // the `xx` short alias → tokenmaxxing
131
105
  writeFileAtomic(join(paths.binDir, "xx"), `#!/bin/sh\nexec ${JSON.stringify(target)} "$@"\n`, 0o755);
132
106
 
133
107
  installSettings();
@@ -139,14 +113,6 @@ export function installSupervisor(): InstallOutcome {
139
113
  };
140
114
  }
141
115
 
142
- // ---- codex supervisor + Stop hook -------------------------------------------
143
-
144
- /** Codex hook declarations we merge into. The FILE nests the event map under a
145
- * `hooks` field (`struct HooksFile { description?, hooks }`, binary-verified
146
- * 0.144.4 after a live parse failure proved the docs' claude-style top-level
147
- * event map wrong: "unknown field Stop, expected description or hooks").
148
- * Loose everywhere: every other event and every foreign Stop entry rides
149
- * along verbatim. */
150
116
  const CodexHookEventsSchema = z.looseObject({
151
117
  Stop: z.array(z.looseObject({ hooks: z.array(z.looseObject({ command: z.string().optional() })).default([]) })).default([]),
152
118
  });
@@ -158,20 +124,9 @@ const CodexHooksFileSchema = z.looseObject({
158
124
  const CODEX_STOP_HOOK_SUBCOMMAND = "__codex-stop-hook";
159
125
 
160
126
  function codexStopHookCommand(): string {
161
- // Quoted like the claude shim commands: an install path with a space would
162
- // otherwise mis-split and the hook would silently never run.
163
127
  return `${JSON.stringify(installedBin())} ${CODEX_STOP_HOOK_SUBCOMMAND}`;
164
128
  }
165
129
 
166
- /** Idempotently install the tokenmaxxing Stop entry in ~/.codex/hooks.json,
167
- * preserving every other declaration. Codex skips new hooks until the user
168
- * trusts them via /hooks (trust is recorded against the hook's hash), so the
169
- * caller must surface that step. */
170
- /** Surgical WITHIN groups, ownership verified structurally (closing-review
171
- * catch, mirroring settings.ts's removeHook fix): the old whole-group filter
172
- * deleted a foreign hook the user had appended into our group - the natural
173
- * edit, since install writes exactly one group - and its includes() match
174
- * claimed any command merely mentioning the subcommand. */
175
130
  function withoutOurCodexStopHooks(groups: { hooks: { type?: string; command?: string }[] }[]): typeof groups {
176
131
  return groups
177
132
  .map((group) => ({ ...group, hooks: group.hooks.filter((hook) => !isOurHookCommand(hook.command ?? "", CODEX_STOP_HOOK_SUBCOMMAND)) }))
@@ -213,7 +168,6 @@ export function codexSupervisorLink(): string {
213
168
  return join(paths.binDir, "codex");
214
169
  }
215
170
 
216
- /** The on-PATH `codex` wrapper + the Stop hook declaration. */
217
171
  export function installCodexSupervisor(): void {
218
172
  mkdirSync(paths.binDir, { recursive: true });
219
173
  writeFileAtomic(codexSupervisorLink(), `#!/bin/sh\nexec ${JSON.stringify(installedBin())} __supervise-codex "$@"\n`, 0o755);
@@ -225,26 +179,18 @@ export function uninstallCodexSupervisor(): void {
225
179
  if (existsSync(codexSupervisorLink())) rmSync(codexSupervisorLink(), { force: true });
226
180
  }
227
181
 
228
- // ---- periodic `check` timer ------------------------------------------------
229
- // The hooks evaluate only at turn boundaries; one long agentic turn can burn a
230
- // window from healthy to depleted with zero boundaries (2026-07-10 incident).
231
- // A timer closes that gap: launchd on macOS, a systemd user timer on Linux.
232
-
233
- const CHECK_INTERVAL_S = 180;
182
+ const CHECK_INTERVAL_S = 60;
234
183
  const LAUNCHD_LABEL = "com.tokenmaxxing.check";
235
184
 
236
185
  function launchdPlist(): string {
237
186
  return join(paths.launchdAgentsDir, `${LAUNCHD_LABEL}.plist`);
238
187
  }
239
188
 
240
- /** `gui/<uid>` launchd domain, or null when the platform has no getuid. */
241
189
  function launchdDomain(): string | null {
242
190
  const uid = process.getuid?.();
243
191
  return uid == null ? null : `gui/${uid}`;
244
192
  }
245
193
 
246
- /** launchctl/systemctl may be absent (spawnSync throws ENOENT) or hang on a
247
- * dead session bus (ssh without lingering) - degrade, never crash or block. */
248
194
  function run(cmd: string[]): boolean {
249
195
  try {
250
196
  return Bun.spawnSync(cmd, { stdout: "ignore", stderr: "ignore", timeout: 10_000 }).exitCode === 0;
@@ -253,12 +199,7 @@ function run(cmd: string[]): boolean {
253
199
  }
254
200
  }
255
201
 
256
- /** Install + activate the periodic check job. False means the unit files are in
257
- * place but activation failed (e.g. systemd user session absent over ssh) -
258
- * the caller prints the manual activation step. */
259
202
  function installCheckTimer(): boolean {
260
- // Nix module owns the timer (TOKENMAXXING_SKIP_TIMER): do not write a second
261
- // unit that would double-fire or clobber the declarative one.
262
203
  if (skipImperativeTimer()) return true;
263
204
 
264
205
  if (process.platform === "darwin") {
@@ -270,7 +211,7 @@ function installCheckTimer(): boolean {
270
211
  <plist version="1.0">
271
212
  <dict>
272
213
  <key>Label</key><string>${LAUNCHD_LABEL}</string>
273
- <key>ProgramArguments</key><array><string>${escape(installedBin())}</string><string>check</string></array>
214
+ <key>ProgramArguments</key><array><string>${escape(installedBin())}</string><string>check</string><string>--if-due</string></array>
274
215
  <key>StartInterval</key><integer>${CHECK_INTERVAL_S}</integer>
275
216
  <key>StandardOutPath</key><string>/dev/null</string>
276
217
  <key>StandardErrorPath</key><string>${escape(join(paths.home, "check.stderr.log"))}</string>
@@ -281,13 +222,11 @@ function installCheckTimer(): boolean {
281
222
  );
282
223
  const domain = launchdDomain();
283
224
  if (domain == null) return false;
284
- run(["launchctl", "bootout", `${domain}/${LAUNCHD_LABEL}`]); // reload a changed plist
285
- // bootstrap can lose a benign race with an in-flight bootout; loaded is loaded.
225
+ run(["launchctl", "bootout", `${domain}/${LAUNCHD_LABEL}`]);
286
226
  return run(["launchctl", "bootstrap", domain, plist]) || checkTimerHealthy();
287
227
  }
288
228
 
289
- // systemd: quote the path and escape `%` (unit-file specifier character).
290
- const exec = `"${installedBin().replaceAll("%", "%%")}" check`;
229
+ const exec = `"${installedBin().replaceAll("%", "%%")}" check --if-due`;
291
230
  writeFileAtomic(
292
231
  join(paths.systemdUserDir, "tokenmaxxing-check.service"),
293
232
  `[Unit]
@@ -307,7 +246,7 @@ Description=tokenmaxxing periodic account-switch check
307
246
  [Timer]
308
247
  OnBootSec=60
309
248
  OnUnitActiveSec=${CHECK_INTERVAL_S}
310
- AccuracySec=30
249
+ AccuracySec=5
311
250
 
312
251
  [Install]
313
252
  WantedBy=timers.target
@@ -320,7 +259,6 @@ WantedBy=timers.target
320
259
  );
321
260
  }
322
261
 
323
- /** The manual activation command for an unloaded timer, per platform. */
324
262
  export function timerActivationHint(): string {
325
263
  if (process.platform === "darwin") {
326
264
  return `launchctl bootstrap gui/$(id -u) ${launchdPlist()}`;
@@ -328,10 +266,7 @@ export function timerActivationHint(): string {
328
266
  return "systemctl --user daemon-reload && systemctl --user enable --now tokenmaxxing-check.timer";
329
267
  }
330
268
 
331
- /** True when the timer unit exists AND the service manager reports it loaded. */
332
269
  export function checkTimerHealthy(): boolean {
333
- // Declarative Nix timer: init wrote nothing; doctor must not demand the
334
- // imperative unit.
335
270
  if (skipImperativeTimer()) return true;
336
271
  if (process.platform === "darwin") {
337
272
  const domain = launchdDomain();
@@ -343,7 +278,6 @@ export function checkTimerHealthy(): boolean {
343
278
  );
344
279
  }
345
280
 
346
- /** The manual deactivation command for a still-loaded timer, per platform. */
347
281
  export function timerDeactivationHint(): string {
348
282
  if (process.platform === "darwin") {
349
283
  return `launchctl bootout gui/$(id -u)/${LAUNCHD_LABEL}`;
@@ -351,10 +285,6 @@ export function timerDeactivationHint(): string {
351
285
  return "systemctl --user disable --now tokenmaxxing-check.timer";
352
286
  }
353
287
 
354
- /** Is the launchd check job loaded? Exit contract verified on this machine
355
- * (macOS 26, 2026-07-20): `launchctl print` exits 0 for a loaded job and 113
356
- * for a missing one. Anything else - including a spawn failure or timeout -
357
- * is "unavailable": an unanswerable probe must never read as "not loaded". */
358
288
  function launchdJobLoaded(): "loaded" | "not-loaded" | "unavailable" {
359
289
  const domain = launchdDomain();
360
290
  if (domain == null) return "unavailable";
@@ -367,15 +297,6 @@ function launchdJobLoaded(): "loaded" | "not-loaded" | "unavailable" {
367
297
  }
368
298
  }
369
299
 
370
- /** Is the systemd check timer active? Classification goes by the state
371
- * string, never the exit code: systemctl(1) documents only "0 if at least
372
- * one is active, non-zero otherwise" for is-active, but guarantees "unless
373
- * --quiet is specified, this will also print the current unit state to
374
- * standard output" (verified against the systemd manpage 2026-07-20). The
375
- * failure mode is container-verified (ubuntu:24.04 systemd, no user bus,
376
- * 2026-07-20): a dead session bus prints NOTHING to stdout ("Failed to
377
- * connect to bus" goes to stderr, exit 1), so an empty or unrecognized
378
- * stdout is "unavailable" - "cannot ask" never reads as "inactive". */
379
300
  function systemdTimerActive(): "active" | "not-active" | "unavailable" {
380
301
  try {
381
302
  const proc = Bun.spawnSync(["systemctl", "--user", "is-active", "tokenmaxxing-check.timer"], { stdout: "pipe", stderr: "ignore", timeout: 10_000 });
@@ -388,14 +309,7 @@ function systemdTimerActive(): "active" | "not-active" | "unavailable" {
388
309
  }
389
310
  }
390
311
 
391
- /** True when the job is verifiably no longer loaded. A swallowed bootout
392
- * failure once meant a half-uninstalled state kept firing `tokenmaxxing
393
- * check` every 180s against a package that may be gone, silently
394
- * (closing-review catch): deactivation is checked, not assumed - a job seen
395
- * loaded must deactivate successfully, and an unanswerable probe (service
396
- * manager unusable) reports false rather than pretending it is gone. */
397
312
  function uninstallCheckTimer(): boolean {
398
- // Nix owns the timer: do not bootout/disable the declarative unit.
399
313
  if (skipImperativeTimer()) return true;
400
314
  if (process.platform === "darwin") {
401
315
  const domain = launchdDomain();
@@ -412,8 +326,6 @@ function uninstallCheckTimer(): boolean {
412
326
  return deactivated;
413
327
  }
414
328
 
415
- /** The rc file of the user's login shell, or null when the shell is unknown.
416
- * Overridable for hermetic tests. */
417
329
  export function shellRcPath(): string | null {
418
330
  const override = process.env.TOKENMAXXING_SHELL_RC;
419
331
  if (override && override.length > 0) return override;
@@ -425,35 +337,18 @@ export function shellRcPath(): string | null {
425
337
 
426
338
  const PATH_LINE_MARK = "# tokenmaxxing PATH";
427
339
 
428
- /** Idempotently append the supervisor-bin PATH line to `rc` (created if absent).
429
- * A pre-existing hand-added line for the bin dir also counts as present.
430
- * Returns `"skipped"` when the resolved target is immutable (nix-store /
431
- * non-writable / TOKENMAXXING_SKIP_SHELL_RC) so callers can print guidance
432
- * instead of surfacing EACCES. */
433
340
  export function ensurePathInRc(rc: string): "added" | "present" | "skipped" {
434
341
  const dir = paths.binDir.startsWith(`${HOME}/`) ? `$HOME${paths.binDir.slice(HOME.length)}` : paths.binDir;
435
- // Write through a dotfile-managed symlink, never over it: writeFileAtomic
436
- // renames a sibling temp over its target, which would replace the link with
437
- // a plain file while the dotfiles target keeps the stale line (PR #36
438
- // second-round catch).
439
342
  const target = existsSync(rc) ? realpathSync(rc) : rc;
440
343
  const current = existsSync(target) ? readFileSync(target, "utf8") : "";
441
344
  const isCurrentExport = (line: string) => line.includes(`${paths.binDir}:`) || line.includes(`${dir}:`);
442
345
  const lines = current === "" ? [] : current.split("\n");
443
- // A marked line for a DIFFERENT dir is removed even when the current dir is
444
- // also exported: PATH prepends stack, so a stale marked line BELOW the
445
- // current one would still win resolution - the recursion incident's exact
446
- // vector (closing-review catch + PR #36 second-round catch). A bare marker
447
- // check alone once kept such a line alive after a TOKENMAXXING_HOME
448
- // relocation.
449
346
  const kept = lines.filter((line) => isCurrentExport(line) || !line.includes(PATH_LINE_MARK));
450
347
  if (kept.length !== lines.length) {
451
348
  if (cannotWriteRcTarget(target)) return "skipped";
452
349
  const body = kept.join("\n");
453
350
  const sep0 = body === "" || body.endsWith("\n") ? "" : "\n";
454
351
  const addition = kept.some(isCurrentExport) ? "" : `export PATH="${dir}:$PATH" ${PATH_LINE_MARK}\n`;
455
- // preserve the rc's own mode: writeFileAtomic defaults to 0600, which
456
- // would silently tighten a normally 0644 shell rc (PR #36 review catch)
457
352
  try {
458
353
  writeFileAtomic(target, `${body}${sep0}${addition}`, statSync(target).mode & 0o777);
459
354
  } catch (e) {
@@ -475,19 +370,12 @@ export function ensurePathInRc(rc: string): "added" | "present" | "skipped" {
475
370
  }
476
371
 
477
372
  const ShellShadowerSchema = z.object({
478
- /** shadow: a `claude` alias/function hides the wrapper entirely.
479
- * bypass: another alias (e.g. `cc`, `cco`) hardcodes an absolute path to a
480
- * claude binary, so launches through it skip supervision. */
481
373
  kind: z.enum(["shadow", "bypass"]),
482
374
  name: z.string(),
483
375
  line: z.string(),
484
376
  });
485
377
  export type ShellShadower = z.infer<typeof ShellShadowerSchema>;
486
378
 
487
- /** Scan shell-rc text for aliases/functions that shadow `claude` or hardcode a
488
- * path to a claude binary. Aliases whose body starts with plain `claude` are
489
- * fine (they expand through PATH into the wrapper); an absolute path is not.
490
- * Lines referencing the wrapper itself are deliberate and skipped. */
491
379
  export function findClaudeShadowers(rcText: string): ShellShadower[] {
492
380
  const out: ShellShadower[] = [];
493
381
  const absClaude = /(?:^|[\s"'=])(\/[^\s"']*\/claude)(?:[\s"']|$)/;
@@ -510,17 +398,8 @@ export function findClaudeShadowers(rcText: string): ShellShadower[] {
510
398
  return out;
511
399
  }
512
400
 
513
- /** Remove ONLY the marker-tagged PATH line this tool added; a hand-added
514
- * PATH entry without the marker is the user's own. A stale `# tokenmaxxing
515
- * PATH` line pointing at an emptied binDir is exactly how the supervisor
516
- * recursion incident started (.memory/supervisor-recursion-guards.md), so
517
- * uninstall must not leave one behind (closing-review catch).
518
- * Returns true when a line was removed. Soft-skips (returns false, no throw)
519
- * when the resolved target is immutable. */
520
401
  export function removePathFromRc(rc: string): boolean {
521
402
  if (!existsSync(rc)) return false;
522
- // same symlink + mode treatment as ensurePathInRc: write through a
523
- // dotfile-managed link, keep the rc's own permissions (PR #36 catches)
524
403
  const target = realpathSync(rc);
525
404
  const lines = readFileSync(target, "utf8").split("\n");
526
405
  const kept = lines.filter((line) => !line.includes(PATH_LINE_MARK));
@@ -1,42 +1,19 @@
1
- // macOS login-keychain generic-password I/O, ps-safe. The darwin backend of
2
- // credstore.ts - construct targets there, not here.
3
- // READ : `security find-generic-password -w` → secret only ever in stdout.
4
- // WRITE : pipe an `add-generic-password -U ... -w <secret>` line into `security -i`
5
- // over STDIN - the secret never appears in any process's argv (verified).
6
- // The service/account are non-secret and may sit in argv.
7
-
8
1
  import { z } from "zod";
9
2
 
10
3
  const KeychainTargetSchema = z.object({ service: z.string(), account: z.string() });
11
4
  export type KeychainTarget = z.infer<typeof KeychainTargetSchema>;
12
5
 
13
6
  const SECURITY = "/usr/bin/security";
14
- // security(1) interactive mode has a 4096-byte line buffer. The gate below
15
- // measures the ASSEMBLED line against this (with margin), never the raw
16
- // secret: quoteDouble expansion (one byte per `"`/`\` when the blob contains
17
- // an apostrophe) once pushed a raw-length-passing line over the buffer, which
18
- // SPLITS the line - the write exits 1 ("unknown command" for the spilled
19
- // remainder) AND the item is left holding a TRUNCATED secret (empirically
20
- // verified 2026-07-20: a 3667-byte secret assembling to a 5574-byte line
21
- // corrupted the item to its first 2682 bytes before the error surfaced).
22
7
  const INTERACTIVE_MAX_LINE = 4000;
23
8
 
24
- /** Double-quote + backslash-escape for security(1)'s interactive tokenizer. */
25
9
  function quoteDouble(s: string): string {
26
10
  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
27
11
  }
28
12
 
29
- /** Wrap a value for the `-i` line. Single-quote when possible (blob has no `'`). */
30
13
  function quoteValue(s: string): string {
31
14
  return s.includes("'") ? quoteDouble(s) : `'${s}'`;
32
15
  }
33
16
 
34
- /** Read an item's password blob. Returns null ONLY when the item verifiably
35
- * does not exist (exit 44, errSecItemNotFound - empirically pinned on this
36
- * Mac 2026-07-20). Every other failure THROWS: a locked keychain or denied
37
- * ACL prompt reading as "absent" silently disarmed every fail-closed
38
- * live-owner guard and the mandatory pre-swap harvest, all of which key off
39
- * a null read (closing-review catch). stderr never carries the secret. */
40
17
  export async function readItem(t: KeychainTarget): Promise<string | null> {
41
18
  const p = Bun.spawn([SECURITY, "find-generic-password", "-s", t.service, "-a", t.account, "-w"], {
42
19
  stdout: "pipe",
@@ -48,11 +25,9 @@ export async function readItem(t: KeychainTarget): Promise<string | null> {
48
25
  if (p.exitCode !== 0) {
49
26
  throw new Error(`keychain read failed (exit ${p.exitCode}): ${err.trim().slice(0, 200)} - a locked keychain or denied ACL must fail loudly, never read as absent`);
50
27
  }
51
- return out.replace(/\n$/, ""); // security appends exactly one trailing newline
28
+ return out.replace(/\n$/, "");
52
29
  }
53
30
 
54
- /** The full `security -i` line for a write; its LENGTH is the argv-fallback
55
- * gate, so it is assembled once, here. */
56
31
  function interactiveLine(t: KeychainTarget, secret: string): string {
57
32
  return (
58
33
  `add-generic-password -U -a ${quoteDouble(t.account)} -s ${quoteDouble(t.service)} ` +
@@ -60,8 +35,6 @@ function interactiveLine(t: KeychainTarget, secret: string): string {
60
35
  );
61
36
  }
62
37
 
63
- /** ps-safe write: the command line + secret arrive on stdin, never in argv.
64
- * The caller guarantees the encoded line fits the interactive buffer. */
65
38
  async function writeViaInteractive(encodedLine: Uint8Array): Promise<void> {
66
39
  const p = Bun.spawn([SECURITY, "-i"], {
67
40
  stdin: encodedLine,
@@ -73,9 +46,6 @@ async function writeViaInteractive(encodedLine: Uint8Array): Promise<void> {
73
46
  if (p.exitCode !== 0) throw new Error(`keychain write (interactive) failed (exit ${p.exitCode}): ${err.trim()}`);
74
47
  }
75
48
 
76
- /** Fallback for blobs over the interactive line limit. The secret is briefly
77
- * visible in `ps` for this one short-lived process - used only when the payload
78
- * (e.g. a live blob with lots of MCP OAuth state) exceeds ~4KB. */
79
49
  async function writeViaArgv(t: KeychainTarget, secret: string): Promise<void> {
80
50
  const p = Bun.spawn([SECURITY, "add-generic-password", "-U", "-a", t.account, "-s", t.service, "-w", secret], {
81
51
  stdout: "ignore",
@@ -86,20 +56,12 @@ async function writeViaArgv(t: KeychainTarget, secret: string): Promise<void> {
86
56
  if (p.exitCode !== 0) throw new Error(`keychain write (argv) failed (exit ${p.exitCode}): ${err.trim()}`);
87
57
  }
88
58
 
89
- /** Create-or-update an item (`-U`) with `secret` as its password. Prefers the
90
- * ps-safe stdin path; falls back to argv when the ASSEMBLED interactive line
91
- * would exceed the buffer (see INTERACTIVE_MAX_LINE - gating on the raw
92
- * secret length let quote expansion corrupt the item). Throws on failure. */
93
59
  export async function writeItem(t: KeychainTarget, secret: string): Promise<void> {
94
- // measured in UTF-8 BYTES, the unit the stdin buffer actually consumes:
95
- // String.length counts UTF-16 code units and under-counts multibyte
96
- // characters (cubic review catch, PR #35).
97
60
  const encoded = new TextEncoder().encode(interactiveLine(t, secret));
98
61
  if (encoded.length <= INTERACTIVE_MAX_LINE) return writeViaInteractive(encoded);
99
62
  return writeViaArgv(t, secret);
100
63
  }
101
64
 
102
- /** Delete an item. Returns true if it existed and was removed. */
103
65
  export async function deleteItem(t: KeychainTarget): Promise<boolean> {
104
66
  const p = Bun.spawn([SECURITY, "delete-generic-password", "-s", t.service, "-a", t.account], {
105
67
  stdout: "ignore",
package/src/lib/lock.ts CHANGED
@@ -1,34 +1,17 @@
1
- // Cross-process advisory locking via flock(2) through bun:ffi on a real file
2
- // descriptor (macOS ships no flock(1) binary, and one codepath serves both
3
- // platforms). The lock is released when we close the fd (explicitly or on
4
- // process exit).
5
- //
6
- // The acquire is NON-BLOCKING (LOCK_EX|LOCK_NB) with an async retry loop: a
7
- // blocking LOCK_EX from this runtime freezes the whole event loop.
8
- // EWOULDBLOCK is told apart from real failures via errno, so a bad fd still
9
- // fails fast instead of spinning.
10
-
11
1
  import { closeSync, mkdirSync, openSync } from "node:fs";
12
2
  import { dirname } from "node:path";
13
3
  import { dlopen, FFIType, read } from "bun:ffi";
14
4
  import { delay } from "es-toolkit";
15
5
 
16
- // sys/file.h (identical on darwin + linux): LOCK_SH=1 LOCK_EX=2 LOCK_NB=4 LOCK_UN=8
17
6
  const LOCK_EX = 2;
18
7
  const LOCK_NB = 4;
19
8
  const LOCK_UN = 8;
20
- // errno.h: EWOULDBLOCK/EAGAIN is 35 on darwin, 11 on linux; EINTR is 4 on both.
21
9
  const EAGAIN = process.platform === "darwin" ? 35 : 11;
22
10
  const EINTR = 4;
23
11
  const RETRY_MS = 75;
24
12
 
25
13
  const FLOCK_DEF = { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } } as const;
26
14
 
27
- // darwin: flock(2) + __error live in libSystem; the bare name resolves via the
28
- // dyld shared cache even though no physical .dylib exists on disk (verified
29
- // 2026-07-08). linux: glibc's libc.so.6 + __errno_location (verified
30
- // in-container 2026-07-09, arm64). Exactly one verified path per platform -
31
- // a broken environment fails fast instead of falling through a guess list.
32
15
  function loadLibc() {
33
16
  if (process.platform === "darwin") {
34
17
  const lib = dlopen("libSystem.B.dylib", { ...FLOCK_DEF, __error: { args: [], returns: FFIType.ptr } });
@@ -50,12 +33,6 @@ function currentErrno(): number {
50
33
  return p == null ? -1 : read.i32(p, 0);
51
34
  }
52
35
 
53
- /**
54
- * Acquire an exclusive advisory lock on `lockPath`, waiting (without blocking
55
- * the event loop) until available. Returns a handle whose release() drops the
56
- * lock. The lock is fd-scoped, so racing hooks in separate processes serialize
57
- * and same-process actors queue on the retry loop.
58
- */
59
36
  export async function acquireLock(lockPath: string): Promise<{ release: () => void }> {
60
37
  mkdirSync(dirname(lockPath), { recursive: true });
61
38
  const fd = openSync(lockPath, "a", 0o600);
@@ -85,7 +62,6 @@ export async function acquireLock(lockPath: string): Promise<{ release: () => vo
85
62
  return { release };
86
63
  }
87
64
 
88
- /** Run `fn` while holding `lockPath`; always releases, even on throw. */
89
65
  export async function withLock<T>(lockPath: string, fn: () => Promise<T> | T): Promise<T> {
90
66
  const held = await acquireLock(lockPath);
91
67
  try {
package/src/lib/log.ts CHANGED
@@ -1,6 +1,3 @@
1
- // Append-only logging. NEVER logs secret material - callers must pass only
2
- // non-secret context (account uuids/emails, percentages, status strings).
3
-
4
1
  import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
5
2
  import { dirname } from "node:path";
6
3
  import { z } from "zod";
@@ -8,19 +5,14 @@ import { paths } from "./paths.ts";
8
5
 
9
6
  const LOG_MAX_BYTES = 5_000_000;
10
7
 
11
- /** Redact anything that looks like a token so an accidental pass-through can't leak. */
12
8
  function redact(s: string): string {
13
9
  return s
14
- // JWT-ish / long opaque tokens
15
10
  .replace(/\b(sk-ant-[A-Za-z0-9._-]{6,})/g, "sk-ant-***")
16
11
  .replace(/\b([A-Za-z0-9_-]{40,})\b/g, (m) => `${m.slice(0, 4)}...(${m.length})`);
17
12
  }
18
13
 
19
14
  let echo: ((input: { event: string; parts: string }) => void) | null = null;
20
15
 
21
- /** Tee every subsequent log() line to a terminal printer. Off by default
22
- * (hooks and the statusline own their stdout protocol). The printer receives
23
- * the same redacted parts the file line gets. */
24
16
  export function setLogEcho(input: { printer: (input: { event: string; parts: string }) => void }): void {
25
17
  echo = input.printer;
26
18
  }
@@ -35,20 +27,14 @@ export function log(event: string, fields: Record<string, unknown> = {}): void {
35
27
  })
36
28
  .join(" ");
37
29
  mkdirSync(dirname(paths.logFile), { recursive: true });
38
- // Rotation cap: the check timer logs every 180s, so an uncapped
39
- // append-only file grows forever on a live install. One .old generation
40
- // bounds total disk at ~2x the cap; older history is disposable diagnostics.
41
30
  if (existsSync(paths.logFile) && statSync(paths.logFile).size > LOG_MAX_BYTES) {
42
31
  renameSync(paths.logFile, `${paths.logFile}.old`);
43
32
  }
44
33
  appendFileSync(paths.logFile, `${new Date().toISOString()} ${event} ${line}\n`);
45
34
  } catch {
46
- // logging must never throw into a hook / supervisor path
47
35
  }
48
- // separate from the file sink: an unwritable log file must not also silence the echo.
49
36
  try {
50
37
  echo?.({ event, parts: line });
51
38
  } catch {
52
- // the echo printer must never throw into a caller path
53
39
  }
54
40
  }
package/src/lib/oauth.ts CHANGED
@@ -1,19 +1,7 @@
1
- // The two OAuth calls tokenmaxxing makes:
2
- // 1. the refresh_token grant that turns a parked account's (possibly expired)
3
- // access token into a fresh one before we install it. Verified against
4
- // Claude Code 2.1.204: POST platform.claude.com, JSON body, public PKCE
5
- // client (no secret), no auth/beta headers on this call.
6
- // 2. the roles lookup that reports which org a token ACTUALLY belongs to.
7
- // Endpoint string extracted from the Claude Code 2.1.205 binary; verified
8
- // live to answer HTTP 200 with plain Bearer auth (with or without the
9
- // oauth beta header - we send it to match the CLI's OAuth convention).
10
-
11
1
  import { z } from "zod";
12
2
  import { http, safeErrorDetail } from "./http.ts";
13
3
  import { RefreshResponseSchema, RolesResponseSchema, type OAuthCreds, type RolesResponse } from "./types.ts";
14
4
 
15
- // zod-parsed like every other env override (repo rule): a set-but-EMPTY value
16
- // parses to undefined and the default applies, instead of posting to "".
17
5
  const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
18
6
  const TOKEN_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_OAUTH_TOKEN_URL) ?? "https://platform.claude.com/v1/oauth/token";
19
7
  const CLIENT_ID = EnvOverrideSchema.parse(process.env.TOKENMAXXING_OAUTH_CLIENT_ID) ?? "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
@@ -27,7 +15,6 @@ const DEFAULT_SCOPES = [
27
15
  "user:file_upload",
28
16
  ];
29
17
 
30
- /** Refresh token is dead / revoked - caller should mark needs_reauth and skip. */
31
18
  export class InvalidGrantError extends Error {
32
19
  constructor(public readonly detail: string) {
33
20
  super(`invalid_grant: ${detail}`);
@@ -35,12 +22,6 @@ export class InvalidGrantError extends Error {
35
22
  }
36
23
  }
37
24
 
38
- /**
39
- * Exchange `creds.refreshToken` for a fresh access token. Returns a NEW OAuthCreds
40
- * with the fresh access token, the rotated refresh token (or the old one if the
41
- * server omitted it), and recomputed absolute expiries. Non-token fields are
42
- * preserved. Throws InvalidGrantError on a dead refresh token.
43
- */
44
25
  export async function refreshCredential(creds: OAuthCreds, now = Date.now()): Promise<OAuthCreds> {
45
26
  const scope = (creds.scopes?.length ? creds.scopes : DEFAULT_SCOPES).join(" ");
46
27
  const body = {
@@ -62,9 +43,6 @@ export async function refreshCredential(creds: OAuthCreds, now = Date.now()): Pr
62
43
 
63
44
  const text = await res.text();
64
45
  if (!res.ok) {
65
- // invalid_grant → dead refresh token; anything else is transient/unknown.
66
- // Bodies go through the safeErrorDetail allowlist, never raw: a token
67
- // endpoint's failure body can echo request material.
68
46
  if (res.status === 400 && /invalid_grant/.test(text)) {
69
47
  throw new InvalidGrantError(safeErrorDetail({ text }));
70
48
  }
@@ -75,7 +53,6 @@ export async function refreshCredential(creds: OAuthCreds, now = Date.now()): Pr
75
53
  try { return JSON.parse(text); } catch { return null; }
76
54
  })());
77
55
  if (!parsed.success) {
78
- // A success-status body holds live tokens: never echo any of it.
79
56
  throw new Error(`token endpoint returned an unrecognized body (${text.length} bytes, withheld)`);
80
57
  }
81
58
  const json = parsed.data;
@@ -86,7 +63,7 @@ export async function refreshCredential(creds: OAuthCreds, now = Date.now()): Pr
86
63
  return {
87
64
  ...creds,
88
65
  accessToken: json.access_token,
89
- refreshToken: json.refresh_token ?? creds.refreshToken, // server may omit → reuse
66
+ refreshToken: json.refresh_token ?? creds.refreshToken,
90
67
  expiresAt: now + expiresIn * 1000,
91
68
  refreshTokenExpiresAt:
92
69
  refreshExpiresIn != null ? now + refreshExpiresIn * 1000 : creds.refreshTokenExpiresAt,
@@ -94,17 +71,10 @@ export async function refreshCredential(creds: OAuthCreds, now = Date.now()): Pr
94
71
  };
95
72
  }
96
73
 
97
- /** True if the access token is within `skewMs` of expiry (or already expired). */
98
74
  export function isAccessTokenExpiring(creds: OAuthCreds, skewMs = 120_000, now = Date.now()): boolean {
99
75
  return !creds.expiresAt || creds.expiresAt - now <= skewMs;
100
76
  }
101
77
 
102
- /**
103
- * Ask the API which org `accessToken` ACTUALLY belongs to. Stored labels
104
- * (accounts.json, oauthAccount) can drift from the credential they describe;
105
- * the token itself cannot lie. Requires a non-expired access token. Read-only:
106
- * never rotates anything.
107
- */
108
78
  export async function fetchTokenOrg(accessToken: string): Promise<RolesResponse> {
109
79
  let res: Response;
110
80
  try {