tokenmaxxing 1.8.0 → 1.9.1

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 +4 -29
  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,13 +1,3 @@
1
- // Per-account codex usage sampling for status/ls. The ACTIVE account (the live
2
- // auth.json's own identity, never the possibly-drifted label) samples through
3
- // the LIVE blob: its parked copy goes stale as the running codex rotates
4
- // tokens, and refreshing a superseded parked refresh token would trip the
5
- // server's reuse punishment and kill the grant family. Parked accounts sample
6
- // through their parked blobs (their owners are not running, so rotation on
7
- // refresh is safe) and every rotation is persisted back immediately.
8
- //
9
- // Caller holds the codex flock: refresh writes must not interleave with a swap.
10
-
11
1
  import { codexIdentityOf, isCodexAccessExpiring, readLiveCodexAuth, readParkedCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
12
2
  import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
13
3
  import { CodexUsageReadError, fetchCodexUsage } from "./codexusage.ts";
@@ -21,7 +11,6 @@ const CodexSampleOutcomeSchema = z.union([
21
11
  ]);
22
12
  export type CodexSampleOutcome = z.infer<typeof CodexSampleOutcomeSchema>;
23
13
 
24
- /** The live credential's own account id, or null when absent/unreadable. */
25
14
  export function liveCodexAccountId(): string | null {
26
15
  const live = readLiveCodexAuth();
27
16
  if (!live) return null;
@@ -35,16 +24,6 @@ export async function sampleCodexAccount(input: { account: CodexAccount; liveAcc
35
24
  let auth = isLive ? readLiveCodexAuth() : readParkedCodexAuth({ credFile: account.credFile });
36
25
  if (!auth) return { ok: false, reason: isLive ? "live auth.json vanished" : "no parked credential", deadGrant: false };
37
26
  if (isCodexAccessExpiring({ auth, now })) {
38
- // NEVER refresh a PRESENT account's token, live or parked. A parked
39
- // blob whose account is RUNNING in another supervised session is
40
- // superseded by that session's live rotations, and the LIVE blob's
41
- // running session can be mid-turn refreshing the same rotating token
42
- // concurrently (both actors share the 300s margin): either way the
43
- // loser of the race is reuse-punished into a dead grant family
44
- // (closing-review catch; the idle-turn-boundary safety argument covers
45
- // only the invoking session's own account). Parked reports a miss; live
46
- // fetches on the unrotated token, still valid within the margin, and
47
- // degrades to an honest miss once it expires.
48
27
  const running = presentCodexAccountIds().has(account.accountId);
49
28
  if (running && !isLive) {
50
29
  return { ok: false, reason: "running in a live codex session (parked token refresh unsafe)", deadGrant: false };
@@ -58,8 +37,6 @@ export async function sampleCodexAccount(input: { account: CodexAccount; liveAcc
58
37
  const usage = await fetchCodexUsage({ auth });
59
38
  return { ok: true, usage };
60
39
  } catch (e) {
61
- // Only the EXPECTED operational failures become a sample miss; parse and
62
- // filesystem errors keep propagating (they are drift or bugs to surface).
63
40
  if (e instanceof CodexInvalidGrantError) {
64
41
  return { ok: false, reason: e.message, deadGrant: true };
65
42
  }
@@ -1,10 +1,3 @@
1
- // codex-accounts.json + codex-lastswap.json persistence. Parallel to the
2
- // claude files in state.ts, deliberately a separate file pair: claude and
3
- // codex accounts have different shapes and swap independently, so neither
4
- // index can clobber the other. Absent files are the empty state; a file that
5
- // exists but fails to parse THROWS (a fabricated empty pool would silently
6
- // orphan parked credentials).
7
-
8
1
  import { existsSync, readFileSync } from "node:fs";
9
2
  import { writeFileAtomic } from "./atomic.ts";
10
3
  import { codexPaths } from "./paths.ts";
@@ -1,19 +1,3 @@
1
- // The codex account-switch sequence. Runs under tokenmaxxing's codex flock
2
- // (held by the caller). Codex itself has NO cross-process lock on auth.json
3
- // (verified rust-v0.144.5: refresh serialization is an in-process semaphore),
4
- // so this flock only serializes tokenmaxxing actors; a RUNNING codex can still
5
- // refresh-write the live file concurrently. The sequence therefore keeps the
6
- // harvest-install window as short as possible, and the Stop-hook trigger fires
7
- // only at an idle turn boundary where the running codex has just finished
8
- // using (and refreshing, if needed) its token.
9
- //
10
- // refresh target's parked blob (network, validates the grant, rotates)
11
- // harvest live auth.json verbatim under its OWN identity (labels drift,
12
- // the id_token cannot lie; an unknown live identity refuses the swap)
13
- // install the refreshed target as the whole live auth.json
14
- // persist the rotation into the target's parked blob
15
- // commit activeAccountId in the same critical section
16
-
17
1
  import { codexIdentityOf, readLiveCodexAuth, readParkedCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
18
2
  import { CodexInvalidGrantError, refreshCodexAuth } from "./codexoauth.ts";
19
3
  import { loadCodexAccounts, saveCodexAccounts, saveCodexLastSwapAt } from "./codexstate.ts";
@@ -27,18 +11,10 @@ export async function performCodexSwap(input: { target: CodexAccount }): Promise
27
11
  const parked = readParkedCodexAuth({ credFile: target.credFile });
28
12
  if (!parked) throw new Error(`no parked codex credential for ${target.label}`);
29
13
 
30
- // Resolve the live credential's owner BEFORE touching the network: a refused
31
- // swap must refuse with the target's parked token still valid. The refresh
32
- // rotates it server-side, and codex punishes reuse of the superseded one, so
33
- // rotating first and then throwing would kill the target's grant family
34
- // (adversarial review catch, 2026-07-16).
35
14
  const live = readLiveCodexAuth();
36
15
  let liveOwner = null;
37
16
  if (live) {
38
17
  const liveIdentity = codexIdentityOf({ auth: live });
39
- // Backstop against a drifted caller: installing the LIVE account over
40
- // itself would refresh its superseded parked token (reuse punishment kills
41
- // the grant family) and rotate the token out from under a running session.
42
18
  if (liveIdentity.accountId === target.accountId) {
43
19
  throw new Error(
44
20
  `${target.label} is already the live codex credential - refusing to swap an account onto itself`,
@@ -72,17 +48,9 @@ export async function performCodexSwap(input: { target: CodexAccount }): Promise
72
48
  }
73
49
  throw e;
74
50
  }
75
- // The rotation exists server-side from this instant: persist it before ANY
76
- // later step can fail, or a crash strands the parked file on the superseded
77
- // (reuse-punished) refresh token.
78
51
  writeParkedCodexAuth({ credFile: target.credFile, auth: fresh });
79
52
 
80
53
  if (live && liveOwner) {
81
- // Last-moment re-read: a running codex (the Apps surface) can rotate the
82
- // live token outside our flock between the identity resolution above and
83
- // this harvest, and parking the earlier snapshot would strand the newest
84
- // rotation of a reuse-punished grant family. A mid-swap identity change
85
- // refuses rather than harvesting under a stale owner.
86
54
  const liveNow = readLiveCodexAuth();
87
55
  if (!liveNow || codexIdentityOf({ auth: liveNow }).accountId !== liveOwner.accountId) {
88
56
  throw new Error("live codex credential changed mid-swap - refusing to harvest under a stale identity; retry");
@@ -1,15 +1,3 @@
1
- // The free codex usage read (user decision 2026-07-16: the direct GET, the same
2
- // HTTP call the codex CLI makes for its own /status). One authed GET returns
3
- // BOTH the token's true identity (account_id/email/plan: the codex analog of
4
- // the claude roles endpoint) and every rate-limit window. No inference runs and
5
- // no session window starts.
6
- //
7
- // Response shape pinned against a live read 2026-07-16: windows carry
8
- // used_percent + reset_at (epoch SECONDS) + limit_window_seconds, the weekly
9
- // window is PRIMARY on plans whose 5h window was removed (July 2026), and
10
- // per-model caps arrive as additional_rate_limits[] rows keyed by limit_name.
11
- // Classification is therefore duration-driven, never position-driven.
12
-
13
1
  import { z } from "zod";
14
2
  import { http, safeErrorDetail } from "./http.ts";
15
3
  import { CodexUsageSchema, type CodexAuthJson, type CodexUsage, type CodexWindow } from "./types.ts";
@@ -19,9 +7,6 @@ import { familyTokens } from "./usage.ts";
19
7
  const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
20
8
  const USAGE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_USAGE_URL) ?? "https://chatgpt.com/backend-api/wham/usage";
21
9
 
22
- /** The usage read could not complete (endpoint unreachable, HTTP failure,
23
- * drifted body): an operational miss to fall back on cached figures for,
24
- * NOT a bug to swallow. */
25
10
  export class CodexUsageReadError extends Error {
26
11
  constructor(detail: string) {
27
12
  super(`codex usage read failed: ${detail}`);
@@ -63,11 +48,6 @@ function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undef
63
48
  return out;
64
49
  }
65
50
 
66
- /**
67
- * One free usage read for the credential in `auth`. Throws on HTTP failure
68
- * (401 included: the caller decides whether to refresh and retry); error
69
- * text carries only a response-body snippet, never request headers.
70
- */
71
51
  export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<CodexUsage> {
72
52
  const { auth } = input;
73
53
  const identity = codexIdentityOf({ auth });
@@ -115,26 +95,21 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<C
115
95
  });
116
96
  }
117
97
 
118
- /** Quota-chart label for an additional_rate_limits row: the model family,
119
- * lowercase ("GPT-5.3-Codex-Spark" -> "spark"). Wire names are versioned, so
120
- * the label is derived structurally (last non-numeric token), never by exact
121
- * string (user rule 2026-07-17: chart names are lowercase, e.g. "spark"). */
98
+ const LIMIT_LABEL_ABBREVIATIONS = new Map([["reserve", "rsrv"]]);
99
+
122
100
  export function codexLimitLabel(input: { limitName: string }): string {
123
101
  const tokens = familyTokens(input.limitName).filter((t) => Number.isNaN(Number(t)));
124
- return tokens.at(-1) ?? input.limitName.trim().toLowerCase();
102
+ const label = tokens.at(-1) ?? input.limitName.trim().toLowerCase();
103
+ return LIMIT_LABEL_ABBREVIATIONS.get(label) ?? label;
125
104
  }
126
105
 
127
106
  const SESSION_WINDOW_MAX_S = 6 * 3600;
128
107
 
129
- /** A window's screening bar: short windows (5h-class) screen at the session
130
- * bar, everything else (weekly aggregates and per-model caps) at the weekly
131
- * bar. An unknown duration is treated as weekly: the strictest reading. */
132
108
  export function isSessionWindow(input: { window: CodexWindow }): boolean {
133
109
  const seconds = input.window.windowSeconds;
134
110
  return seconds != null && seconds <= SESSION_WINDOW_MAX_S;
135
111
  }
136
112
 
137
- /** The account's weekly aggregate window: the longest-duration aggregate row. */
138
113
  export function weeklyWindowOf(input: { aggregate: CodexWindow[] }): CodexWindow | null {
139
114
  let best: CodexWindow | null = null;
140
115
  for (const window of input.aggregate) {
@@ -1,12 +1,3 @@
1
- // Platform credential store - ONE backend per platform, selected when a target
2
- // is constructed:
3
- // darwin : login-keychain generic-password via security(1) (keychain.ts)
4
- // linux : plaintext files, matching claude's own Linux store
5
- // (live = <configDir>/.credentials.json, mode 0600; verified 2.1.205:
6
- // the Linux build has no keyring path at all)
7
- // Parked backups live in the keychain (darwin) or ~/.config/tokenmaxxing/creds
8
- // (linux). Call sites never branch on platform - they pass targets around.
9
-
10
1
  import { mkdirSync, readFileSync, unlinkSync } from "node:fs";
11
2
  import { dirname, join } from "node:path";
12
3
  import { z } from "zod";
@@ -29,7 +20,6 @@ function isEnoent(e: unknown): boolean {
29
20
  return e instanceof Error && "code" in e && e.code === "ENOENT";
30
21
  }
31
22
 
32
- /** Read a target's credential blob. Returns null if it does not exist. */
33
23
  export async function readItem(t: CredTarget): Promise<string | null> {
34
24
  if (t.kind === "keychain") return kc.readItem(t);
35
25
  try {
@@ -40,14 +30,12 @@ export async function readItem(t: CredTarget): Promise<string | null> {
40
30
  }
41
31
  }
42
32
 
43
- /** Create-or-update a target with `secret` as its blob. Throws on failure. */
44
33
  export async function writeItem(t: CredTarget, secret: string): Promise<void> {
45
34
  if (t.kind === "keychain") return kc.writeItem(t, secret);
46
35
  mkdirSync(dirname(t.path), { recursive: true, mode: 0o700 });
47
36
  writeFileAtomic(t.path, secret, 0o600);
48
37
  }
49
38
 
50
- /** Delete a target. Returns true if it existed and was removed. */
51
39
  export async function deleteItem(t: CredTarget): Promise<boolean> {
52
40
  if (t.kind === "keychain") return kc.deleteItem(t);
53
41
  try {
@@ -59,42 +47,30 @@ export async function deleteItem(t: CredTarget): Promise<boolean> {
59
47
  }
60
48
  }
61
49
 
62
- /** The live default credential claude reads (`Claude Code-credentials` / $USER
63
- * on darwin, `<credDir()>/.credentials.json` on linux). */
64
50
  export function liveTarget(): CredTarget {
65
51
  return darwin
66
52
  ? { kind: "keychain", service: kcNames.service, account: kcNames.account }
67
53
  : { kind: "file", path: join(credDir(), ".credentials.json") };
68
54
  }
69
55
 
70
- /** A parked account's backup (`tokenmaxxing-cred-<uuid8>` item / .json file). */
71
56
  export function parkedTarget(itemName: string): CredTarget {
72
57
  return darwin
73
58
  ? { kind: "keychain", service: itemName, account: kcNames.account }
74
59
  : { kind: "file", path: join(paths.credsDir, `${itemName}.json`) };
75
60
  }
76
61
 
77
- /** The credential claude uses under CLAUDE_CONFIG_DIR=`configDirRaw`: a
78
- * hash-namespaced keychain service on darwin, the file inside the dir on linux.
79
- * The dir string must be byte-stable on darwin (hash is over the RAW string). */
80
62
  export function isolatedTarget(configDirRaw: string): CredTarget {
81
63
  return darwin
82
64
  ? { kind: "keychain", service: namespacedCredService(configDirRaw), account: kcNames.account }
83
65
  : { kind: "file", path: join(configDirRaw, ".credentials.json") };
84
66
  }
85
67
 
86
- /** Reduce a full credential blob to just `{claudeAiOauth}` - what parked items
87
- * store (small → always the ps-safe keychain write path on darwin). */
88
68
  export function claudeAiOauthOnly(fullBlobRaw: string): string {
89
69
  const b = CredentialBlobSchema.parse(JSON.parse(fullBlobRaw));
90
70
  return JSON.stringify({ claudeAiOauth: b.claudeAiOauth });
91
71
  }
92
72
 
93
- /** Merge a fresh `claudeAiOauth` into the CURRENT live blob, preserving every
94
- * sibling key (MCP OAuth state, etc.). Returns the full blob string to install. */
95
73
  export function mergeIntoLive(currentLiveRaw: string | null, freshClaudeAiOauth: unknown): string {
96
- // null-check, not falsiness: an EXISTING-but-empty blob is corruption and
97
- // must surface (JSON.parse throws), never be silently replaced as "missing".
98
74
  const base = currentLiveRaw == null ? {} : BlobRecordSchema.parse(JSON.parse(currentLiveRaw));
99
75
  base["claudeAiOauth"] = freshClaudeAiOauth;
100
76
  return JSON.stringify(base);