tokenmaxxing 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +2 -4
- package/README.md +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +1 -8
- package/src/cli/auth.ts +0 -23
- package/src/cli/check.ts +19 -13
- package/src/cli/codexadd.ts +0 -17
- package/src/cli/codexinit.ts +0 -40
- package/src/cli/codexrm.ts +0 -13
- package/src/cli/codexswitch.ts +0 -15
- package/src/cli/config.ts +0 -30
- package/src/cli/doctor.ts +1 -14
- package/src/cli/init.ts +1 -33
- package/src/cli/ls.ts +0 -2
- package/src/cli/onboard.ts +0 -37
- package/src/cli/rename.ts +0 -19
- package/src/cli/render.ts +0 -23
- package/src/cli/rm.ts +0 -19
- package/src/cli/status.ts +0 -80
- package/src/cli/switch.ts +1 -49
- package/src/cli/watch.ts +0 -17
- package/src/entries/codexstophook.ts +2 -73
- package/src/entries/codexsupervisor.ts +1 -67
- package/src/entries/mcp.ts +0 -11
- package/src/entries/sessionstart.ts +1 -8
- package/src/entries/statusline.ts +0 -66
- package/src/entries/stopfailurehook.ts +93 -0
- package/src/entries/stophook.ts +3 -34
- package/src/entries/subagentstatusline.ts +0 -19
- package/src/entries/supervisor.ts +32 -132
- package/src/lib/atomic.ts +0 -16
- package/src/lib/claudebin.ts +4 -55
- package/src/lib/claudejson.ts +0 -10
- package/src/lib/claudelock.ts +13 -35
- package/src/lib/codexauth.ts +0 -29
- package/src/lib/codexbin.ts +0 -10
- package/src/lib/codexdecide.ts +1 -112
- package/src/lib/codexoauth.ts +0 -16
- package/src/lib/codexpick.ts +0 -31
- package/src/lib/codexpresence.ts +0 -35
- package/src/lib/codexsample.ts +0 -23
- package/src/lib/codexstate.ts +0 -7
- package/src/lib/codexswap.ts +0 -32
- package/src/lib/codexusage.ts +0 -28
- package/src/lib/credstore.ts +0 -24
- package/src/lib/decide.ts +127 -180
- package/src/lib/http.ts +0 -9
- package/src/lib/install.ts +6 -127
- package/src/lib/keychain.ts +1 -39
- package/src/lib/lock.ts +0 -24
- package/src/lib/log.ts +0 -14
- package/src/lib/oauth.ts +1 -31
- package/src/lib/paths.ts +1 -48
- package/src/lib/picker.ts +1 -84
- package/src/lib/proc.ts +0 -17
- package/src/lib/sample.ts +0 -68
- package/src/lib/sessions.ts +0 -13
- package/src/lib/settings.ts +15 -42
- package/src/lib/state.ts +23 -77
- package/src/lib/swap.ts +3 -87
- package/src/lib/tty.ts +0 -4
- package/src/lib/types.ts +13 -140
- package/src/lib/usage.ts +108 -196
- package/src/lib/worktree.ts +0 -8
- package/src/main.ts +5 -40
- package/src/sdk.ts +0 -59
- package/agent-plugin/agents/tokenmaxxing-claude.md +0 -43
- package/agent-plugin/agents/tokenmaxxing-codex.md +0 -40
- package/agent-plugin/hooks/cursor-relay.json +0 -14
- package/agent-plugin/skills/relay-session/SKILL.md +0 -118
- package/agent-plugin/skills/relay-session/references/ipc.md +0 -23
- package/src/cli/relay.ts +0 -323
- package/src/entries/relaypermission.ts +0 -105
- package/src/lib/relay/config.ts +0 -84
- package/src/lib/relay/decide.ts +0 -75
- package/src/lib/relay/gc.ts +0 -80
- package/src/lib/relay/install.ts +0 -143
- package/src/lib/relay/markers.ts +0 -148
- package/src/lib/relay/modes.ts +0 -82
- package/src/lib/relay/protocol.ts +0 -61
- package/src/lib/relay/registry.ts +0 -175
- package/src/lib/relay/tmux.ts +0 -109
- package/src/lib/relay/turn.ts +0 -137
- package/src/lib/relay/worker.ts +0 -141
package/src/sdk.ts
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
// Programmatic surface for pairing tokenmaxxing with the Claude Code Agent SDK
|
|
2
|
-
// (personal use across your own pooled accounts - user decision 2026-07-16).
|
|
3
|
-
//
|
|
4
|
-
// The Agent SDK spawns a claude CLI subprocess per query() and that subprocess
|
|
5
|
-
// reads credentials at spawn time: no statusLine tee, no supervisor, no
|
|
6
|
-
// mid-query hot-swap. So the integration is boundary-driven - run the switch
|
|
7
|
-
// decision BEFORE a spawn so it lands on the best account, and again at
|
|
8
|
-
// Stop-hook turn boundaries so the NEXT spawn does; a running subprocess keeps
|
|
9
|
-
// its snapshotted token either way, which is exactly the clean-boundary
|
|
10
|
-
// semantics the CLI supervisor enforces with markers.
|
|
11
|
-
//
|
|
12
|
-
// Nothing here imports the Agent SDK: the helpers return plain values that
|
|
13
|
-
// spread structurally into its Options, so tokenmaxxing keeps its exact
|
|
14
|
-
// dependency set (zod, es-toolkit, ky).
|
|
15
|
-
|
|
16
1
|
import { z } from "zod";
|
|
17
2
|
import { UNMANAGED_ENV, resolveRealClaude } from "./lib/claudebin.ts";
|
|
18
3
|
import { evaluateAndMaybeSwap, type SwapDecision } from "./lib/decide.ts";
|
|
@@ -22,49 +7,16 @@ import { log } from "./lib/log.ts";
|
|
|
22
7
|
export { evaluateAndMaybeSwap };
|
|
23
8
|
export type { SwapDecision };
|
|
24
9
|
|
|
25
|
-
/**
|
|
26
|
-
* Run the same greedy pace-pressure decision the hooks and check timer run
|
|
27
|
-
* (never anticipatory: there is no supervisor to pause an SDK session, so a
|
|
28
|
-
* depleted pre-park would yank it onto a known-blocked account for nothing).
|
|
29
|
-
* Call it right before query() so the subprocess spawns on the best account.
|
|
30
|
-
*/
|
|
31
10
|
export async function ensureBestAccount(now = Date.now()): Promise<SwapDecision> {
|
|
32
11
|
return evaluateAndMaybeSwap(now, false);
|
|
33
12
|
}
|
|
34
13
|
|
|
35
|
-
/** The pinned real claude binary, for Options.pathToClaudeCodeExecutable.
|
|
36
|
-
* Never the supervisor wrapper: an SDK subprocess is headless print mode, so
|
|
37
|
-
* the wrapper's respawn machinery buys nothing and only adds recursion risk. */
|
|
38
14
|
export function claudeExecutablePath(): string {
|
|
39
15
|
return resolveRealClaude();
|
|
40
16
|
}
|
|
41
17
|
|
|
42
|
-
/** The pooled surface requires the DEFAULT Claude Code credential store. An
|
|
43
|
-
* ambient config-dir override desyncs the two sides of a swap on Linux: the
|
|
44
|
-
* swap (running in THIS process) writes the live credential where these vars
|
|
45
|
-
* point (credDir() honors them), while the scrubbed subprocess reads the
|
|
46
|
-
* default store - so the subprocess silently runs on a stale or absent
|
|
47
|
-
* credential. Fail fast on both platforms rather than platform-split the
|
|
48
|
-
* behavior (adversarial review catch, 2026-07-16). */
|
|
49
18
|
const AMBIENT_STORE_VARS = ["CLAUDE_SECURESTORAGE_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
|
|
50
19
|
|
|
51
|
-
/**
|
|
52
|
-
* The env an SDK-spawned claude must run under to meter the POOLED live
|
|
53
|
-
* credential: every ambient credential override is scrubbed (claude honors
|
|
54
|
-
* them BEFORE its keychain/file lookup, so one inherited ANTHROPIC_API_KEY
|
|
55
|
-
* silently meters the wrong account), and the unmanaged-zone sentinel is set
|
|
56
|
-
* so a descendant invoking `claude`/`codex` through the on-PATH wrapper gets a
|
|
57
|
-
* passthrough to the real binary instead of nested supervision (or, before
|
|
58
|
-
* this sentinel existed, a depth-cap abort that broke legitimate nested runs
|
|
59
|
-
* like a repo's `bun run validate` invoking `claude -p`). The wrap depth is
|
|
60
|
-
* NOT preset: it keeps counting real wrapper entries, so a poisoned claudeBin
|
|
61
|
-
* pin below here still dies at the cap.
|
|
62
|
-
*
|
|
63
|
-
* Returns a FULL environment, not a patch: the Agent SDK's Options.env
|
|
64
|
-
* REPLACES the subprocess env rather than merging over process.env (verified
|
|
65
|
-
* against the official TS reference 2026-07-16), which is what makes deleting
|
|
66
|
-
* keys from this copy effective.
|
|
67
|
-
*/
|
|
68
20
|
export function pooledSpawnEnv(): Record<string, string> {
|
|
69
21
|
for (const k of AMBIENT_STORE_VARS) {
|
|
70
22
|
if (process.env[k]) {
|
|
@@ -84,7 +36,6 @@ const PooledOptionsSchema = z.object({
|
|
|
84
36
|
});
|
|
85
37
|
export type PooledOptions = z.infer<typeof PooledOptionsSchema>;
|
|
86
38
|
|
|
87
|
-
/** Options fragment to spread into the Agent SDK's Options. */
|
|
88
39
|
export function pooledOptions(): PooledOptions {
|
|
89
40
|
return PooledOptionsSchema.parse({
|
|
90
41
|
pathToClaudeCodeExecutable: claudeExecutablePath(),
|
|
@@ -92,16 +43,6 @@ export function pooledOptions(): PooledOptions {
|
|
|
92
43
|
});
|
|
93
44
|
}
|
|
94
45
|
|
|
95
|
-
/**
|
|
96
|
-
* Agent SDK Stop-hook callback (structurally matches HookCallback; the args
|
|
97
|
-
* are irrelevant here; `{}` is the documented no-op output). Runs the switch
|
|
98
|
-
* decision at the turn boundary; a swap landed here takes effect on the next
|
|
99
|
-
* subprocess spawn. Errors are caught LOUDLY (stderr + log), not rethrown:
|
|
100
|
-
* the SDK hooks reference states an unhandled exception can interrupt the
|
|
101
|
-
* agent (verified 2026-07-16), and aborting the caller's turn because a
|
|
102
|
-
* switch check failed costs more than riding out the current account. Call
|
|
103
|
-
* ensureBestAccount() directly where a broken pool should throw.
|
|
104
|
-
*/
|
|
105
46
|
export async function stopHookCheck(): Promise<Record<string, never>> {
|
|
106
47
|
try {
|
|
107
48
|
await evaluateAndMaybeSwap(Date.now(), false);
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tokenmaxxing-claude
|
|
3
|
-
description: Cheap shell-only relay to a durable Claude Code worker via tokenmaxxing relay. Use for long-running Claude sessions with permission pings back to main.
|
|
4
|
-
model: inherit
|
|
5
|
-
readonly: false
|
|
6
|
-
is_background: false
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Tokenmaxxing Claude relay
|
|
10
|
-
|
|
11
|
-
You are a thin shell. Do not edit files yourself. Do not spawn nested Task/Agent children. Only call `tokenmaxxing relay` (alias `xx relay`).
|
|
12
|
-
|
|
13
|
-
## Commands
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
tokenmaxxing relay turn --worker claude --permission-mode auto --cwd <dir> --session <uuid-or-omit> "<prompt>"
|
|
17
|
-
tokenmaxxing relay decide --session <uuid> --approve
|
|
18
|
-
tokenmaxxing relay decide --session <uuid> --deny
|
|
19
|
-
tokenmaxxing relay set-permission-mode --session <uuid> --permission-mode <mode>
|
|
20
|
-
tokenmaxxing relay status --session <uuid>
|
|
21
|
-
tokenmaxxing relay destroy --session <uuid>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Default permission mode is `auto`. Modes: `default` (alias `manual`), `acceptEdits`, `plan`, `auto`, `dontAsk`, `bypassPermissions`.
|
|
25
|
-
|
|
26
|
-
## Stdout protocol
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
session: <id>
|
|
30
|
-
permission-mode: <mode>
|
|
31
|
-
# turn output OR:
|
|
32
|
-
permission-needed: <requestId>
|
|
33
|
-
summary: <one line>
|
|
34
|
-
detail: <flagged action>
|
|
35
|
-
session: <id>
|
|
36
|
-
permission-mode: auto
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
On `permission-needed`, return that block to main and stop. Main decides (or auto-approves when Cursor Run Mode is Run Everything, or when Claude main is `bypassPermissions`). Then you run `relay decide` and wait.
|
|
40
|
-
|
|
41
|
-
## Finish hooks
|
|
42
|
-
|
|
43
|
-
Your host finish hook (`subagentStop` / `SubagentStop`) must run on every return, including permission-needed. A permission park is not a worker Stop.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tokenmaxxing-codex
|
|
3
|
-
description: Cheap shell-only relay to a durable Codex worker via tokenmaxxing relay. Claude permission-mode names map to Codex sandbox flags.
|
|
4
|
-
model: inherit
|
|
5
|
-
readonly: false
|
|
6
|
-
is_background: false
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Tokenmaxxing Codex relay
|
|
10
|
-
|
|
11
|
-
You are a thin shell. Do not edit files yourself. Do not spawn nested Task/Agent children. Only call `tokenmaxxing relay` (alias `xx relay`).
|
|
12
|
-
|
|
13
|
-
## Commands
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
tokenmaxxing relay turn --worker codex --permission-mode auto --cwd <dir> --session <uuid-or-omit> "<prompt>"
|
|
17
|
-
tokenmaxxing relay decide --session <uuid> --approve
|
|
18
|
-
tokenmaxxing relay decide --session <uuid> --deny
|
|
19
|
-
tokenmaxxing relay set-permission-mode --session <uuid> --permission-mode <mode>
|
|
20
|
-
tokenmaxxing relay status --session <uuid>
|
|
21
|
-
tokenmaxxing relay destroy --session <uuid>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
CLI still speaks Claude permission-mode names. Codex mapping:
|
|
25
|
-
|
|
26
|
-
| Mode | Codex flags |
|
|
27
|
-
|---|---|
|
|
28
|
-
| `plan` / `default` | `--sandbox read-only` + ask-for-approval on |
|
|
29
|
-
| `acceptEdits` | `--sandbox workspace-write` |
|
|
30
|
-
| `auto` | workspace-write + ask-for-approval |
|
|
31
|
-
| `dontAsk` | read-only + approval never |
|
|
32
|
-
| `bypassPermissions` | `--sandbox danger-full-access` + approval off |
|
|
33
|
-
|
|
34
|
-
## Stdout protocol
|
|
35
|
-
|
|
36
|
-
Same as the Claude relay agent. On `permission-needed`, return the block to main; then `relay decide`.
|
|
37
|
-
|
|
38
|
-
## Finish hooks
|
|
39
|
-
|
|
40
|
-
Host finish hooks run on every return including permission-needed. Worker Stop still runs only on real turn finish.
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"hooks": {
|
|
4
|
-
"tokenmaxxingRelay": {
|
|
5
|
-
"description": "tokenmaxxing-owned Cursor hook group for relay finish behavior. Host finish hooks must still run on every subagent return including permission-needed.",
|
|
6
|
-
"subagentStop": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "/usr/bin/true"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: relay-session
|
|
3
|
-
description: Run durable Claude Code or Codex workers through tokenmaxxing relay (tmux). Use when a host agent needs a long-lived pooled worker, permission pings to main, or tokenmaxxing-claude / tokenmaxxing-codex agents.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Relay session
|
|
7
|
-
|
|
8
|
-
Host agents stay cheap. They shell into `tokenmaxxing relay`, which owns a durable tmux worker (Claude Code or Codex via tokenmaxxing). High-churn concurrent sessions are expected.
|
|
9
|
-
|
|
10
|
-
Install templates: `tokenmaxxing relay install --target cursor|claude|all`.
|
|
11
|
-
|
|
12
|
-
## Claude worker permission modes
|
|
13
|
-
|
|
14
|
-
CLI: `claude --permission-mode <mode>` / `relay set-permission-mode --permission-mode <mode>`.
|
|
15
|
-
|
|
16
|
-
| Mode | What runs without asking |
|
|
17
|
-
|---|---|
|
|
18
|
-
| `default` (alias `manual`) | Reads only |
|
|
19
|
-
| `acceptEdits` | Reads, file edits, common filesystem cmds in cwd |
|
|
20
|
-
| `plan` | Reads (+ classifier-approved cmds when auto available) |
|
|
21
|
-
| `auto` | Everything with background safety classifier (**relay default**) |
|
|
22
|
-
| `dontAsk` | Only pre-approved tools; else deny |
|
|
23
|
-
| `bypassPermissions` | Everything (needs allow-dangerously flags) |
|
|
24
|
-
|
|
25
|
-
Claude `auto` is classifier-assisted autonomy, not "defer to main". Relay pings main only when the worker would still prompt.
|
|
26
|
-
|
|
27
|
-
## Cursor Run Modes (main only)
|
|
28
|
-
|
|
29
|
-
| Run Mode | Role for relay |
|
|
30
|
-
|---|---|
|
|
31
|
-
| Auto-review | Main decides each ping or escalates to the user |
|
|
32
|
-
| Allowlist | Main decides / escalates |
|
|
33
|
-
| Run Everything | Main auto-`relay decide --approve` on worker pings |
|
|
34
|
-
|
|
35
|
-
Do not probe undocumented APIs for main's Run Mode. Main states or infers it.
|
|
36
|
-
|
|
37
|
-
If Claude main is itself in `bypassPermissions`, auto-approve worker pings the same way.
|
|
38
|
-
|
|
39
|
-
## Codex mapping
|
|
40
|
-
|
|
41
|
-
| Claude mode | Codex flags |
|
|
42
|
-
|---|---|
|
|
43
|
-
| `plan` / `default` | `--sandbox read-only` + ask-for-approval on |
|
|
44
|
-
| `acceptEdits` | `--sandbox workspace-write` |
|
|
45
|
-
| `auto` | workspace-write + ask-for-approval |
|
|
46
|
-
| `dontAsk` | read-only + approval never |
|
|
47
|
-
| `bypassPermissions` | `--sandbox danger-full-access` + approval off |
|
|
48
|
-
|
|
49
|
-
## Shared stdout protocol
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
session: <id>
|
|
53
|
-
permission-mode: <mode>
|
|
54
|
-
# turn output OR:
|
|
55
|
-
permission-needed: <requestId>
|
|
56
|
-
summary: <one line>
|
|
57
|
-
detail: <flagged action>
|
|
58
|
-
session: <id>
|
|
59
|
-
permission-mode: auto
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Commands
|
|
63
|
-
|
|
64
|
-
| Command | Job |
|
|
65
|
-
|---|---|
|
|
66
|
-
| `relay turn` | Ensure session, send prompt, wait until turn-done or permission-needed |
|
|
67
|
-
| `relay decide` | Approve/deny pending ping; resume worker |
|
|
68
|
-
| `relay set-permission-mode` | Change live worker mode |
|
|
69
|
-
| `relay status` / `destroy` / `gc` | Inspect, tear down, reap |
|
|
70
|
-
| `relay install` / `relay config` | Host templates + relay.json |
|
|
71
|
-
|
|
72
|
-
Flags: `--worker claude|codex`, `--session`, `--cwd`, `--permission-mode`, prompt via argv/stdin.
|
|
73
|
-
|
|
74
|
-
## Finish hooks
|
|
75
|
-
|
|
76
|
-
1. Cheap relay host finish hooks (`subagentStop` / `SubagentStop`) fire on every return, including `permission-needed`.
|
|
77
|
-
2. Worker Stop / Codex Stop still run on real turn finish (pool logic unchanged).
|
|
78
|
-
3. Relay turn-done markers are additive under `$TOKENMAXXING_HOME/relay/turn-done/`. Permission park is not a fake worker Stop. Never write into `respawn/`.
|
|
79
|
-
|
|
80
|
-
## Cursor IPC matrix
|
|
81
|
-
|
|
82
|
-
| Contract | Relay support |
|
|
83
|
-
|---|---|
|
|
84
|
-
| Task / custom agent prompt | Primary in |
|
|
85
|
-
| Task final message | Primary out (delta or permission-needed) |
|
|
86
|
-
| Task resume / interrupt | Continue with decide / set-permission-mode / next turn |
|
|
87
|
-
| Foreground / background + completion notify | Prefer foreground under modes that still prompt |
|
|
88
|
-
| Frontmatter model / readonly / is_background / tools | Shell-only templates; configurable cheap model |
|
|
89
|
-
| `/name` delegate | Same agents |
|
|
90
|
-
| subagentStart / subagentStop (+ followup_message) | Optional hooks; finish hooks always run |
|
|
91
|
-
| Task preToolUse / postToolUse | Optional |
|
|
92
|
-
| UpdateCurrentStep | UI phases only, not permission channel |
|
|
93
|
-
| Nesting | Relay must not spawn Task children |
|
|
94
|
-
|
|
95
|
-
## Claude IPC matrix
|
|
96
|
-
|
|
97
|
-
| Contract | Relay support |
|
|
98
|
-
|---|---|
|
|
99
|
-
| Agent tool prompt / final result | Primary |
|
|
100
|
-
| SendMessage resume / mid-run | Primary continue path |
|
|
101
|
-
| Background + completion notify | Supported |
|
|
102
|
-
| Fork / `/subtask` | Supported if host uses it |
|
|
103
|
-
| Permission UI bubble | Orthogonal; do not suppress |
|
|
104
|
-
| PermissionRequest / Notification | Optional hooks; worker pings stay stdout protocol |
|
|
105
|
-
| SubagentStart / SubagentStop | Finish hooks always run on every return including pings |
|
|
106
|
-
| Pre/PostToolUse (incl. on Agent) | Optional |
|
|
107
|
-
| Agent teams mailbox / task list | Same stdout contract over team delivery |
|
|
108
|
-
| Cross-session SendMessage | Supported; peer messages never carry user authority |
|
|
109
|
-
| subagentStatusLine | UI only |
|
|
110
|
-
|
|
111
|
-
## Hard stops
|
|
112
|
-
|
|
113
|
-
- Never pattern-kill tmux. Destroy by exact session name only.
|
|
114
|
-
- Never print credentials or OAuth tokens.
|
|
115
|
-
- Never spawn nested Task/Agent from the cheap relay agent.
|
|
116
|
-
- Prefer `tokenmaxxing-claude` / `tokenmaxxing-codex` agents over ad-hoc shells.
|
|
117
|
-
|
|
118
|
-
See [references/ipc.md](references/ipc.md).
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Relay IPC checklist
|
|
2
|
-
|
|
3
|
-
## Cursor
|
|
4
|
-
|
|
5
|
-
- In: Task prompt, `/tokenmaxxing-claude`, `/tokenmaxxing-codex`
|
|
6
|
-
- Out: final message with stdout protocol
|
|
7
|
-
- Continue: Task resume after `relay decide` or `relay set-permission-mode`
|
|
8
|
-
- Finish: `subagentStop` always (including permission-needed returns)
|
|
9
|
-
- Optional: `subagentStart`, Task `preToolUse` / `postToolUse`
|
|
10
|
-
- Not a permission channel: `UpdateCurrentStep`, transcripts
|
|
11
|
-
|
|
12
|
-
## Claude Code
|
|
13
|
-
|
|
14
|
-
- In: Agent tool prompt
|
|
15
|
-
- Out: Agent final result with stdout protocol
|
|
16
|
-
- Continue: SendMessage / next Agent turn after decide
|
|
17
|
-
- Finish: `SubagentStop` always (including permission-needed returns)
|
|
18
|
-
- Optional: `PermissionRequest` / `Notification` on main; worker pings still use relay stdout
|
|
19
|
-
- UI only: `subagentStatusLine`
|
|
20
|
-
|
|
21
|
-
## Finish-hook rule
|
|
22
|
-
|
|
23
|
-
Permission park returns to main without faking a worker Stop. Worker Stop hooks still run when the worker actually finishes a turn and write additive `relay/turn-done/<session>` markers only.
|
package/src/cli/relay.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
// `tokenmaxxing relay` - host-agnostic durable tmux relay companion.
|
|
2
|
-
|
|
3
|
-
import { cwd } from "node:process";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { c } from "./render.ts";
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_RELAY_CONFIG,
|
|
8
|
-
loadRelayConfig,
|
|
9
|
-
mergeRelayConfigFile,
|
|
10
|
-
writeRelayConfig,
|
|
11
|
-
type RelayConfigFile,
|
|
12
|
-
} from "../lib/relay/config.ts";
|
|
13
|
-
import { runDecide } from "../lib/relay/decide.ts";
|
|
14
|
-
import { destroySession, gcSessions, statusRows } from "../lib/relay/gc.ts";
|
|
15
|
-
import { installRelayHosts, type InstallTarget } from "../lib/relay/install.ts";
|
|
16
|
-
import { parsePermissionMode, tryParsePermissionMode } from "../lib/relay/modes.ts";
|
|
17
|
-
import { runTurn } from "../lib/relay/turn.ts";
|
|
18
|
-
import { setLivePermissionMode } from "../lib/relay/worker.ts";
|
|
19
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
-
import { paths } from "../lib/paths.ts";
|
|
21
|
-
|
|
22
|
-
function printHelp(): void {
|
|
23
|
-
console.log(`${c.bold("tokenmaxxing relay")} - durable tmux workers for host agents
|
|
24
|
-
|
|
25
|
-
${c.cyan("relay turn")} [--worker claude|codex] [--session <uuid>] [--cwd <dir>]
|
|
26
|
-
[--permission-mode <mode>] [prompt...]
|
|
27
|
-
Ensure session, send prompt (argv or stdin), wait for turn-done or permission-needed
|
|
28
|
-
${c.cyan("relay decide")} --session <uuid> [--request <id>] --approve|--deny [--no-wait]
|
|
29
|
-
Approve or deny a pending permission ping; by default wait for the next marker
|
|
30
|
-
${c.cyan("relay set-permission-mode")} --session <uuid> --permission-mode <mode>
|
|
31
|
-
Change the live worker permission mode
|
|
32
|
-
${c.cyan("relay status")} [--session <uuid>]
|
|
33
|
-
Inspect relay sessions
|
|
34
|
-
${c.cyan("relay destroy")} --session <uuid>
|
|
35
|
-
Tear down one session (exact tmux name)
|
|
36
|
-
${c.cyan("relay gc")}
|
|
37
|
-
Reap dead or idle sessions (idleTtlMs)
|
|
38
|
-
${c.cyan("relay install")} [--target cursor|claude|all]
|
|
39
|
-
Write agent templates and merge tokenmaxxing-owned hook keys
|
|
40
|
-
${c.cyan("relay config")} [get|set|show]
|
|
41
|
-
Inspect or edit $TOKENMAXXING_HOME/relay.json
|
|
42
|
-
|
|
43
|
-
Permission modes (Claude worker): default|acceptEdits|plan|auto|dontAsk|bypassPermissions
|
|
44
|
-
Alias: manual → default. Default in relay.json: auto.
|
|
45
|
-
Codex maps those names onto --sandbox / --ask-for-approval.`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type FlagMap = {
|
|
49
|
-
worker?: "claude" | "codex";
|
|
50
|
-
session?: string;
|
|
51
|
-
cwd?: string;
|
|
52
|
-
permissionMode?: string;
|
|
53
|
-
request?: string;
|
|
54
|
-
approve?: boolean;
|
|
55
|
-
deny?: boolean;
|
|
56
|
-
noWait?: boolean;
|
|
57
|
-
target?: string;
|
|
58
|
-
positionals: string[];
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
function parseFlags(args: string[]): FlagMap {
|
|
62
|
-
const out: FlagMap = { positionals: [] };
|
|
63
|
-
for (let i = 0; i < args.length; i++) {
|
|
64
|
-
const a = args[i]!;
|
|
65
|
-
if (a === "--worker") {
|
|
66
|
-
const v = z.enum(["claude", "codex"]).parse(args[++i]);
|
|
67
|
-
out.worker = v;
|
|
68
|
-
} else if (a.startsWith("--worker=")) {
|
|
69
|
-
out.worker = z.enum(["claude", "codex"]).parse(a.slice("--worker=".length));
|
|
70
|
-
} else if (a === "--session") {
|
|
71
|
-
out.session = z.string().min(1).parse(args[++i]);
|
|
72
|
-
} else if (a.startsWith("--session=")) {
|
|
73
|
-
out.session = a.slice("--session=".length);
|
|
74
|
-
} else if (a === "--cwd") {
|
|
75
|
-
out.cwd = z.string().min(1).parse(args[++i]);
|
|
76
|
-
} else if (a.startsWith("--cwd=")) {
|
|
77
|
-
out.cwd = a.slice("--cwd=".length);
|
|
78
|
-
} else if (a === "--permission-mode") {
|
|
79
|
-
out.permissionMode = z.string().min(1).parse(args[++i]);
|
|
80
|
-
} else if (a.startsWith("--permission-mode=")) {
|
|
81
|
-
out.permissionMode = a.slice("--permission-mode=".length);
|
|
82
|
-
} else if (a === "--request") {
|
|
83
|
-
out.request = z.string().min(1).parse(args[++i]);
|
|
84
|
-
} else if (a.startsWith("--request=")) {
|
|
85
|
-
out.request = a.slice("--request=".length);
|
|
86
|
-
} else if (a === "--approve") {
|
|
87
|
-
out.approve = true;
|
|
88
|
-
} else if (a === "--deny") {
|
|
89
|
-
out.deny = true;
|
|
90
|
-
} else if (a === "--no-wait") {
|
|
91
|
-
out.noWait = true;
|
|
92
|
-
} else if (a === "--target") {
|
|
93
|
-
out.target = z.string().min(1).parse(args[++i]);
|
|
94
|
-
} else if (a.startsWith("--target=")) {
|
|
95
|
-
out.target = a.slice("--target=".length);
|
|
96
|
-
} else if (a === "--help" || a === "-h") {
|
|
97
|
-
out.positionals.push(a);
|
|
98
|
-
} else if (a.startsWith("-")) {
|
|
99
|
-
throw new Error(`unknown flag: ${a}`);
|
|
100
|
-
} else {
|
|
101
|
-
out.positionals.push(a);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function readPrompt(flags: FlagMap): Promise<string> {
|
|
108
|
-
if (flags.positionals.length > 0) return flags.positionals.join(" ");
|
|
109
|
-
if (process.stdin.isTTY) return "";
|
|
110
|
-
const chunks: Uint8Array[] = [];
|
|
111
|
-
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
|
|
112
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function cmdTurn(args: string[]): Promise<number> {
|
|
116
|
-
const flags = parseFlags(args);
|
|
117
|
-
if (flags.positionals.includes("--help") || flags.positionals.includes("-h")) {
|
|
118
|
-
printHelp();
|
|
119
|
-
return 0;
|
|
120
|
-
}
|
|
121
|
-
const mode = flags.permissionMode != null
|
|
122
|
-
? parsePermissionMode({ raw: flags.permissionMode })
|
|
123
|
-
: undefined;
|
|
124
|
-
const prompt = await readPrompt(flags);
|
|
125
|
-
const result = await runTurn({
|
|
126
|
-
sessionId: flags.session,
|
|
127
|
-
worker: flags.worker,
|
|
128
|
-
permissionMode: mode,
|
|
129
|
-
cwd: flags.cwd ?? cwd(),
|
|
130
|
-
prompt,
|
|
131
|
-
});
|
|
132
|
-
process.stdout.write(result.stdout);
|
|
133
|
-
return result.exitCode;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function cmdDecide(args: string[]): Promise<number> {
|
|
137
|
-
const flags = parseFlags(args);
|
|
138
|
-
if (flags.session == null) {
|
|
139
|
-
console.error(c.red("relay decide requires --session <uuid>"));
|
|
140
|
-
return 1;
|
|
141
|
-
}
|
|
142
|
-
if (flags.approve === true && flags.deny === true) {
|
|
143
|
-
console.error(c.red("pass only one of --approve or --deny"));
|
|
144
|
-
return 1;
|
|
145
|
-
}
|
|
146
|
-
if (flags.approve !== true && flags.deny !== true) {
|
|
147
|
-
console.error(c.red("relay decide requires --approve or --deny"));
|
|
148
|
-
return 1;
|
|
149
|
-
}
|
|
150
|
-
const result = await runDecide({
|
|
151
|
-
sessionId: flags.session,
|
|
152
|
-
requestId: flags.request,
|
|
153
|
-
approve: flags.approve === true,
|
|
154
|
-
wait: flags.noWait !== true,
|
|
155
|
-
cwd: flags.cwd ?? cwd(),
|
|
156
|
-
});
|
|
157
|
-
if (result.turn != null) {
|
|
158
|
-
process.stdout.write(result.turn.stdout);
|
|
159
|
-
return result.turn.exitCode;
|
|
160
|
-
}
|
|
161
|
-
console.log(`decision written: ${result.requestId} (${flags.approve ? "approve" : "deny"})`);
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function cmdSetPermissionMode(args: string[]): Promise<number> {
|
|
166
|
-
const flags = parseFlags(args);
|
|
167
|
-
if (flags.session == null || flags.permissionMode == null) {
|
|
168
|
-
console.error(c.red("relay set-permission-mode requires --session and --permission-mode"));
|
|
169
|
-
return 1;
|
|
170
|
-
}
|
|
171
|
-
const mode = parsePermissionMode({ raw: flags.permissionMode });
|
|
172
|
-
const entry = await setLivePermissionMode({ sessionId: flags.session, permissionMode: mode });
|
|
173
|
-
console.log(`session: ${entry.sessionId}`);
|
|
174
|
-
console.log(`permission-mode: ${entry.permissionMode}`);
|
|
175
|
-
return 0;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function cmdStatus(args: string[]): number {
|
|
179
|
-
const flags = parseFlags(args);
|
|
180
|
-
const rows = statusRows();
|
|
181
|
-
const filtered = flags.session != null
|
|
182
|
-
? rows.filter((r) => r.sessionId === flags.session)
|
|
183
|
-
: rows;
|
|
184
|
-
if (filtered.length === 0) {
|
|
185
|
-
console.log(c.dim("(no relay sessions)"));
|
|
186
|
-
return 0;
|
|
187
|
-
}
|
|
188
|
-
for (const row of filtered) {
|
|
189
|
-
const alive = row.tmuxAlive ? c.green("up") : c.red("down");
|
|
190
|
-
console.log(
|
|
191
|
-
`${row.sessionId} ${row.worker} ${row.permissionMode} ${row.state} tmux=${alive} ${row.tmuxName}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
return 0;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function cmdDestroy(args: string[]): Promise<number> {
|
|
198
|
-
const flags = parseFlags(args);
|
|
199
|
-
if (flags.session == null) {
|
|
200
|
-
console.error(c.red("relay destroy requires --session <uuid>"));
|
|
201
|
-
return 1;
|
|
202
|
-
}
|
|
203
|
-
const ok = await destroySession({ sessionId: flags.session });
|
|
204
|
-
if (!ok) {
|
|
205
|
-
console.error(c.red(`relay session not found: ${flags.session}`));
|
|
206
|
-
return 1;
|
|
207
|
-
}
|
|
208
|
-
console.log(`destroyed ${flags.session}`);
|
|
209
|
-
return 0;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function cmdGc(): Promise<number> {
|
|
213
|
-
const result = await gcSessions();
|
|
214
|
-
console.log(`reaped ${result.reaped.length}; kept ${result.kept.length}`);
|
|
215
|
-
for (const id of result.reaped) console.log(` reaped ${id}`);
|
|
216
|
-
return 0;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function cmdInstall(args: string[]): number {
|
|
220
|
-
const flags = parseFlags(args);
|
|
221
|
-
const target = z.enum(["cursor", "claude", "all"]).catch("all").parse(flags.target ?? "all") as InstallTarget;
|
|
222
|
-
const result = installRelayHosts({ target });
|
|
223
|
-
console.log(`agents: ${result.agentsWritten.length}`);
|
|
224
|
-
for (const p of result.agentsWritten) console.log(` ${p}`);
|
|
225
|
-
console.log(`skill: ${result.skillWritten ? "written" : "skipped"}`);
|
|
226
|
-
console.log(`hooks: ${result.hooksMerged.length}`);
|
|
227
|
-
for (const p of result.hooksMerged) console.log(` ${p}`);
|
|
228
|
-
return 0;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function cmdConfig(args: string[]): number {
|
|
232
|
-
const sub = args[0] ?? "show";
|
|
233
|
-
if (sub === "show" || sub === "get" && args[1] == null) {
|
|
234
|
-
const cfg = loadRelayConfig();
|
|
235
|
-
console.log(c.dim(`relay.json: ${paths.relayJson}`));
|
|
236
|
-
console.log(JSON.stringify(cfg, null, 2));
|
|
237
|
-
return 0;
|
|
238
|
-
}
|
|
239
|
-
if (sub === "get") {
|
|
240
|
-
const key = args[1];
|
|
241
|
-
if (key == null) {
|
|
242
|
-
console.error(c.red("relay config get <key>"));
|
|
243
|
-
return 1;
|
|
244
|
-
}
|
|
245
|
-
const cfg = loadRelayConfig() as Record<string, unknown>;
|
|
246
|
-
if (!(key in cfg)) {
|
|
247
|
-
console.error(c.red(`unknown key: ${key}`));
|
|
248
|
-
return 1;
|
|
249
|
-
}
|
|
250
|
-
console.log(JSON.stringify(cfg[key]));
|
|
251
|
-
return 0;
|
|
252
|
-
}
|
|
253
|
-
if (sub === "set") {
|
|
254
|
-
const key = args[1];
|
|
255
|
-
const valueText = args[2];
|
|
256
|
-
if (key == null || valueText == null) {
|
|
257
|
-
console.error(c.red("relay config set <key> <value>"));
|
|
258
|
-
return 1;
|
|
259
|
-
}
|
|
260
|
-
let value: unknown;
|
|
261
|
-
try {
|
|
262
|
-
value = JSON.parse(valueText);
|
|
263
|
-
} catch {
|
|
264
|
-
value = valueText;
|
|
265
|
-
}
|
|
266
|
-
if (key === "defaultPermissionMode") {
|
|
267
|
-
const mode = tryParsePermissionMode({ raw: String(value) });
|
|
268
|
-
if (mode == null) {
|
|
269
|
-
console.error(c.red(`invalid permission mode: ${value}`));
|
|
270
|
-
return 1;
|
|
271
|
-
}
|
|
272
|
-
value = mode;
|
|
273
|
-
}
|
|
274
|
-
const patch = { [key]: value } as RelayConfigFile;
|
|
275
|
-
const next = mergeRelayConfigFile({ patch });
|
|
276
|
-
console.log(`${key}: ${JSON.stringify((next as Record<string, unknown>)[key])}`);
|
|
277
|
-
return 0;
|
|
278
|
-
}
|
|
279
|
-
if (sub === "init") {
|
|
280
|
-
if (!existsSync(paths.relayJson)) {
|
|
281
|
-
writeRelayConfig({ file: {} });
|
|
282
|
-
console.log(`wrote defaults-capable ${paths.relayJson}`);
|
|
283
|
-
} else {
|
|
284
|
-
console.log(`already exists: ${paths.relayJson}`);
|
|
285
|
-
console.log(readFileSync(paths.relayJson, "utf8"));
|
|
286
|
-
}
|
|
287
|
-
console.log(c.dim(`effective defaultPermissionMode=${DEFAULT_RELAY_CONFIG.defaultPermissionMode}`));
|
|
288
|
-
return 0;
|
|
289
|
-
}
|
|
290
|
-
console.error(c.red(`unknown relay config subcommand: ${sub}`));
|
|
291
|
-
return 1;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export async function cmdRelay(args: string[]): Promise<number> {
|
|
295
|
-
const sub = args[0];
|
|
296
|
-
if (sub == null || sub === "--help" || sub === "-h" || sub === "help") {
|
|
297
|
-
printHelp();
|
|
298
|
-
return 0;
|
|
299
|
-
}
|
|
300
|
-
const rest = args.slice(1);
|
|
301
|
-
switch (sub) {
|
|
302
|
-
case "turn":
|
|
303
|
-
return cmdTurn(rest);
|
|
304
|
-
case "decide":
|
|
305
|
-
return cmdDecide(rest);
|
|
306
|
-
case "set-permission-mode":
|
|
307
|
-
return cmdSetPermissionMode(rest);
|
|
308
|
-
case "status":
|
|
309
|
-
return cmdStatus(rest);
|
|
310
|
-
case "destroy":
|
|
311
|
-
return cmdDestroy(rest);
|
|
312
|
-
case "gc":
|
|
313
|
-
return cmdGc();
|
|
314
|
-
case "install":
|
|
315
|
-
return cmdInstall(rest);
|
|
316
|
-
case "config":
|
|
317
|
-
return cmdConfig(rest);
|
|
318
|
-
default:
|
|
319
|
-
console.error(c.red(`unknown relay command: ${sub}`));
|
|
320
|
-
printHelp();
|
|
321
|
-
return 2;
|
|
322
|
-
}
|
|
323
|
-
}
|