tokenmaxxing 1.20.0 → 1.23.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 (38) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +3 -30
  3. package/package.json +1 -7
  4. package/src/cli/check.ts +0 -3
  5. package/src/cli/config.ts +0 -1
  6. package/src/cli/status.ts +2 -12
  7. package/src/entries/codexstophook.ts +0 -1
  8. package/src/entries/stopfailurehook.ts +6 -9
  9. package/src/entries/stophook.ts +0 -1
  10. package/src/entries/supervisor.ts +1 -2
  11. package/src/lib/codexdecide.ts +1 -25
  12. package/src/lib/codexpick.ts +3 -3
  13. package/src/lib/codexusage.ts +0 -9
  14. package/src/lib/decide.ts +4 -69
  15. package/src/lib/paths.ts +0 -1
  16. package/src/lib/state.ts +2 -82
  17. package/src/lib/types.ts +0 -53
  18. package/agent-plugin/bin/tokenmaxxing-mcp +0 -7
  19. package/agent-plugin/mcp.json +0 -10
  20. package/agent-plugin/plugin.json +0 -20
  21. package/agent-plugin/skills/codex-pool/SKILL.md +0 -23
  22. package/agent-plugin/skills/codex-pool/references/codex.md +0 -5
  23. package/agent-plugin/skills/credentials-hygiene/SKILL.md +0 -27
  24. package/agent-plugin/skills/credentials-hygiene/references/credentials.md +0 -6
  25. package/agent-plugin/skills/doctor-diagnostics/SKILL.md +0 -26
  26. package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +0 -5
  27. package/agent-plugin/skills/pool-status/SKILL.md +0 -27
  28. package/agent-plugin/skills/pool-status/references/commands.md +0 -9
  29. package/agent-plugin/skills/safe-contribution/SKILL.md +0 -27
  30. package/agent-plugin/skills/safe-contribution/references/ship.md +0 -5
  31. package/agent-plugin/skills/sdk-pairing/SKILL.md +0 -34
  32. package/agent-plugin/skills/sdk-pairing/references/sdk.md +0 -6
  33. package/agent-plugin/skills/switching-policy/SKILL.md +0 -35
  34. package/agent-plugin/skills/switching-policy/references/policy.md +0 -10
  35. package/src/entries/mcp.ts +0 -277
  36. package/src/lib/bankedreset.ts +0 -234
  37. package/src/lib/codexreset.ts +0 -122
  38. package/src/sdk.ts +0 -55
package/src/lib/state.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import { closeSync, existsSync, fstatSync, openSync, readFileSync, rmSync, utimesSync } from "node:fs";
2
- import { isEqual, uniq } from "es-toolkit";
2
+ import { isEqual } from "es-toolkit";
3
3
  import { z } from "zod";
4
4
  import { paths, realClaudeBinFromEnv, realCodexBinFromEnv } from "./paths.ts";
5
5
  import { writeFileAtomic } from "./atomic.ts";
6
6
  import {
7
7
  AccountsIndexSchema,
8
- BankedResetProvidersSchema,
9
8
  ConfigSchema,
10
9
  LastSwapSchema,
11
10
  ModelUsageStateSchema,
@@ -14,11 +13,8 @@ import {
14
13
  UsageStateSchema,
15
14
  type AccountsIndex,
16
15
  type Config,
17
- type FamilyAnchor,
18
16
  type ModelUsageState,
19
17
  type UsageState,
20
- type UsageWindow,
21
- type WindowAnchor,
22
18
  } from "./types.ts";
23
19
 
24
20
  const DEFAULT_CONFIG: Config = {
@@ -34,7 +30,6 @@ const DEFAULT_CONFIG: Config = {
34
30
  usagePollTtlMs: 90_000,
35
31
  maxWaitMs: 3_600_000,
36
32
  checkIntervalMs: 60_000,
37
- preferToUseBankedReset: [],
38
33
  },
39
34
  };
40
35
 
@@ -55,7 +50,6 @@ export const ConfigFileSchema = z
55
50
  usagePollTtlMs: z.number().int().positive(),
56
51
  maxWaitMs: z.number().int().positive(),
57
52
  checkIntervalMs: z.number().int().min(10_000),
58
- preferToUseBankedReset: BankedResetProvidersSchema,
59
53
  })
60
54
  .partial(),
61
55
  })
@@ -89,9 +83,6 @@ export function mergeConfigFile(p: z.infer<typeof ConfigFileSchema>): MergeOutco
89
83
  if (p.policy?.switchModels) {
90
84
  cfg.policy.switchModels = p.policy.switchModels.map((s) => s.toLowerCase());
91
85
  }
92
- if (p.policy?.preferToUseBankedReset) {
93
- cfg.policy.preferToUseBankedReset = uniq(p.policy.preferToUseBankedReset);
94
- }
95
86
  const envBin = realClaudeBinFromEnv();
96
87
  if (envBin) cfg.claudeBin = envBin;
97
88
  const envCodexBin = realCodexBinFromEnv();
@@ -196,29 +187,6 @@ export function saveLastSwapAt(ts: number): void {
196
187
  writeFileAtomic(paths.lastSwapJson, JSON.stringify(LastSwapSchema.parse({ ts })));
197
188
  }
198
189
 
199
- export function loadLastResetAt(): number | null {
200
- if (!existsSync(paths.lastResetJson)) return null;
201
- let json: unknown;
202
- try {
203
- json = JSON.parse(readFileSync(paths.lastResetJson, "utf8"));
204
- } catch {
205
- throw new Error(`${paths.lastResetJson} is corrupt (unparsable JSON) - refusing to treat a damaged reset clock as never-reset; repair or remove the file`);
206
- }
207
- return LastSwapSchema.parse(json).ts;
208
- }
209
-
210
- export function saveLastResetAt(ts: number): void {
211
- writeFileAtomic(paths.lastResetJson, JSON.stringify(LastSwapSchema.parse({ ts })));
212
- }
213
-
214
- export function loadLastSeatChangeAt(): number | null {
215
- const swapAt = loadLastSwapAt();
216
- const resetAt = loadLastResetAt();
217
- if (swapAt == null) return resetAt;
218
- if (resetAt == null) return swapAt;
219
- return Math.max(swapAt, resetAt);
220
- }
221
-
222
190
  const DepletedWaitSchema = z.object({ waitUntil: z.number(), accountUuid: z.string(), ts: z.number() });
223
191
  export type DepletedWait = z.infer<typeof DepletedWaitSchema>;
224
192
 
@@ -270,61 +238,13 @@ export function clearNextCheck(): void {
270
238
 
271
239
  const USAGE_TS_REFRESH_MS = 10 * 60_000;
272
240
  const SAMPLED_AT_REFRESH_MS = 30_000;
273
- const SESSION_COST_MIN_DELTA = 25;
274
-
275
- function anchorHolds(anchor: WindowAnchor, next: UsageState): boolean {
276
- return (
277
- anchor.fiveHourResetsAt === next.fiveHour.resetsAt &&
278
- anchor.sevenDayResetsAt === next.sevenDay.resetsAt &&
279
- next.fiveHour.usedPercentage >= anchor.session &&
280
- next.sevenDay.usedPercentage >= anchor.weekly
281
- );
282
- }
283
-
284
- export function measureSessionWindow(prev: UsageState | null, next: UsageState, stamp: boolean): Pick<UsageState, "anchor" | "sessionWindowWeeklyCost"> {
285
- const same = prev != null && prev.account === next.account;
286
- const carried = same ? prev.sessionWindowWeeklyCost : undefined;
287
- if (stamp) return { ...(same && prev.anchor ? { anchor: prev.anchor } : {}), ...(carried != null ? { sessionWindowWeeklyCost: carried } : {}) };
288
- const anchor: WindowAnchor =
289
- same && prev.anchor && anchorHolds(prev.anchor, next)
290
- ? prev.anchor
291
- : { fiveHourResetsAt: next.fiveHour.resetsAt, sevenDayResetsAt: next.sevenDay.resetsAt, session: next.fiveHour.usedPercentage, weekly: next.sevenDay.usedPercentage };
292
- const sessionDelta = next.fiveHour.usedPercentage - anchor.session;
293
- const weeklyDelta = next.sevenDay.usedPercentage - anchor.weekly;
294
- const measured = sessionDelta >= SESSION_COST_MIN_DELTA && weeklyDelta > 0 ? (100 * weeklyDelta) / sessionDelta : undefined;
295
- const cost = measured ?? carried;
296
- return { anchor, ...(cost != null ? { sessionWindowWeeklyCost: cost } : {}) };
297
- }
298
-
299
- function familyAnchorHolds(anchor: FamilyAnchor, session: UsageWindow, caps: Record<string, number>): boolean {
300
- return (
301
- anchor.fiveHourResetsAt === session.resetsAt &&
302
- session.usedPercentage >= anchor.session &&
303
- Object.entries(caps).every(([family, used]) => anchor.caps[family] == null || used >= anchor.caps[family])
304
- );
305
- }
306
-
307
- export function measureFamilyWindow(prev: ModelUsageState | null, account: string | null, session: UsageWindow | null, caps: Record<string, number>): Pick<ModelUsageState, "anchor" | "familyCosts"> {
308
- const same = prev != null && prev.account === account;
309
- const carried = same ? prev.familyCosts : undefined;
310
- if (session == null) return { ...(same && prev.anchor ? { anchor: prev.anchor } : {}), ...(carried != null ? { familyCosts: carried } : {}) };
311
- const held = same && prev.anchor && familyAnchorHolds(prev.anchor, session, caps) ? prev.anchor : null;
312
- const anchor: FamilyAnchor = held ? { ...held, caps: { ...caps, ...held.caps } } : { fiveHourResetsAt: session.resetsAt, session: session.usedPercentage, caps };
313
- const sessionDelta = session.usedPercentage - anchor.session;
314
- const measured = Object.entries(caps).flatMap(([family, used]) => {
315
- const capDelta = used - (anchor.caps[family] ?? used);
316
- return sessionDelta >= SESSION_COST_MIN_DELTA && capDelta > 0 ? [[family, (100 * capDelta) / sessionDelta] as const] : [];
317
- });
318
- const familyCosts = { ...carried, ...Object.fromEntries(measured) };
319
- return { anchor, ...(Object.keys(familyCosts).length > 0 ? { familyCosts } : {}) };
320
- }
321
241
 
322
242
  export function writeUsage(input: UsageState, opts: { stamp?: boolean } = {}): boolean {
323
243
  const prev = loadUsage();
324
244
  const stamp = opts.stamp === true;
325
245
  const carriedSampleAt = prev != null && prev.account === input.account ? (prev.sampledAt ?? prev.ts) : undefined;
326
246
  const sampledAt = stamp ? (carriedSampleAt ?? input.sampledAt) : input.ts;
327
- const next: UsageState = { ...input, ...measureSessionWindow(prev, input, stamp), ...(sampledAt != null ? { sampledAt } : {}) };
247
+ const next: UsageState = { ...input, ...(sampledAt != null ? { sampledAt } : {}) };
328
248
  if (
329
249
  prev &&
330
250
  isEqual({ ...prev, ts: 0, sampledAt: 0 }, { ...next, ts: 0, sampledAt: 0 }) &&
package/src/lib/types.ts CHANGED
@@ -41,51 +41,22 @@ export type UsageWindows = z.infer<typeof UsageWindowsSchema>;
41
41
  const ModelInfoSchema = z.object({ id: z.string(), display: z.string() });
42
42
  export type ModelInfo = z.infer<typeof ModelInfoSchema>;
43
43
 
44
- const WindowAnchorSchema = z.object({
45
- fiveHourResetsAt: z.number().nullable(),
46
- sevenDayResetsAt: z.number().nullable(),
47
- session: z.number(),
48
- weekly: z.number(),
49
- });
50
- export type WindowAnchor = z.infer<typeof WindowAnchorSchema>;
51
-
52
44
  export const UsageStateSchema = UsageWindowsSchema.extend({
53
45
  account: z.string().nullable(),
54
46
  ts: z.number(),
55
47
  model: ModelInfoSchema.nullable().default(null),
56
- anchor: WindowAnchorSchema.optional(),
57
- sessionWindowWeeklyCost: z.number().positive().optional(),
58
48
  sampledAt: z.number().optional(),
59
49
  });
60
50
  export type UsageState = z.infer<typeof UsageStateSchema>;
61
51
 
62
- const FamilyAnchorSchema = z.object({
63
- fiveHourResetsAt: z.number().nullable(),
64
- session: z.number(),
65
- caps: z.record(z.string(), z.number()),
66
- });
67
- export type FamilyAnchor = z.infer<typeof FamilyAnchorSchema>;
68
-
69
52
  export const ModelUsageStateSchema = z.object({
70
53
  perModel: z.record(z.string(), UsageWindowSchema).default({}),
71
54
  account: z.string().nullable(),
72
55
  ts: z.number(),
73
56
  sampledAt: z.number().optional(),
74
- anchor: FamilyAnchorSchema.optional(),
75
- familyCosts: z.record(z.string(), z.number()).optional(),
76
57
  });
77
58
  export type ModelUsageState = z.infer<typeof ModelUsageStateSchema>;
78
59
 
79
- export const BankedResetOutcomeSchema = z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "rate_limited", "auth_error", "error"]);
80
- export type BankedResetOutcome = z.infer<typeof BankedResetOutcomeSchema>;
81
-
82
- export const BankedResetRecordSchema = z.object({
83
- outcome: BankedResetOutcomeSchema,
84
- at: z.number(),
85
- nextAvailableAt: z.number().nullable(),
86
- });
87
- export type BankedResetRecord = z.infer<typeof BankedResetRecordSchema>;
88
-
89
60
  export const AccountSchema = z.object({
90
61
  accountUuid: z.string(),
91
62
  email: z.string(),
@@ -102,8 +73,6 @@ export const AccountSchema = z.object({
102
73
  needsReauth: z.boolean().optional(),
103
74
  subscriptionType: z.string().optional(),
104
75
  rateLimitTier: z.string().optional(),
105
- sessionWindowWeeklyCost: z.number().positive().optional(),
106
- bankedReset: BankedResetRecordSchema.optional(),
107
76
  });
108
77
  export type Account = z.infer<typeof AccountSchema>;
109
78
 
@@ -120,7 +89,6 @@ export const NextCheckSchema = z.object({ dueAt: z.number(), ts: z.number() });
120
89
 
121
90
  export const EnforcedLimitSchema = z.object({
122
91
  account: z.string(),
123
- kind: z.enum(["session", "weekly", "model"]),
124
92
  family: z.string().nullable(),
125
93
  resetsAt: z.number().nullable(),
126
94
  windowMs: z.number(),
@@ -145,10 +113,6 @@ export const ScreeningThresholdsSchema = z.object({
145
113
  weekly: z.number().min(0).max(100),
146
114
  });
147
115
 
148
- export const BankedResetProviderSchema = z.enum(["claude", "codex"]);
149
- export type BankedResetProvider = z.infer<typeof BankedResetProviderSchema>;
150
- export const BankedResetProvidersSchema = z.array(BankedResetProviderSchema);
151
-
152
116
  export const ConfigSchema = z
153
117
  .object({
154
118
  thresholds: ScreeningThresholdsSchema,
@@ -163,7 +127,6 @@ export const ConfigSchema = z
163
127
  usagePollTtlMs: z.number().int().positive(),
164
128
  maxWaitMs: z.number().int().positive(),
165
129
  checkIntervalMs: z.number().int().min(10_000),
166
- preferToUseBankedReset: BankedResetProvidersSchema,
167
130
  }),
168
131
  })
169
132
  .refine((cfg) => cfg.policy.projectionMargin < Math.min(...cfg.thresholds.session, cfg.thresholds.weekly), {
@@ -181,7 +144,6 @@ export const RespawnMarkerSchema = z.object({
181
144
  sessionId: z.string(),
182
145
  prompt: z.string().optional(),
183
146
  launchedAt: z.number().optional(),
184
- reset: z.boolean().optional(),
185
147
  });
186
148
 
187
149
  export const RateLimitsStdinSchema = z.looseObject({
@@ -281,21 +243,9 @@ export const CodexUsageSchema = z.object({
281
243
  planType: z.string().nullable(),
282
244
  aggregate: z.array(CodexWindowSchema),
283
245
  perLimit: z.record(z.string(), z.array(CodexWindowSchema)),
284
- resetCredits: z.number().int().nullable(),
285
- reachedType: z.string().nullable(),
286
246
  });
287
247
  export type CodexUsage = z.infer<typeof CodexUsageSchema>;
288
248
 
289
- export const CodexBankedResetOutcomeSchema = z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed", "auth_error", "error"]);
290
- export type CodexBankedResetOutcome = z.infer<typeof CodexBankedResetOutcomeSchema>;
291
-
292
- export const CodexBankedResetRecordSchema = z.object({
293
- outcome: CodexBankedResetOutcomeSchema,
294
- at: z.number(),
295
- key: z.string().optional(),
296
- });
297
- export type CodexBankedResetRecord = z.infer<typeof CodexBankedResetRecordSchema>;
298
-
299
249
  const BareFileNameSchema = z
300
250
  .string()
301
251
  .refine((s) => s.length > 0 && s !== "." && s !== ".." && !s.includes("/") && !s.includes("\\"), {
@@ -314,12 +264,9 @@ export const CodexAccountSchema = z.object({
314
264
  .object({
315
265
  aggregate: z.array(CodexWindowSchema),
316
266
  perLimit: z.record(z.string(), z.array(CodexWindowSchema)),
317
- resetCredits: z.number().int().nullable().optional(),
318
- reachedType: z.string().nullable().optional(),
319
267
  })
320
268
  .optional(),
321
269
  lastUsageAt: z.number().optional(),
322
- bankedReset: CodexBankedResetRecordSchema.optional(),
323
270
  });
324
271
  export type CodexAccount = z.infer<typeof CodexAccountSchema>;
325
272
 
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { main } from "../../src/entries/mcp.ts";
3
-
4
- main().catch((e: unknown) => {
5
- console.error(e instanceof Error ? e.message : String(e));
6
- process.exit(1);
7
- });
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
- "mcpServers": {
4
- "tokenmaxxing": {
5
- "type": "stdio",
6
- "command": "./bin/tokenmaxxing-mcp",
7
- "cwd": "${PLUGIN_ROOT}"
8
- }
9
- }
10
- }
@@ -1,20 +0,0 @@
1
- {
2
- "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
- "name": "tokenmaxxing",
4
- "version": "1.20.0",
5
- "description": "Pool Claude Code and Codex logins, switch on pace pressure, and expose safe ops tools to agent clients.",
6
- "author": {
7
- "name": "anaclumos",
8
- "url": "https://github.com/anaclumos"
9
- },
10
- "homepage": "https://github.com/anaclumos/tokenmaxxing",
11
- "repository": "https://github.com/anaclumos/tokenmaxxing",
12
- "license": "MIT",
13
- "keywords": [
14
- "claude",
15
- "codex",
16
- "quota",
17
- "account-switching",
18
- "mcp"
19
- ]
20
- }
@@ -1,23 +0,0 @@
1
- ---
2
- name: codex-pool
3
- description: Operate the Codex side of the pool (restart-is-switch, no Layer 2, never target a running sibling). Use for codex init/switch/status questions or MCP pool_switch with codex=true.
4
- ---
5
-
6
- # Codex pool
7
-
8
- ## Mechanics
9
-
10
- - No hot-swap: restart IS the switch (`codex resume <sid>`). A credential write takes effect on the next codex start.
11
- - Auto-switching needs trusted hooks (`/hooks`). Do not clobber the user's `notify` key in `config.toml`.
12
- - Refresh-token reuse is punished; a superseded token can kill the grant family. Persist every rotation immediately.
13
- - An account running in another supervised session is never a swap target and never sampler-refreshed. Parked does not mean idle.
14
- - Classify windows by DURATION, never by position. Some plans have no 5h window.
15
- - Layer 2 wall squeeze is Claude-only. Do not extend it to Codex.
16
-
17
- ## Agent actions
18
-
19
- - Read: `pool_ls` / `pool_status` (Codex section included when present).
20
- - Mutate: MCP `pool_switch` with `codex=true`, `confirm=true`, and `TOKENMAXXING_AGENT_MUTATIONS=1` after user approval.
21
- - Interactive `init --codex` / `add --codex` / reauth stay human-driven.
22
-
23
- See [references/codex.md](references/codex.md).
@@ -1,5 +0,0 @@
1
- # Codex references
2
-
3
- - `docs/content/docs/codex.mdx`
4
- - `src/lib/codexdecide.ts`, `src/lib/codexpick.ts`, `src/lib/codexswap.ts`
5
- - `.memory/cc-codex-auth-mechanics.md`
@@ -1,27 +0,0 @@
1
- ---
2
- name: credentials-hygiene
3
- description: Credential and identity rules for tokenmaxxing (fetchTokenIdentity account-uuid identity, flock, no blob compare, never print secrets). Use before any auth, swap, or credential-touching change.
4
- ---
5
-
6
- # Credentials hygiene
7
-
8
- ## Identity
9
-
10
- - Identity is the `accountUuid` that `fetchTokenIdentity` reports for a token (`GET /api/oauth/profile`), never the organization, never a stored label, and never a blob byte compare. Every seat of a Team plan shares one `organizationUuid`, so an org-keyed lookup collapses the seats onto one account. Two rotations of one account differ byte-for-byte.
11
- - Park a credential under its token's real owner. Commit the active label in the same critical section as credential writes.
12
- - Every live-store write goes through `withClaudeRefreshLock` (Claude). Near-expiry sessions can rotate into the live store at any moment.
13
- - A refresh rotation revokes the previous access token at once, so refreshing any copy of a credential that is live elsewhere kills those sessions' token.
14
-
15
- ## Isolation
16
-
17
- - Refuse ambient `CLAUDE_CONFIG_DIR` / `CLAUDE_SECURESTORAGE_CONFIG_DIR` in CLI, SDK, and MCP.
18
- - A missing namespaced keychain item never falls back to the live one.
19
- - Same accounts pooled on several hosts race on refresh (documented limitation).
20
-
21
- ## Agent hard stops
22
-
23
- - No tool may return keychain blobs, `.credentials.json`, `auth.json`, or OAuth access/refresh tokens.
24
- - Do not bypass Claude `/usage` with direct `GET /api/oauth/usage` (owner-rejected; 429s on the active session).
25
- - Prefer MCP read tools; never `cat` credential paths into the transcript.
26
-
27
- See [references/credentials.md](references/credentials.md).
@@ -1,6 +0,0 @@
1
- # Credential references
2
-
3
- - `docs/content/docs/credentials.mdx`
4
- - `DESIGN.md` sections on install and swap
5
- - `.memory/cc-codex-auth-mechanics.md`
6
- - `src/lib/credstore.ts`, `src/lib/oauth.ts`
@@ -1,26 +0,0 @@
1
- ---
2
- name: doctor-diagnostics
3
- description: Diagnose a broken or incomplete tokenmaxxing install (supervisor, hooks, timer, credential identity). Use when doctor fails, PATH looks wrong, or identity drift is suspected. Prefer the MCP doctor tool.
4
- ---
5
-
6
- # Doctor and diagnostics
7
-
8
- ## Prefer MCP
9
-
10
- Call `doctor`. It checks the supervisor wrapper, settings hooks, periodic timer, live/parked credential presence, and org identity match. Output is pass/fail plus labels.
11
-
12
- Also useful: `config_get` (no key) for effective config sources, and `pool_ls` for needs-reauth.
13
-
14
- ## Hard stops
15
-
16
- - Never dump keychain items, `.credentials.json`, `auth.json`, or OAuth tokens into chat.
17
- - Doctor never refreshes tokens. Expired access tokens show as unverifiable identity, not as a prompt to paste secrets.
18
- - Do not run `init` / `add` / `auth` from an agent session without the user present for interactive login.
19
-
20
- ## Common repairs (ask before mutating)
21
-
22
- - Missing wrapper or hooks: user runs `tokenmaxxing init` (or `init --codex`).
23
- - needs-reauth: user runs `tokenmaxxing auth <label>` (or `--all`).
24
- - PATH: `binDir` must precede the real claude.
25
-
26
- See [references/troubleshooting.md](references/troubleshooting.md).
@@ -1,5 +0,0 @@
1
- # Troubleshooting pointers
2
-
3
- - `docs/content/docs/troubleshooting.mdx`
4
- - `docs/content/docs/state-directory.mdx` for where state lives under `~/.config/tokenmaxxing` (or `TOKENMAXXING_HOME`)
5
- - Ambient `CLAUDE_CONFIG_DIR` / `CLAUDE_SECURESTORAGE_CONFIG_DIR` must be unset for CLI, SDK, and this MCP server
@@ -1,27 +0,0 @@
1
- ---
2
- name: pool-status
3
- description: Read the Claude and Codex account pool safely (list, usage bars, watch). Use when checking quota, which account is active, needs-reauth, or before any switch. Prefer MCP pool_ls and pool_status over raw shell.
4
- ---
5
-
6
- # Pool status (safe reads)
7
-
8
- ## Prefer MCP
9
-
10
- - `pool_ls` for labels, active marker, needs-reauth
11
- - `pool_status` for 5h / weekly / per-model bars (free `/usage` path)
12
-
13
- If MCP is unavailable, run `tokenmaxxing ls --json` or `tokenmaxxing status --json` (alias `xx`) and parse the document. Never add `--ping`.
14
-
15
- ## Hard stops
16
-
17
- - Do not run `status --ping` or bare `xx --ping`. That pings every account with a real request and opens each 5h window.
18
- - Do not print credential files, keychain blobs, or OAuth tokens. Labels and status only.
19
- - Do not kill sessions or supervisors to "free" an account.
20
-
21
- ## Notes
22
-
23
- - Active Claude usage often comes from the statusLine tee; parked accounts are probed in isolation.
24
- - `watch` re-renders status on an interval and never pings.
25
- - Hermetic agents: set `TOKENMAXXING_HOME` to a throwaway directory. That isolates state files only: `init`, `uninstall`, and the codex hook install still write settings.json, codex `hooks.json`, the shell rc, and the timer unit under `HOME`, so never run them from an agent.
26
-
27
- See [references/commands.md](references/commands.md).
@@ -1,9 +0,0 @@
1
- # Status and list commands
2
-
3
- - `tokenmaxxing` / `tokenmaxxing status`: sample and show bars; persists cached usage for the picker.
4
- - `tokenmaxxing status --ping [--count N]`: DENIED for agents without explicit user approval. Meters every account, or N random ones under `--count`.
5
- - `tokenmaxxing ls`: compact list.
6
- - `tokenmaxxing watch [seconds]`: live re-render (default 120), never `--ping`.
7
- - `--json` on any of these: one JSON document on stdout (`ok` mirrors the exit code); parse it instead of scraping the bars. `watch --json` prints one document per tick and never exits on its own.
8
-
9
- Docs: `docs/content/docs/commands.mdx`.
@@ -1,27 +0,0 @@
1
- ---
2
- name: safe-contribution
3
- description: Safely change and ship tokenmaxxing (verification without a test suite, PR window, Mac vs Linux skew, public-repo hygiene). Use when editing this repo, opening PRs, or verifying decision-path changes.
4
- ---
5
-
6
- # Safe contribution
7
-
8
- ## Verify
9
-
10
- - The repo carries no test code and no comments (owner ruling 2026-09-02). Never add tests, a test script, or comments.
11
- - Verify with `bun run typecheck`, then hermetic CLI runs: `TOKENMAXXING_HOME=/tmp/xx-test bun run src/main.ts ...` (for decision-path changes, write accounts.json, usage.json, config.json, and a claude.json under that root and run `check`; point `TOKENMAXXING_KEYCHAIN_SERVICE` and `TOKENMAXXING_KEYCHAIN_ACCOUNT` at throwaway names and `TOKENMAXXING_CLAUDE_JSON` at that claude.json, because the CLI resolves claude.json from HOME, not from `TOKENMAXXING_HOME`).
12
- - Ask before any run that meters real quota (`status --ping`, live-pool inference). Free `/usage` / plain status is fine.
13
-
14
- ## Ship
15
-
16
- - Work on a branch; bump `package.json` and `agent-plugin/plugin.json` to the same version in the same PR; open PR; wait the full review window; handle every review; merge; `gh release create v<version>`; verify npm publish.
17
- - A PR that changes no packed file skips the bump and the release, and merging it is shipping it. Check membership with `npm pack --dry-run`, not the `files` array: the tarball is `src`, `agent-plugin`, `README.md`, `DESIGN.md`, `LICENSE`, and `package.json`, the last two always packed though `files` never names them. So a `docs/`, `.memory/`, or AGENTS.md change is exempt, while a LICENSE or metadata change is not. Everything else, the full review window included, still applies.
18
- - Never push directly to main. npm trusted publishing is bound to workflow filename `ci.yml`.
19
-
20
- ## Machine gotchas
21
-
22
- - The owner's Mac may run this working tree live. Do not kill sessions or supervisors without asking.
23
- - Linux boxes often run an older npm global: compare installed version before "works on Mac not Linux" debugging.
24
- - Repo is PUBLIC: no Slack IDs, device info, or secrets in commits, PRs, docs, or `.memory`.
25
- - Do not reintroduce banned patterns listed in `AGENTS.md` (Stop text-sniff failsafe, rejected statusline formats, old-state shims).
26
-
27
- See [references/ship.md](references/ship.md).
@@ -1,5 +0,0 @@
1
- # Ship and testing references
2
-
3
- - `AGENTS.md` (Release and CI, Testing, Do not reintroduce)
4
- - `.memory/shipping-pr-based.md`
5
- - `.memory/review-window-is-mandatory-full-length.md`
@@ -1,34 +0,0 @@
1
- ---
2
- name: sdk-pairing
3
- description: Pair tokenmaxxing with the Claude Agent SDK (ensureBestAccount, pooledOptions, stopHookCheck). Use when building or debugging Bun agents that spawn Claude through the pooled credential store.
4
- ---
5
-
6
- # SDK pairing
7
-
8
- Import from the package root (`tokenmaxxing` → `src/sdk.ts`). Bun only.
9
-
10
- ```ts
11
- import { query } from "@anthropic-ai/claude-agent-sdk";
12
- import { ensureBestAccount, pooledOptions, stopHookCheck } from "tokenmaxxing";
13
-
14
- await ensureBestAccount();
15
- for await (const message of query({
16
- prompt: "...",
17
- options: {
18
- ...pooledOptions(),
19
- hooks: { Stop: [{ hooks: [stopHookCheck] }] },
20
- },
21
- })) {
22
- }
23
- ```
24
-
25
- The loop body receives the message stream; keep the session id from the init message for `resume` across swaps.
26
-
27
- ## Rules
28
-
29
- - Call `ensureBestAccount()` before each `query()` spawn (no mid-query hot-swap).
30
- - `pooledOptions()` pins the real claude binary and a full replacement env with credential overrides scrubbed. It throws if `CLAUDE_CONFIG_DIR` or `CLAUDE_SECURESTORAGE_CONFIG_DIR` is set.
31
- - `stopHookCheck` re-decides at turn boundaries; errors are swallowed so a broken check does not abort the turn.
32
- - Own-accounts only (see terms docs). Offering pooled subscription logins to third parties is a ToS problem.
33
-
34
- See [references/sdk.md](references/sdk.md).
@@ -1,6 +0,0 @@
1
- # SDK references
2
-
3
- - `src/sdk.ts` (self-documenting header)
4
- - `docs/content/docs/sdk.mdx`
5
- - `docs/content/docs/terms.mdx`
6
- - `.memory/agent-sdk-auth-surface.md`
@@ -1,35 +0,0 @@
1
- ---
2
- name: switching-policy
3
- description: Explain and apply tokenmaxxing switch policy (greedy vs hard path, pace pressure, Layer 2 wall squeeze Claude-only, model-family matching). Use before pool_switch, pool_check, or when changing thresholds.
4
- ---
5
-
6
- # Switching policy
7
-
8
- ## Vocabulary
9
-
10
- - **Engaged**: session used >= `policy.greedySessionFloor` (default 80) or any screening bar crossed.
11
- - **GREEDY path** permits lateral moves only within the incumbent organization and retains `greedySwapMargin` hysteresis.
12
- - **HARD path** starts at a crossed screening bar and may cross organizations, with Layer 2 as its fallback.
13
- - **Organization preference** requires measured headroom in session, aggregate weekly and gated model windows under the [policy](references/policy.md).
14
- - **Verification** attempts at most two stale candidates per evaluation, with a 12-second deadline covering identity reads and the CLI for each candidate.
15
- - **Probe results** are persisted and re-ranked, while screening exhaustion remains eligible for the wall squeeze and reset selection.
16
- - **Unavailable verification** leaves cached figures in force, and verification never spends a parked refresh grant.
17
- - **Manual switch** retains pure pace ranking with no organization preference or incumbent margin.
18
- - **Pace pressure**: remaining weekly percent / time to weekly reset (highest first). Not most-remaining.
19
- - **Effective bars**: `effectiveBars(cfg, pool)` = the active rung of the 5h ladder (`thresholds.session`, default `[90]`, a single rung: the lowest rung some pooled account, the current one included, still clears) and the weekly bar, each minus `policy.projectionMargin`. Trigger and screening must share these bars or swaps ping-pong. Codex reads `terminalBars(cfg)`, the top rung only. The check cadence is capped one band per rung climbed, and every band is a multiple of the tick `policy.checkIntervalMs` (default 60000).
20
- - **Banked reset**: opt-in via `policy.preferToUseBankedReset` (a provider list, default `[]`), hard path only. Claude: the seat holds between the session rung and the wall while a reset is believed available and the weekly windows have room for one more measured session window (`sessionWindowWeeklyCost`; unmeasured swaps), claims the `/limit-reset` server call at the wall, and keeps the seat on `reset` (the failed turn is retriggered in place). Codex: rides to the wall while a reset credit exists, consumes one, no restart. The greedy path never resets.
21
-
22
- ## Layer 2 (Claude only)
23
-
24
- When the hard path finds no usable target, judge against the wall (`hardThresholds` minus margin). Under-wall seat HOLDS; walled seat swaps to best under-wall sibling. Codex has no Layer 2 last-drop swap (cannot hot-adopt).
25
-
26
- ## Model matching
27
-
28
- Match model families by exact token after splitting id/display on spaces, dots, and hyphens (`familyTokens` / `matchedFamily` in `src/lib/usage.ts`). Never exact full display strings (names drift: "Fable" / "Fable 5"). Unmeasured usage is unknown and ranks last, never 0 / first. Per-model weekly caps: Sonnet and Fable exist; only Fable gates a switch by default (`policy.switchModels`).
29
-
30
- ## Agent actions
31
-
32
- - Explain with this skill; mutate only via MCP `pool_switch` / `pool_check` with user approval, `confirm=true`, and `TOKENMAXXING_AGENT_MUTATIONS=1`.
33
- - Do not reintroduce Stop-hook text-sniffing limit failsafes.
34
-
35
- See [references/policy.md](references/policy.md).
@@ -1,10 +0,0 @@
1
- # Policy sources
2
-
3
- - `docs/content/docs/switching.mdx`
4
- - `src/lib/decide.ts`, `src/lib/picker.ts`
5
- - `src/lib/bankedreset.ts`, `src/lib/codexreset.ts` (banked limit resets)
6
- - `.cursor/rules/switch-verification.md`
7
-
8
- Automatic organization preference requires session usage below `greedySessionFloor` and more than `max(1, session bar - greedySessionFloor)` points below every applicable bar.
9
-
10
- The exact policy, defaults, verification limits and manual-switch behavior live in `docs/content/docs/switching.mdx`.