tokenmaxxing 1.21.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.
- package/DESIGN.md +2 -2
- package/README.md +0 -26
- package/package.json +1 -7
- package/agent-plugin/bin/tokenmaxxing-mcp +0 -7
- package/agent-plugin/mcp.json +0 -10
- package/agent-plugin/plugin.json +0 -20
- package/agent-plugin/skills/codex-pool/SKILL.md +0 -23
- package/agent-plugin/skills/codex-pool/references/codex.md +0 -5
- package/agent-plugin/skills/credentials-hygiene/SKILL.md +0 -27
- package/agent-plugin/skills/credentials-hygiene/references/credentials.md +0 -6
- package/agent-plugin/skills/doctor-diagnostics/SKILL.md +0 -26
- package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +0 -5
- package/agent-plugin/skills/pool-status/SKILL.md +0 -27
- package/agent-plugin/skills/pool-status/references/commands.md +0 -9
- package/agent-plugin/skills/safe-contribution/SKILL.md +0 -27
- package/agent-plugin/skills/safe-contribution/references/ship.md +0 -5
- package/agent-plugin/skills/sdk-pairing/SKILL.md +0 -34
- package/agent-plugin/skills/sdk-pairing/references/sdk.md +0 -6
- package/agent-plugin/skills/switching-policy/SKILL.md +0 -34
- package/agent-plugin/skills/switching-policy/references/policy.md +0 -9
- package/src/entries/mcp.ts +0 -277
- package/src/sdk.ts +0 -55
package/DESIGN.md
CHANGED
|
@@ -72,7 +72,7 @@ Each terminal ran the supervisor, so each has its own child `claude` and its own
|
|
|
72
72
|
|
|
73
73
|
- **`tokenmaxxing init` imports the account you're already on - automatically, no prompts, no re-login.** It reads the live `Claude Code-credentials` keychain blob plus the `oauthAccount` object in `~/.claude.json` (email, `organizationUuid`, `accountUuid`, plan tier) and writes them as **account #1** into tokenmaxxing's store (`tokenmaxxing-cred-<accountUuid[:8]>` + an `accounts.json` index entry). Nothing about your current session changes - that account stays active; it's now just also a registered pool member. After this one command you already have a working (single-account) pool. `init` also installs the supervisor + the four settings entries.
|
|
74
74
|
- If the current auth is API-key mode (`ANTHROPIC_API_KEY`/`apiKeyHelper`) rather than a subscription `/login`, there's no quota-poolable subscription credential to import - `init` says so and points you to `/login` first (per-token API billing isn't what tokenmaxxing pools).
|
|
75
|
-
- **`tokenmaxxing add`** - registers *additional* accounts: logs one in via a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard` (your primary login untouched), harvests it into the store, deletes the temp dir + its namespaced item. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway isolated stores like this one: the same harvest serves `auth`, and parked-account `/usage` sampling probes under `sample/<item>` the same way; an AMBIENT `CLAUDE_CONFIG_DIR` is refused by every CLI command
|
|
75
|
+
- **`tokenmaxxing add`** - registers *additional* accounts: logs one in via a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard` (your primary login untouched), harvests it into the store, deletes the temp dir + its namespaced item. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway isolated stores like this one: the same harvest serves `auth`, and parked-account `/usage` sampling probes under `sample/<item>` the same way; an AMBIENT `CLAUDE_CONFIG_DIR` is refused by every CLI command.
|
|
76
76
|
- **`tokenmaxxing auth [sel | --all]`** - reauthenticates an *existing* pool member whose refresh token died (a needs-reauth account can never heal through a swap: the dead token is exactly what a swap would need). Same isolated-login harvest as `add`, but it states which email to sign in with and **requires the login to land on the target account** (harvested `accountUuid` must match, else nothing changes) - the credential write and the needs-reauth clear happen in one flock critical section so a concurrent swap's harvest cannot clobber the fresh backup. Bare `auth` lists the pool with emails and asks which; `--all` walks every flagged account one by one.
|
|
77
77
|
- Both commands exercise `security` reads/writes interactively (where a macOS keychain ACL prompt is acceptable), so the first access never happens cold inside a headless hook.
|
|
78
78
|
|
|
@@ -118,7 +118,7 @@ The [wall policy](docs/content/docs/switching.mdx#the-wall) defines Layer 2 reco
|
|
|
118
118
|
|
|
119
119
|
**v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
|
|
120
120
|
|
|
121
|
-
**
|
|
121
|
+
**Removed (1.23.0):** the programmatic SDK surface, the stdio MCP server, and the Agent Plugin that shipped from 0.11.0 through 1.22.0. The CLI, the hooks, and the supervisor are the only surfaces.
|
|
122
122
|
|
|
123
123
|
**Later:** tool-agnostic picker.
|
|
124
124
|
|
package/README.md
CHANGED
|
@@ -117,32 +117,6 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
117
117
|
|
|
118
118
|
State lives entirely in `~/.config/tokenmaxxing/`. Per-account credentials follow the platform's Claude Code store: the login keychain on macOS (`tokenmaxxing-cred-<uuid8>` items, never plaintext on disk), 0600 files under `~/.config/tokenmaxxing/creds/` on Linux (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
|
|
119
119
|
|
|
120
|
-
## Pairing with the Claude Agent SDK
|
|
121
|
-
|
|
122
|
-
For agents you build on the [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) against **your own** pooled accounts, `tokenmaxxing` is importable as a library (your agent app must run under Bun: tokenmaxxing ships TypeScript source and uses `bun:ffi`):
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
126
|
-
import { ensureBestAccount, pooledOptions, stopHookCheck } from "tokenmaxxing";
|
|
127
|
-
|
|
128
|
-
await ensureBestAccount();
|
|
129
|
-
|
|
130
|
-
for await (const message of query({
|
|
131
|
-
prompt: "...",
|
|
132
|
-
options: {
|
|
133
|
-
...pooledOptions(),
|
|
134
|
-
hooks: { Stop: [{ hooks: [stopHookCheck] }] },
|
|
135
|
-
},
|
|
136
|
-
})) {
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
The loop body receives the message stream; keep the session id from the init message if you want `resume` across swaps.
|
|
141
|
-
|
|
142
|
-
The SDK reads credentials when it spawns the claude subprocess and has no statusLine, so none of the CLI-side supervisor machinery applies; the integration is boundary-driven instead. `ensureBestAccount()` runs the exact greedy decision the CLI hooks and timer run (screening bars, pace-pressure target, post-swap cooldown - all shared code); like them, it deliberately does nothing until the decision engages (the active session past `policy.greedySessionFloor`, or a bar crossed), so a fresh account rides instead of churning. `pooledOptions()` pins `pathToClaudeCodeExecutable` to the real claude binary and supplies a full replacement `env` with every ambient credential override (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, ...) scrubbed, so the subprocess resolves the pool's live credential and nothing else. The pooled surface requires the default Claude Code credential store: it fails fast if `CLAUDE_CONFIG_DIR` or `CLAUDE_SECURESTORAGE_CONFIG_DIR` is set in your app's environment, because a swap would write the live credential where those point while the spawned subprocess reads the default store. `stopHookCheck` re-runs the decision at turn boundaries; a swap it lands takes effect on the next subprocess spawn (it never yanks a mid-query token). If your app loads user settings (see the SDK's `settingSources`), the Stop hook `tokenmaxxing init` installed may already fire in SDK sessions too - `stopHookCheck` makes the check explicit and works when settings are restricted.
|
|
143
|
-
|
|
144
|
-
This is for pooling **your own** subscription accounts in agents you run yourself - the same personal-use posture as the CLI. Anthropic does not allow third-party products to offer claude.ai login or rate limits, including agents built on the Agent SDK; don't ship this surface to third parties.
|
|
145
|
-
|
|
146
120
|
## Codex support
|
|
147
121
|
|
|
148
122
|
The same pooling works for OpenAI's Codex CLI (your own ChatGPT-subscription accounts):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,13 +13,8 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"tokenmaxxing": "./src/main.ts"
|
|
15
15
|
},
|
|
16
|
-
"exports": {
|
|
17
|
-
".": "./src/sdk.ts",
|
|
18
|
-
"./package.json": "./package.json"
|
|
19
|
-
},
|
|
20
16
|
"files": [
|
|
21
17
|
"src",
|
|
22
|
-
"agent-plugin",
|
|
23
18
|
"README.md",
|
|
24
19
|
"DESIGN.md"
|
|
25
20
|
],
|
|
@@ -41,7 +36,6 @@
|
|
|
41
36
|
"typescript": "^5.6.0"
|
|
42
37
|
},
|
|
43
38
|
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "1.29.0",
|
|
45
39
|
"es-toolkit": "^1.49.0",
|
|
46
40
|
"ky": "^2.0.2",
|
|
47
41
|
"zod": "^4.4.3"
|
package/agent-plugin/mcp.json
DELETED
package/agent-plugin/plugin.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
-
"name": "tokenmaxxing",
|
|
4
|
-
"version": "1.21.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,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,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,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,34 +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
|
-
|
|
21
|
-
## Layer 2 (Claude only)
|
|
22
|
-
|
|
23
|
-
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).
|
|
24
|
-
|
|
25
|
-
## Model matching
|
|
26
|
-
|
|
27
|
-
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`).
|
|
28
|
-
|
|
29
|
-
## Agent actions
|
|
30
|
-
|
|
31
|
-
- Explain with this skill; mutate only via MCP `pool_switch` / `pool_check` with user approval, `confirm=true`, and `TOKENMAXXING_AGENT_MUTATIONS=1`.
|
|
32
|
-
- Do not reintroduce Stop-hook text-sniffing limit failsafes.
|
|
33
|
-
|
|
34
|
-
See [references/policy.md](references/policy.md).
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Policy sources
|
|
2
|
-
|
|
3
|
-
- `docs/content/docs/switching.mdx`
|
|
4
|
-
- `src/lib/decide.ts`, `src/lib/picker.ts`
|
|
5
|
-
- `.cursor/rules/switch-verification.md`
|
|
6
|
-
|
|
7
|
-
Automatic organization preference requires session usage below `greedySessionFloor` and more than `max(1, session bar - greedySessionFloor)` points below every applicable bar.
|
|
8
|
-
|
|
9
|
-
The exact policy, defaults, verification limits and manual-switch behavior live in `docs/content/docs/switching.mdx`.
|
package/src/entries/mcp.ts
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { cmdLs } from "../cli/ls.ts";
|
|
7
|
-
import { cmdStatus } from "../cli/status.ts";
|
|
8
|
-
import { cmdDoctor } from "../cli/doctor.ts";
|
|
9
|
-
import { cmdConfig } from "../cli/config.ts";
|
|
10
|
-
import { cmdSwitch } from "../cli/switch.ts";
|
|
11
|
-
import { cmdCodexSwitch } from "../cli/codexswitch.ts";
|
|
12
|
-
import { cmdCheck } from "../cli/check.ts";
|
|
13
|
-
|
|
14
|
-
const MUTATIONS_ENV = "TOKENMAXXING_AGENT_MUTATIONS";
|
|
15
|
-
const PACKAGE_ROOT = join(import.meta.dir, "../..");
|
|
16
|
-
|
|
17
|
-
function packageVersion(): string {
|
|
18
|
-
try {
|
|
19
|
-
const raw = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")) as { version?: string };
|
|
20
|
-
return raw.version ?? "0.0.0";
|
|
21
|
-
} catch {
|
|
22
|
-
return "0.0.0";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function refuseAmbientStoreEnv(): string | null {
|
|
27
|
-
const nonEmpty = (v: string | undefined) => (v != null && v !== "" ? v : null);
|
|
28
|
-
return nonEmpty(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR) ?? nonEmpty(process.env.CLAUDE_CONFIG_DIR);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function scrubSecrets(text: string): string {
|
|
32
|
-
return text
|
|
33
|
-
.replace(/\b(Bearer\s+)[A-Za-z0-9._\-+/=]+/gi, "$1[redacted]")
|
|
34
|
-
.replace(/\b(sk-ant-[A-Za-z0-9_-]+)\b/g, "[redacted]")
|
|
35
|
-
.replace(/\b(accessToken|refreshToken|claudeAiOauth)\b\s*[:=]\s*["']?[^"'}\s,]+/gi, "$1=[redacted]");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type CaptureResult = { code: number; stdout: string; stderr: string };
|
|
39
|
-
|
|
40
|
-
let captureChain: Promise<unknown> = Promise.resolve();
|
|
41
|
-
|
|
42
|
-
export async function captureCli(run: () => number | Promise<number>): Promise<CaptureResult> {
|
|
43
|
-
const job = async (): Promise<CaptureResult> => {
|
|
44
|
-
const out: string[] = [];
|
|
45
|
-
const err: string[] = [];
|
|
46
|
-
const joinArgs = (args: unknown[]) => args.map((a) => (typeof a === "string" ? a : String(a))).join(" ");
|
|
47
|
-
const log = console.log;
|
|
48
|
-
const error = console.error;
|
|
49
|
-
console.log = (...args: unknown[]) => { out.push(joinArgs(args)); };
|
|
50
|
-
console.error = (...args: unknown[]) => { err.push(joinArgs(args)); };
|
|
51
|
-
try {
|
|
52
|
-
const code = await run();
|
|
53
|
-
return { code, stdout: out.join("\n"), stderr: err.join("\n") };
|
|
54
|
-
} finally {
|
|
55
|
-
console.log = log;
|
|
56
|
-
console.error = error;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
const next = captureChain.then(job, job);
|
|
60
|
-
captureChain = next.then(
|
|
61
|
-
() => undefined,
|
|
62
|
-
() => undefined,
|
|
63
|
-
);
|
|
64
|
-
return next;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function textResult(input: { text: string; isError?: boolean }) {
|
|
68
|
-
return {
|
|
69
|
-
content: [{ type: "text" as const, text: scrubSecrets(input.text) }],
|
|
70
|
-
...(input.isError ? { isError: true } : {}),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function formatCapture(cap: CaptureResult): string {
|
|
75
|
-
const parts: string[] = [];
|
|
76
|
-
if (cap.stdout) parts.push(cap.stdout);
|
|
77
|
-
if (cap.stderr) parts.push(cap.stderr);
|
|
78
|
-
parts.push(`exit ${cap.code}`);
|
|
79
|
-
return parts.join("\n").trimEnd();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function mutationsEnabled(): boolean {
|
|
83
|
-
return process.env[MUTATIONS_ENV] === "1";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function mutationDenied(confirm: boolean): string | null {
|
|
87
|
-
if (!confirm) {
|
|
88
|
-
return `Mutating tools require confirm=true. Also set ${MUTATIONS_ENV}=1 in the MCP server environment after the user approves.`;
|
|
89
|
-
}
|
|
90
|
-
if (!mutationsEnabled()) {
|
|
91
|
-
return `Mutations are disabled. Set ${MUTATIONS_ENV}=1 in the MCP server environment only after the user explicitly approves a pool mutation.`;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const HELP_TEXT = `tokenmaxxing agent MCP
|
|
97
|
-
|
|
98
|
-
Read tools (always available):
|
|
99
|
-
pool_ls list pooled Claude and Codex accounts (labels/status only)
|
|
100
|
-
pool_status sample usage bars (free /usage path; never --ping)
|
|
101
|
-
doctor verify install health (labels/status only)
|
|
102
|
-
config_get read effective config, or one key when provided
|
|
103
|
-
help this catalog
|
|
104
|
-
|
|
105
|
-
Mutating tools (confirm=true AND ${MUTATIONS_ENV}=1):
|
|
106
|
-
pool_switch Claude greedy/forced switch, or Codex when codex=true
|
|
107
|
-
pool_check one evaluate-and-maybe-swap pass
|
|
108
|
-
config_set write a config.json override
|
|
109
|
-
config_unset remove a config.json override
|
|
110
|
-
|
|
111
|
-
Hard deny (no tools):
|
|
112
|
-
status --ping / metered pings
|
|
113
|
-
init / add / auth / rm / uninstall
|
|
114
|
-
credential blobs or token values
|
|
115
|
-
killing sessions or supervisors
|
|
116
|
-
|
|
117
|
-
Prefer these tools over raw shell for pool ops. Honor TOKENMAXXING_HOME for hermetic use.
|
|
118
|
-
`;
|
|
119
|
-
|
|
120
|
-
export function createTokenmaxxingMcpServer(): McpServer {
|
|
121
|
-
const server = new McpServer({
|
|
122
|
-
name: "tokenmaxxing",
|
|
123
|
-
version: packageVersion(),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
server.registerTool(
|
|
127
|
-
"help",
|
|
128
|
-
{
|
|
129
|
-
description: "Catalog of tokenmaxxing MCP tools and hard safety rules. Use when deciding which pool tool to call.",
|
|
130
|
-
inputSchema: {},
|
|
131
|
-
},
|
|
132
|
-
async () => textResult({ text: HELP_TEXT }),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
server.registerTool(
|
|
136
|
-
"pool_ls",
|
|
137
|
-
{
|
|
138
|
-
description: "List pooled Claude and Codex accounts with active and needs-reauth flags. Labels and status only; never credential material.",
|
|
139
|
-
inputSchema: {},
|
|
140
|
-
},
|
|
141
|
-
async () => {
|
|
142
|
-
const cap = await captureCli(() => cmdLs());
|
|
143
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
144
|
-
},
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
server.registerTool(
|
|
148
|
-
"pool_status",
|
|
149
|
-
{
|
|
150
|
-
description: "Show pool usage bars via the free /usage path. Never opens 5h windows. Do not request --ping; that tool does not exist.",
|
|
151
|
-
inputSchema: {},
|
|
152
|
-
},
|
|
153
|
-
async () => {
|
|
154
|
-
const cap = await captureCli(() => cmdStatus());
|
|
155
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
156
|
-
},
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
server.registerTool(
|
|
160
|
-
"doctor",
|
|
161
|
-
{
|
|
162
|
-
description: "Verify supervisor, hooks, timer, and credential identity health. Reports labels and pass/fail only; never returns credential blobs.",
|
|
163
|
-
inputSchema: {},
|
|
164
|
-
},
|
|
165
|
-
async () => {
|
|
166
|
-
const cap = await captureCli(() => cmdDoctor());
|
|
167
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
168
|
-
},
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
server.registerTool(
|
|
172
|
-
"config_get",
|
|
173
|
-
{
|
|
174
|
-
description: "Read effective config with sources, or one dotted key when key is set (e.g. thresholds.session).",
|
|
175
|
-
inputSchema: {
|
|
176
|
-
key: z.string().optional().describe("Optional dotted config key; omit for the full effective table"),
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
async ({ key }) => {
|
|
180
|
-
const args = key ? ["get", key] : [];
|
|
181
|
-
const cap = await captureCli(() => cmdConfig(args));
|
|
182
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
server.registerTool(
|
|
187
|
-
"pool_switch",
|
|
188
|
-
{
|
|
189
|
-
description: "Switch the Claude pool (or Codex when codex=true). Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1. Hot-swaps live Claude; Codex takes effect on next start.",
|
|
190
|
-
inputSchema: {
|
|
191
|
-
confirm: z.boolean().describe("Must be true after the user approves the mutation"),
|
|
192
|
-
selector: z.string().optional().describe("Optional account selector; omit for greedy best"),
|
|
193
|
-
codex: z.boolean().optional().describe("When true, run the Codex pool switch instead"),
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
async ({ confirm, selector, codex }) => {
|
|
197
|
-
const denied = mutationDenied(confirm);
|
|
198
|
-
if (denied) return textResult({ text: denied, isError: true });
|
|
199
|
-
const cap = await captureCli(() => (codex ? cmdCodexSwitch(selector) : cmdSwitch(selector)));
|
|
200
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
201
|
-
},
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
server.registerTool(
|
|
205
|
-
"pool_check",
|
|
206
|
-
{
|
|
207
|
-
description: "Run one evaluate-and-maybe-swap pass (the periodic timer path). Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
|
|
208
|
-
inputSchema: {
|
|
209
|
-
confirm: z.boolean().describe("Must be true after the user approves the mutation"),
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
async ({ confirm }) => {
|
|
213
|
-
const denied = mutationDenied(confirm);
|
|
214
|
-
if (denied) return textResult({ text: denied, isError: true });
|
|
215
|
-
const cap = await captureCli(() => cmdCheck());
|
|
216
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
server.registerTool(
|
|
221
|
-
"config_set",
|
|
222
|
-
{
|
|
223
|
-
description: "Write a config.json override. Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
|
|
224
|
-
inputSchema: {
|
|
225
|
-
confirm: z.boolean().describe("Must be true after the user approves the mutation"),
|
|
226
|
-
key: z.string().describe("Dotted config key"),
|
|
227
|
-
value: z.string().describe("JSON or literal string value"),
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
async ({ confirm, key, value }) => {
|
|
231
|
-
const denied = mutationDenied(confirm);
|
|
232
|
-
if (denied) return textResult({ text: denied, isError: true });
|
|
233
|
-
const cap = await captureCli(() => cmdConfig(["set", key, value]));
|
|
234
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
235
|
-
},
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
server.registerTool(
|
|
239
|
-
"config_unset",
|
|
240
|
-
{
|
|
241
|
-
description: "Remove a config.json override. Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
|
|
242
|
-
inputSchema: {
|
|
243
|
-
confirm: z.boolean().describe("Must be true after the user approves the mutation"),
|
|
244
|
-
key: z.string().describe("Dotted config key"),
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
async ({ confirm, key }) => {
|
|
248
|
-
const denied = mutationDenied(confirm);
|
|
249
|
-
if (denied) return textResult({ text: denied, isError: true });
|
|
250
|
-
const cap = await captureCli(() => cmdConfig(["unset", key]));
|
|
251
|
-
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
252
|
-
},
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
return server;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export async function main(): Promise<void> {
|
|
259
|
-
const ambient = refuseAmbientStoreEnv();
|
|
260
|
-
if (ambient != null) {
|
|
261
|
-
console.error(
|
|
262
|
-
`CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set (${ambient}): the pooled MCP surface requires the default Claude Code credential store. Unset it and retry.`,
|
|
263
|
-
);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
const server = createTokenmaxxingMcpServer();
|
|
267
|
-
const transport = new StdioServerTransport();
|
|
268
|
-
await server.connect(transport);
|
|
269
|
-
console.error("tokenmaxxing MCP server running on stdio");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (import.meta.main) {
|
|
273
|
-
main().catch((e) => {
|
|
274
|
-
console.error(e instanceof Error ? e.message : String(e));
|
|
275
|
-
process.exit(1);
|
|
276
|
-
});
|
|
277
|
-
}
|
package/src/sdk.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { UNMANAGED_ENV, resolveRealClaude } from "./lib/claudebin.ts";
|
|
3
|
-
import { evaluateAndMaybeSwap, type SwapDecision } from "./lib/decide.ts";
|
|
4
|
-
import { CRED_ENV_OVERRIDES } from "./lib/usage.ts";
|
|
5
|
-
import { log } from "./lib/log.ts";
|
|
6
|
-
|
|
7
|
-
export { evaluateAndMaybeSwap };
|
|
8
|
-
export type { SwapDecision };
|
|
9
|
-
|
|
10
|
-
export async function ensureBestAccount(now = Date.now()): Promise<SwapDecision> {
|
|
11
|
-
return evaluateAndMaybeSwap(now, false);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function claudeExecutablePath(): string {
|
|
15
|
-
return resolveRealClaude();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const AMBIENT_STORE_VARS = ["CLAUDE_SECURESTORAGE_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
|
|
19
|
-
|
|
20
|
-
export function pooledSpawnEnv(): Record<string, string> {
|
|
21
|
-
for (const k of AMBIENT_STORE_VARS) {
|
|
22
|
-
if (process.env[k]) {
|
|
23
|
-
throw new Error(
|
|
24
|
-
`${k} is set: the pooled SDK surface requires the default Claude Code credential store (a swap writes the live credential where ${k} points, while the spawned subprocess reads the default store). Unset it in the process running tokenmaxxing.`,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const env: Record<string, string> = { ...process.env, [UNMANAGED_ENV]: "1" };
|
|
29
|
-
for (const k of CRED_ENV_OVERRIDES) delete env[k];
|
|
30
|
-
return env;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const PooledOptionsSchema = z.object({
|
|
34
|
-
pathToClaudeCodeExecutable: z.string(),
|
|
35
|
-
env: z.record(z.string(), z.string()),
|
|
36
|
-
});
|
|
37
|
-
export type PooledOptions = z.infer<typeof PooledOptionsSchema>;
|
|
38
|
-
|
|
39
|
-
export function pooledOptions(): PooledOptions {
|
|
40
|
-
return PooledOptionsSchema.parse({
|
|
41
|
-
pathToClaudeCodeExecutable: claudeExecutablePath(),
|
|
42
|
-
env: pooledSpawnEnv(),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function stopHookCheck(): Promise<Record<string, never>> {
|
|
47
|
-
try {
|
|
48
|
-
await evaluateAndMaybeSwap(Date.now(), false);
|
|
49
|
-
} catch (e) {
|
|
50
|
-
const err = e instanceof Error ? e.message : String(e);
|
|
51
|
-
console.error(`tokenmaxxing: switch check failed at turn boundary: ${err}`);
|
|
52
|
-
log("sdk.stop_error", { err });
|
|
53
|
-
}
|
|
54
|
-
return {};
|
|
55
|
-
}
|