tokenmaxxing 1.130.0 → 1.132.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 +3 -3
- package/README.md +7 -7
- package/package.json +1 -1
- package/src/cli/auth.ts +1 -1
- package/src/cli/key.ts +147 -0
- package/src/cli/rename.ts +1 -1
- package/src/cli/seat.ts +0 -6
- package/src/cli/status.ts +50 -11
- package/src/entries/statusline.ts +10 -0
- package/src/entries/stopfailurehook.ts +24 -6
- package/src/entries/stophook.ts +7 -6
- package/src/entries/supervisor.ts +105 -34
- package/src/lib/apikeys.ts +142 -0
- package/src/lib/claude.ts +25 -7
- package/src/lib/compact.ts +3 -4
- package/src/lib/decide.ts +27 -6
- package/src/lib/paths.ts +3 -19
- package/src/lib/pi.ts +1 -0
- package/src/lib/presence.ts +6 -5
- package/src/lib/provider.ts +9 -2
- package/src/lib/sessions.ts +9 -4
- package/src/lib/state.ts +1 -2
- package/src/lib/statusonly.ts +1 -1
- package/src/lib/supervise.ts +2 -2
- package/src/lib/types.ts +30 -1
- package/src/lib/usage.ts +1 -1
- package/src/main.ts +25 -22
- package/src/lib/opencodego.ts +0 -99
package/DESIGN.md
CHANGED
|
@@ -30,7 +30,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
|
|
|
30
30
|
- A `claude` **supervisor** on your PATH ahead of the real binary (`~/.config/tokenmaxxing/bin/claude`) - you invoke it identically; a `claude` shell function or alias would shadow it, and `doctor` warns about one.
|
|
31
31
|
- Five `~/.claude/settings.json` entries (merged - other settings keys are preserved, but the `statusLine` slot is taken over): the tokenmaxxing `statusLine` renderer (native since 2026-07-11; it also tees usage), a `subagentStatusLine` (per-subagent rows in the agents panel), a `Stop` hook, a `StopFailure` hook, a `SessionStart` hook.
|
|
32
32
|
- **`~/.config/tokenmaxxing/`** - the single home for config and state:
|
|
33
|
-
- `config.json` - SPARSE overrides only (thresholds.session/weekly/accounts, claudeBin/codexBin/grokBin/
|
|
33
|
+
- `config.json` - SPARSE overrides only (thresholds.session/weekly/accounts, claudeBin/codexBin/grokBin/piBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
|
|
34
34
|
- `accounts.json` - non-secret index `{id, label, email, tier, addedAt, windows, lastUsageAt, lastProbeAt, probeFails, storeFails, usageRetryAt, enforcedUntil, needsReauth, oauthAccount}` (each cached window carries its name, duration, reset, and sample time; the same shape serves the Codex pool as `codex-accounts.json`). Neither index names an active account: the schema is `{version, accounts}`, and an older file that still carries `activeId` loads and drops the key on the next save.
|
|
35
35
|
- `stores/<uuid8>/` - one Claude Code credential store per pooled account (see below).
|
|
36
36
|
- `codex-stores/<uuid8>/` - one Codex credential store per pooled account: its `auth.json`, with everything else symlinked to the shared `~/.codex` so sessions, history, and config stay shared and `codex resume` works across stores.
|
|
@@ -40,7 +40,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
|
|
|
40
40
|
- `wait-queue.json` - the claims of supervised sessions that are moving or waiting (`{sessionId, accountId, at, waitUntil}`), naming the account each relaunches on: placement counts a claimed session there, and at most eight sessions wait on one account; a session that arrives when every reset within `policy.maxWaitMs` is full waits for the next reset with room up to twice `policy.maxWaitMs` out, joining the least-crowded reset when the horizon is full.
|
|
41
41
|
- `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
|
|
42
42
|
- `pi-stores/claude/<uuid8>/` and `pi-stores/codex/<uuid8>/` - one pi store per pooled account that has a pi login: the account's own pi grant in `auth.json`, everything else symlinked to the shared `~/.pi/agent` ([pi support](docs/content/docs/pi.mdx)).
|
|
43
|
-
- `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*`
|
|
43
|
+
- `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` state of the status-only grok pool: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
|
|
44
44
|
- Per-account **credential stores** follow the platform's Claude Code store: macOS = a login-keychain item keyed by the store path (never plaintext on disk); Linux = a 0600 `.credentials.json` inside the 0700 store directory (the same plaintext model claude itself uses - its Linux build has no keyring path at all, binary-verified 2.1.205). One `credstore` facade dispatches on a `{kind: keychain|file}` target; call sites never branch on platform. tokenmaxxing writes a store once, at onboarding; Claude Code's own refresh is the only writer afterwards.
|
|
45
45
|
|
|
46
46
|
- A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): it runs the decision with no seat (nothing moves), folds fresh tees into the index, samples up to three accounts whose last sample attempt is oldest (the direct no-spend usage GET), deletes leftover atomic-write temp files under the state root, and on a Bun global install checks npm for a newer version once a day, so the cached figures every placement reads stay fresh.
|
|
@@ -118,7 +118,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
|
|
|
118
118
|
|
|
119
119
|
**Removed (1.37.0, issue #79):** the shared live credential and everything that served it: the swap-time harvest, park, and install, tokenmaxxing's own OAuth refresh grant and its interlock with Claude Code's refresh lock, the `~/.claude.json` `oauthAccount` rewrite, the 45-second post-swap cooldown, the pool-wide depleted-wait record, the parked-credential slots (`creds/`, `tokenmaxxing-cred-*`), the ambient `CLAUDE_CONFIG_DIR` refusal, and manual `switch` for the Claude pool.
|
|
120
120
|
|
|
121
|
-
**Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, per-account `codex-stores/`, own flock). Since 1.35.0 the engine is one: a generic account record (`id`, `label`, `email`, `tier`, `windows`), one picker, one decision, and one set of account commands run against a `Provider` (`src/lib/provider.ts`); `src/lib/claude.ts` and `src/lib/codex.ts` hold only what differs per product (credential store, identity, usage source, presence, login and install flows), and the provider's `seats` capability (`shared` for Claude, `live` for Codex) is how the engine knows whether present accounts stay candidates and whether a move needs the caller's respawn. Codex differences that shaped it: restart IS the switch (a running codex refuses another account's credential), one store per pooled account with `CODEX_HOME` set per session (everything but `auth.json` symlinked to the shared home, so resume works across stores), usage is a free direct GET with epoch resets and a duration-classified window set (the weekly window is primary on current plans), the refresh token rotates with reuse punished (persisted into the account's own store the instant it returns, never refreshed for an account running in another session), and the codex Stop hook drives the auto-switch through a codex supervisor shim that respawns `codex resume <session-id>` under the target's store (hooks must be trusted once via `/hooks`).
|
|
121
|
+
**Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, per-account `codex-stores/`, own flock). Since 1.35.0 the engine is one: a generic account record (`id`, `label`, `email`, `tier`, `windows`), one picker, one decision, and one set of account commands run against a `Provider` (`src/lib/provider.ts`); `src/lib/claude.ts` and `src/lib/codex.ts` hold only what differs per product (credential store, identity, usage source, presence, login and install flows), and the provider's `seats` capability (`shared` for Claude, `live` for Codex) is how the engine knows whether present accounts stay candidates and whether a move needs the caller's respawn. Codex differences that shaped it: restart IS the switch (a running codex refuses another account's credential), one store per pooled account with `CODEX_HOME` set per session (everything but `auth.json` symlinked to the shared home, so resume works across stores), usage is a free direct GET with epoch resets and a duration-classified window set (the weekly window is primary on current plans), the refresh token rotates with reuse punished (persisted into the account's own store the instant it returns, never refreshed for an account running in another session), and the codex Stop hook drives the auto-switch through a codex supervisor shim that respawns `codex resume <session-id>` under the target's store (hooks must be trusted once via `/hooks`). The status-only grok pool shares the engine's account commands and state layout with `statusOnly: true` and no supervisor or hooks ([Status-only pools](docs/content/docs/status-only-pools.mdx)). grok `status` reads the weekly subscription credit window from the credits billing GET.
|
|
122
122
|
|
|
123
123
|
**Removed (issue #173):** the Cursor Cloud relay and its setup tokens: `tokenmaxxing setup-token`, `tokenmaxxing cursor init`, `tokenmaxxing cloud run`, `setup-tokens.json`, `TOKENMAXXING_TOKENS`, and the `cloud/` state.
|
|
124
124
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Automatic Claude Code account switching.** Run `claude` exactly as you always do; each session starts on the pooled account with the most headroom per running session, and when that account nears its usage limit, tokenmaxxing resumes the session on a fresher account at a safe turn boundary - same conversation, compacted on the account it leaves and restarted under another account's credential store with a first prompt that asks Claude to continue. Works across many concurrent sessions, which spread over the pool instead of draining one account together. When the whole pool is at its limit, a session queues first come first served for a reset (at most eight sessions wait on one account; overflow waits for the next reset with room) and pauses with a countdown until its assigned reset, when that reset falls within twice `policy.maxWaitMs` (default 1h - a longer wait stays put rather than parking your terminal for hours).
|
|
4
4
|
|
|
5
|
-
> **Scope:** Claude Code on macOS and Linux, plus OpenAI's Codex CLI as a second switching pool; grok
|
|
5
|
+
> **Scope:** Claude Code on macOS and Linux, plus OpenAI's Codex CLI as a second switching pool; grok as a status-only pool. It pools **subscription** accounts (Pro/Max), not API keys.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
$ claude
|
|
@@ -66,15 +66,15 @@ claude # use claude as always; each session gets its own accoun
|
|
|
66
66
|
| `tokenmaxxing init` | verify and pin the real `claude`, log the first account in (isolated) + install supervisor, hooks, PATH line, check timer, and hub service |
|
|
67
67
|
| `tokenmaxxing init --codex` | same for codex: log in the first account (isolated), install codex supervisor + Stop hook |
|
|
68
68
|
| `tokenmaxxing init --pi` | verify and pin the real `pi` and install the `pi` supervisor, which runs pi sessions on pooled Claude and ChatGPT accounts that have a pi login |
|
|
69
|
-
| `tokenmaxxing init --grok`
|
|
70
|
-
| `tokenmaxxing add [--codex \| --grok
|
|
71
|
-
| `tokenmaxxing auth [--codex \| --grok
|
|
69
|
+
| `tokenmaxxing init --grok` | pool the first grok login, status-only: no supervisor, no switching |
|
|
70
|
+
| `tokenmaxxing add [--codex \| --grok]` | register an additional account in that pool (isolated login, harvested once into its own store) |
|
|
71
|
+
| `tokenmaxxing auth [--codex \| --grok] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every account that is flagged or has no usable credential in its store, one by one |
|
|
72
72
|
| `tokenmaxxing auth --pi [--codex] [sel \| --all]` | log a pooled Claude account (or a codex account with `--codex`) into pi, isolated, and keep that login as the account's pi credential after an identity check; `--all` walks every account without a pi login |
|
|
73
73
|
| `tokenmaxxing status [--cached]` | every pool: accounts with 5h / weekly / per-model usage bars, live session counts, exhausted-until-reset; `--cached` renders the stored figures without sampling |
|
|
74
74
|
| `tokenmaxxing config` | the config path and the effective values; edit the file in an editor, a bad value fails the next load with the field name |
|
|
75
75
|
| `tokenmaxxing check` | fold fresh tees, sample up to three stale accounts, and self-update a Bun global install once a day; the periodic timer runs this every tick |
|
|
76
76
|
| `tokenmaxxing doctor` | verify the install: PATH order, `claudeBin`, the five settings entries, the timer, the hub service, the credential identity of every Codex store and of every Claude store whose access token is still fresh (an expiring Claude token is reported as unverifiable, not as a failure), codex hook trust, and shell aliases that shadow `claude` |
|
|
77
|
-
| `tokenmaxxing rename [--codex \| --grok
|
|
77
|
+
| `tokenmaxxing rename [--codex \| --grok] <sel> <label>` / `rm [...] <sel>` | manage a pool (one email can hold both a claude and a codex account); `rm` is refused for an account with a running supervised session |
|
|
78
78
|
| `tokenmaxxing serve` | serve the CLIProxyAPI-compatible usage API on `http://localhost:8317` (`hub.port`) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows the quota of every pooled Claude and Codex account; the management key is the contents of `hub-key` in the state directory, minted on the first run, and `init` runs it as a background service beside the check timer. Details in [Usage hub](docs/content/docs/hub.mdx) |
|
|
79
79
|
| `tokenmaxxing uninstall [--yes]` | print the targets, then remove the shims, settings entries, codex Stop hook, check timer, hub service, and rc PATH line (accounts/stores kept); refused without `--yes` when `HOME` is the login home |
|
|
80
80
|
| `--json` | machine-readable output: one JSON document on stdout for `status`, `config`, and `check` (`ok` mirrors the exit code, failures add `error`) |
|
|
@@ -112,7 +112,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left, and a session on it past its bars moves to such an account; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick. `claudeBin`, `codexBin`, `grokBin`,
|
|
115
|
+
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left, and a session on it past its bars moves to such an account; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick. `claudeBin`, `codexBin`, `grokBin`, and `piBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, and `TOKENMAXXING_PI_BIN` override them for one process, except that `init` pins the binary it resolved into `config.json`.
|
|
116
116
|
|
|
117
117
|
State lives in `~/.config/tokenmaxxing/`; outside it the install touches Claude Code's `settings.json`, codex's `hooks.json`, the timer and usage hub units, and your shell rc. Each account's credential store is `stores/<uuid8>/`, which Claude Code reads and refreshes as its own: on macOS the credential is a login-keychain item keyed by the store path (never plaintext on disk), on Linux a 0600 `.credentials.json` inside the store (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
|
|
118
118
|
|
|
@@ -151,7 +151,7 @@ pi keeps its own credential and refreshes it itself, so each pooled account gets
|
|
|
151
151
|
- **One shared identity file.** `~/.claude.json` holds one `oauthAccount` that Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email; the statusline's `◆` marks the account a session actually uses.
|
|
152
152
|
- **Keychain (macOS).** The first keychain access for a new store happens inside the interactive `init`, `add`, or `auth` run, where a prompt can be answered, not inside a headless hook.
|
|
153
153
|
- **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
|
|
154
|
-
- **
|
|
154
|
+
- **The status-only grok pool does not switch.** grok `status` shows the weekly subscription credit window, and no grok session moves.
|
|
155
155
|
|
|
156
156
|
## How it's built
|
|
157
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.132.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/auth.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { usageNote } from "./add.ts";
|
|
|
11
11
|
import { c, count } from "./render.ts";
|
|
12
12
|
import type { Account, AccountsIndex } from "../lib/types.ts";
|
|
13
13
|
|
|
14
|
-
const AUTH_USAGE = "usage: tokenmaxxing auth [--codex | --grok
|
|
14
|
+
const AUTH_USAGE = "usage: tokenmaxxing auth [--codex | --grok] [<email|label|id> | --all]\n tokenmaxxing auth --pi [--codex] [<email|label|id> | --all]";
|
|
15
15
|
|
|
16
16
|
export type AuthPlan = { kind: "usage" } | { kind: "error"; message: string } | { kind: "pick" } | { kind: "targets"; ids: string[] };
|
|
17
17
|
|
package/src/cli/key.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { keySessions, loadApiKeys, removeApiKeySecret, saveApiKeys, writeApiKeySecret } from "../lib/apikeys.ts";
|
|
3
|
+
import { withLock } from "../lib/lock.ts";
|
|
4
|
+
import { claudePool } from "../lib/paths.ts";
|
|
5
|
+
import { readStdin } from "../lib/proc.ts";
|
|
6
|
+
import type { ApiKey } from "../lib/types.ts";
|
|
7
|
+
import { c, emitError, plain } from "./render.ts";
|
|
8
|
+
|
|
9
|
+
const USAGE = "usage: tokenmaxxing key add <label> [credit-usd] [workspace-id] (the key on stdin) | key credit <label> <usd> | key rm <label>";
|
|
10
|
+
|
|
11
|
+
const UsdSchema = z.coerce.number().finite().min(0);
|
|
12
|
+
|
|
13
|
+
const WorkspaceIdSchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.refine((id) => id.trim() === id && !id.includes("\n") && !id.includes("\r"), "a workspace id is one line with no surrounding whitespace");
|
|
17
|
+
|
|
18
|
+
export function usd(amount: number): string {
|
|
19
|
+
return `$${amount.toFixed(2)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findKey(keys: ApiKey[], label: string): ApiKey | undefined {
|
|
23
|
+
return keys.find((k) => k.label.toLowerCase() === label.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseUsd(raw: string): number | null {
|
|
27
|
+
const parsed = UsdSchema.safeParse(raw);
|
|
28
|
+
if (parsed.success) return parsed.data;
|
|
29
|
+
emitError({ message: `"${raw}" is not a dollar amount (a number at or above 0)` });
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function add(args: string[]): Promise<number> {
|
|
34
|
+
const [label, creditRaw, workspaceRaw] = args;
|
|
35
|
+
if (!label) {
|
|
36
|
+
emitError({ message: USAGE, paint: plain });
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
const credit = creditRaw == null ? undefined : parseUsd(creditRaw);
|
|
40
|
+
if (credit === null) return 2;
|
|
41
|
+
const workspace = workspaceRaw == null ? undefined : WorkspaceIdSchema.safeParse(workspaceRaw);
|
|
42
|
+
if (workspace != null && !workspace.success) {
|
|
43
|
+
emitError({ message: `"${workspaceRaw}" is not a workspace id: ${workspace.error.issues[0]?.message ?? "invalid"}` });
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
const workspaceId = workspace?.data;
|
|
47
|
+
if (process.stdin.isTTY) {
|
|
48
|
+
emitError({ message: "refusing to read the API key from a terminal, which would echo it - pipe it in: read -rs KEY && printf '%s' \"$KEY\" | tokenmaxxing key add <label> ...; unset KEY" });
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const secret = (await readStdin()).trim();
|
|
52
|
+
if (secret === "") {
|
|
53
|
+
emitError({ message: "no API key on stdin - pipe the key in, for example from your password manager" });
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
if (secret.includes("\n") || secret.includes("\r")) {
|
|
57
|
+
emitError({ message: "stdin holds more than one line - pass one API key per `key add`" });
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
if (secret.startsWith("sk-ant-admin")) {
|
|
61
|
+
emitError({ message: "that is an Admin API key, which cannot run inference - create a regular API key in the Console" });
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
return withLock(claudePool.lockFile, () => {
|
|
65
|
+
const idx = loadApiKeys();
|
|
66
|
+
const taken = findKey(idx.keys, label);
|
|
67
|
+
if (taken) {
|
|
68
|
+
emitError({ message: `label "${label}" is already used by another API key - labels must be unique` });
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
let id = crypto.randomUUID().slice(0, 8);
|
|
72
|
+
while (idx.keys.some((k) => k.id === id)) id = crypto.randomUUID().slice(0, 8);
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
writeApiKeySecret(id, secret);
|
|
75
|
+
idx.keys.push({
|
|
76
|
+
id,
|
|
77
|
+
label,
|
|
78
|
+
...(workspaceId != null ? { workspaceId } : {}),
|
|
79
|
+
...(credit != null ? { creditUsd: credit } : {}),
|
|
80
|
+
spentUsd: 0,
|
|
81
|
+
creditSetAt: now,
|
|
82
|
+
addedAt: now,
|
|
83
|
+
});
|
|
84
|
+
saveApiKeys(idx);
|
|
85
|
+
console.log(`added API key ${c.bold(label)} (${credit != null ? `credit ${usd(credit)}` : "credit unknown, ranks after keys with a balance"}${workspaceId != null ? `, workspace ${workspaceId}` : ""})`);
|
|
86
|
+
return 0;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function credit(args: string[]): Promise<number> {
|
|
91
|
+
const [label, amountRaw] = args;
|
|
92
|
+
if (!label || amountRaw == null || args.length > 2) {
|
|
93
|
+
emitError({ message: USAGE, paint: plain });
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const amount = parseUsd(amountRaw);
|
|
97
|
+
if (amount === null) return 2;
|
|
98
|
+
return withLock(claudePool.lockFile, () => {
|
|
99
|
+
const idx = loadApiKeys();
|
|
100
|
+
const key = findKey(idx.keys, label);
|
|
101
|
+
if (!key) {
|
|
102
|
+
emitError({ message: `no API key is labeled "${label}"` });
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
key.creditUsd = amount;
|
|
106
|
+
key.spentUsd = 0;
|
|
107
|
+
key.creditSetAt = Date.now();
|
|
108
|
+
key.refusedAt = undefined;
|
|
109
|
+
saveApiKeys(idx);
|
|
110
|
+
console.log(`set ${c.bold(key.label)} credit to ${usd(amount)}`);
|
|
111
|
+
return 0;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function rm(args: string[]): Promise<number> {
|
|
116
|
+
const [label] = args;
|
|
117
|
+
if (!label || args.length > 1) {
|
|
118
|
+
emitError({ message: USAGE, paint: plain });
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
return withLock(claudePool.lockFile, () => {
|
|
122
|
+
const idx = loadApiKeys();
|
|
123
|
+
const key = findKey(idx.keys, label);
|
|
124
|
+
if (!key) {
|
|
125
|
+
emitError({ message: `no API key is labeled "${label}"` });
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (keySessions().has(key.id)) {
|
|
129
|
+
emitError({ message: `${key.label} is running in a live session - close that session before removing the key.` });
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
removeApiKeySecret(key.id);
|
|
133
|
+
idx.keys = idx.keys.filter((k) => k.id !== key.id);
|
|
134
|
+
saveApiKeys(idx);
|
|
135
|
+
console.log(`removed API key ${c.bold(key.label)} (${idx.keys.length} left)`);
|
|
136
|
+
return 0;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function cmdKey(args: string[]): Promise<number> {
|
|
141
|
+
const [action, ...rest] = args;
|
|
142
|
+
if (action === "add") return add(rest);
|
|
143
|
+
if (action === "credit") return credit(rest);
|
|
144
|
+
if (action === "rm") return rm(rest);
|
|
145
|
+
emitError({ message: USAGE, paint: plain });
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
package/src/cli/rename.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function findAccount(accounts: Account[], selector: string): Account | un
|
|
|
16
16
|
export async function cmdRename(p: Provider, argv: string[]): Promise<number> {
|
|
17
17
|
const [selector, newLabel] = argv;
|
|
18
18
|
if (!selector || !newLabel) {
|
|
19
|
-
emitError({ message: "usage: tokenmaxxing rename [--codex | --grok
|
|
19
|
+
emitError({ message: "usage: tokenmaxxing rename [--codex | --grok] <email|label|id> <new-label>", paint: plain });
|
|
20
20
|
return 2;
|
|
21
21
|
}
|
|
22
22
|
return withLock(p.pool.lockFile, async () => {
|
package/src/cli/seat.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { borrowClaudeSeat } from "../lib/claude.ts";
|
|
2
2
|
import { borrowCodexSeat } from "../lib/codex.ts";
|
|
3
|
-
import { borrowOpencodeGoSeat } from "../lib/opencodego.ts";
|
|
4
3
|
import { emitError } from "./render.ts";
|
|
5
4
|
|
|
6
5
|
const POOLS = {
|
|
@@ -14,11 +13,6 @@ const POOLS = {
|
|
|
14
13
|
usage: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account, lent to <pid> until it exits and shared with codex sessions and other borrowers; codex processes that share one auth.json can lose a refresh race that signs the account out until `tokenmaxxing auth --codex` runs again",
|
|
15
14
|
none: "no usable codex account (pool empty, or every account at a limit, flagged for reauthentication, held by a pi session, or held by the supervised codex session this runs in) - use the ambient codex login",
|
|
16
15
|
},
|
|
17
|
-
"opencode-go": {
|
|
18
|
-
borrow: borrowOpencodeGoSeat,
|
|
19
|
-
usage: "usage: tokenmaxxing seat --opencode-go <pid> - print the store directory of one pooled opencode-go key, whose auth.json is an opencode auth file, lent to <pid> until it exits and shared with other borrowers",
|
|
20
|
-
none: "no usable opencode-go key (pool empty, or no store holds a usable key)",
|
|
21
|
-
},
|
|
22
16
|
};
|
|
23
17
|
|
|
24
18
|
export async function cmdSeat(pool: keyof typeof POOLS, pidRaw: string | undefined, extra: string[]): Promise<number> {
|
package/src/cli/status.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { chunk, sortBy } from "es-toolkit";
|
|
2
|
+
import { creditLeft, keySessions, loadApiKeys } from "../lib/apikeys.ts";
|
|
2
3
|
import { claude } from "../lib/claude.ts";
|
|
3
4
|
import { codex } from "../lib/codex.ts";
|
|
4
5
|
import { grok } from "../lib/grok.ts";
|
|
5
|
-
import { opencodeGo } from "../lib/opencodego.ts";
|
|
6
6
|
import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
|
|
7
7
|
import { withLock } from "../lib/lock.ts";
|
|
8
|
-
import { codexPool, grokPool
|
|
8
|
+
import { codexPool, grokPool } from "../lib/paths.ts";
|
|
9
9
|
import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
10
10
|
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
11
|
+
import { usd } from "./key.ts";
|
|
11
12
|
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
12
13
|
import type { Account, AccountThresholds, Bars, Config, Thresholds, Window } from "../lib/types.ts";
|
|
13
14
|
|
|
@@ -39,7 +40,48 @@ type PoolReport = {
|
|
|
39
40
|
accounts: StatusAccount[];
|
|
40
41
|
};
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
type StatusApiKey = {
|
|
44
|
+
label: string;
|
|
45
|
+
id: string;
|
|
46
|
+
workspaceId: string | null;
|
|
47
|
+
creditUsd: number | null;
|
|
48
|
+
spentUsd: number;
|
|
49
|
+
creditLeftUsd: number | null;
|
|
50
|
+
refused: boolean;
|
|
51
|
+
refusedAt: number | null;
|
|
52
|
+
sessions: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; apiKeys?: StatusApiKey[] };
|
|
56
|
+
|
|
57
|
+
function apiKeyReport(): StatusApiKey[] {
|
|
58
|
+
const sessions = keySessions();
|
|
59
|
+
return loadApiKeys().keys.map((k) => ({
|
|
60
|
+
label: k.label,
|
|
61
|
+
id: k.id,
|
|
62
|
+
workspaceId: k.workspaceId ?? null,
|
|
63
|
+
creditUsd: k.creditUsd ?? null,
|
|
64
|
+
spentUsd: k.spentUsd,
|
|
65
|
+
creditLeftUsd: creditLeft(k),
|
|
66
|
+
refused: k.refusedAt != null,
|
|
67
|
+
refusedAt: k.refusedAt ?? null,
|
|
68
|
+
sessions: sessions.get(k.id) ?? 0,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderApiKeys(keys: StatusApiKey[]): void {
|
|
73
|
+
console.log(c.dim(`api keys (${count({ n: keys.length, noun: "key" })}, used only while every claude account is at its limit)`));
|
|
74
|
+
console.log();
|
|
75
|
+
for (const k of keys) {
|
|
76
|
+
const badges: string[] = [];
|
|
77
|
+
if (k.sessions > 0) badges.push(c.green(count({ n: k.sessions, noun: "session" })));
|
|
78
|
+
if (k.refused) badges.push(c.red("refused"));
|
|
79
|
+
else if (k.creditLeftUsd != null && k.creditLeftUsd <= 0) badges.push(c.yellow("spent"));
|
|
80
|
+
const credit = k.creditLeftUsd == null ? c.dim("credit unknown") : `credit left ${usd(k.creditLeftUsd)}`;
|
|
81
|
+
console.log(`${k.sessions > 0 ? c.green("●") : c.dim("○")} ${c.bold(k.label)} ${credit}${badges.length ? ` ${badges.join(" ")}` : ""}`);
|
|
82
|
+
}
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
43
85
|
|
|
44
86
|
function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): WindowReport {
|
|
45
87
|
const passed = w.resetsAt != null && w.resetsAt <= now;
|
|
@@ -260,13 +302,13 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
260
302
|
const cfg = loadConfig();
|
|
261
303
|
const now = Date.now();
|
|
262
304
|
const claudeReport = await collect(claude, cfg, now, cached);
|
|
305
|
+
const apiKeys = apiKeyReport();
|
|
263
306
|
if (!json) {
|
|
264
307
|
const codexPooled = loadAccounts(codexPool).accounts.length > 0;
|
|
265
308
|
const grokPooled = loadAccounts(grokPool).accounts.length > 0;
|
|
266
|
-
const opencodeGoPooled = loadAccounts(opencodeGoPool).accounts.length > 0;
|
|
267
309
|
if (claudeReport.accounts.length === 0) {
|
|
268
|
-
if (!codexPooled && !grokPooled &&
|
|
269
|
-
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok
|
|
310
|
+
if (!codexPooled && !grokPooled && apiKeys.length === 0) {
|
|
311
|
+
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`)"));
|
|
270
312
|
return 0;
|
|
271
313
|
}
|
|
272
314
|
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
@@ -275,12 +317,12 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
275
317
|
const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% bars 5h ${claudeReport.bars.session}% weekly ${claudeReport.bars.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
|
|
276
318
|
renderPool(claude, claudeReport, header, Date.now(), cfg.policy.usagePollTtlMs);
|
|
277
319
|
}
|
|
320
|
+
if (apiKeys.length > 0) renderApiKeys(apiKeys);
|
|
278
321
|
}
|
|
279
322
|
const codexReport = await collect(codex, cfg, now, cached);
|
|
280
323
|
const grokReport = await collect(grok, cfg, now, cached);
|
|
281
|
-
const opencodeGoReport = await collect(opencodeGo, cfg, now, cached);
|
|
282
324
|
if (json) {
|
|
283
|
-
const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport,
|
|
325
|
+
const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport, ...(apiKeys.length > 0 ? { apiKeys } : {}) };
|
|
284
326
|
emitJson({ ok: true, ...report });
|
|
285
327
|
return 0;
|
|
286
328
|
}
|
|
@@ -290,8 +332,5 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
290
332
|
if (grokReport.accounts.length > 0) {
|
|
291
333
|
renderPool(grok, grokReport, `grok (${count({ n: grokReport.accounts.length, noun: "account" })}, status-only)`, Date.now(), cfg.policy.usagePollTtlMs);
|
|
292
334
|
}
|
|
293
|
-
if (opencodeGoReport.accounts.length > 0) {
|
|
294
|
-
renderPool(opencodeGo, opencodeGoReport, `opencode-go (${count({ n: opencodeGoReport.accounts.length, noun: "account" })}, status-only)`, Date.now(), cfg.policy.usagePollTtlMs);
|
|
295
|
-
}
|
|
296
335
|
return 0;
|
|
297
336
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { sortBy } from "es-toolkit";
|
|
3
|
+
import { writeSessionCost } from "../lib/apikeys.ts";
|
|
2
4
|
import { claudePool, seatFromEnv } from "../lib/paths.ts";
|
|
5
|
+
import { supervisedSession } from "../lib/sessions.ts";
|
|
3
6
|
import { errorMessage } from "../lib/log.ts";
|
|
4
7
|
import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
|
|
5
8
|
import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
|
|
@@ -8,6 +11,7 @@ import { readStdin } from "../lib/proc.ts";
|
|
|
8
11
|
import { worktreeName } from "../lib/worktree.ts";
|
|
9
12
|
import { fmtResetShort, makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
|
|
10
13
|
import {
|
|
14
|
+
CostLineSchema,
|
|
11
15
|
JsonTextSchema,
|
|
12
16
|
StatusLineStdinSchema,
|
|
13
17
|
type Account,
|
|
@@ -127,6 +131,12 @@ export async function runStatusline(): Promise<number> {
|
|
|
127
131
|
}
|
|
128
132
|
} catch {
|
|
129
133
|
}
|
|
134
|
+
try {
|
|
135
|
+
const session = existsSync(claudePool.apiKeysJson) ? supervisedSession() : null;
|
|
136
|
+
const cost = session == null ? null : (CostLineSchema.safeParse(obj).data ?? null);
|
|
137
|
+
if (session != null && cost != null && cost.sessionId === session.live) writeSessionCost(session.sid, cost);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
130
140
|
|
|
131
141
|
let line: string;
|
|
132
142
|
try {
|
|
@@ -2,9 +2,10 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { claude } from "../lib/claude.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { liveApiKeyId, refuseApiKey } from "../lib/apikeys.ts";
|
|
6
|
+
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
6
7
|
import { readStdin } from "../lib/proc.ts";
|
|
7
|
-
import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
8
|
+
import { adoptLiveSession, keyMovesSupported, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
8
9
|
import { loadConfig } from "../lib/state.ts";
|
|
9
10
|
import { classifyEnforcedLimit, ENFORCED_ERRORS, findEnforcedRow, parseErrorBody, readTranscriptTail, type TranscriptRow } from "../lib/usage.ts";
|
|
10
11
|
import { paths } from "../lib/paths.ts";
|
|
@@ -60,6 +61,22 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
60
61
|
return 0;
|
|
61
62
|
}
|
|
62
63
|
const canRespawn = session != null && mainLoop;
|
|
64
|
+
const keyId = account == null ? liveApiKeyId() : null;
|
|
65
|
+
if (keyId != null) {
|
|
66
|
+
if (error !== "billing_error") return 0;
|
|
67
|
+
const known = await refuseApiKey(keyId, now);
|
|
68
|
+
log("stopfailure.key_refused", { key: keyId, known, subagent: !mainLoop });
|
|
69
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, null, { waiterId: canRespawn ? session?.sid : undefined, refused: refusedAccounts(), apiKeys: true, apiKeyId: keyId });
|
|
70
|
+
const target = moveTarget(decision);
|
|
71
|
+
if (session && canRespawn && target) {
|
|
72
|
+
writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? now, compact: false, origin: "stopfailure" });
|
|
73
|
+
log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil ?? now });
|
|
74
|
+
} else {
|
|
75
|
+
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (error === "billing_error") return 0;
|
|
63
80
|
const cfg = loadConfig();
|
|
64
81
|
const row = stdin.transcript_path
|
|
65
82
|
? await awaitEnforcedRow({ transcriptPath: stdin.transcript_path, agentId: stdin.agent_id, error, lastAssistantMessage: stdin.last_assistant_message })
|
|
@@ -92,10 +109,11 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
const refused = enforced?.kind === "credits" ? [...new Set([...refusedAccounts(), enforced.account])] : undefined;
|
|
95
|
-
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined, exclude: refused });
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined, exclude: refused, apiKeys: canRespawn && keyMovesSupported() });
|
|
113
|
+
const target = moveTarget(decision);
|
|
114
|
+
if (session && canRespawn && target) {
|
|
115
|
+
writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? now, compact: false, origin: "stopfailure", refused });
|
|
116
|
+
log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil ?? now });
|
|
99
117
|
} else {
|
|
100
118
|
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
101
119
|
}
|
package/src/entries/stophook.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
|
-
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
|
+
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
4
4
|
import { readStdin } from "../lib/proc.ts";
|
|
5
|
-
import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { adoptLiveSession, keyMovesSupported, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
6
6
|
import { JsonTextSchema } from "../lib/types.ts";
|
|
7
7
|
import { errorMessage, log } from "../lib/log.ts";
|
|
8
8
|
|
|
@@ -24,10 +24,11 @@ export async function runBoundaryHook(event: "stop" | "sessionstart"): Promise<n
|
|
|
24
24
|
log(`${event}.nested_session`, { stdin: stdin.session_id?.slice(0, 8), supervised: supervised.live.slice(0, 8), source: stdin.source });
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
|
-
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts() });
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts(), apiKeys: session != null && keyMovesSupported() });
|
|
28
|
+
const target = moveTarget(decision);
|
|
29
|
+
if (session && target) {
|
|
30
|
+
writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? Date.now(), compact: true, origin: event });
|
|
31
|
+
log(`${event}.${target.waitUntil !== undefined ? "wait" : "move"}`, { source: stdin.source, account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil, session: session.sid.slice(0, 8), live: session.live.slice(0, 8) });
|
|
31
32
|
}
|
|
32
33
|
} catch (e) {
|
|
33
34
|
log(`${event}.error`, { err: errorMessage(e) });
|