Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,68 @@ bun add -g tokenmaxx
tokenmaxx #starts the dashboard
```

### Run in the background and start at login

The dashboard connects to a separate background manager. You can close the dashboard or its terminal and keep using your AI clients. To start the manager without opening the dashboard:

```bash
tokenmaxx daemon start
```

To route your clients and have macOS start the manager automatically whenever you log in:

```bash
tokenmaxx install --autostart
```

For pi, use `tokenmaxx install pi --autostart`. If your clients are already configured, add login startup on its own:

```bash
tokenmaxx daemon install
```

Run this as your normal macOS user, without `sudo`. It starts the manager immediately and installs a per-user LaunchAgent that restarts it if it exits. After restarting your Mac, it starts when you log in, when your login Keychain is available. It does not run while the Mac is asleep or shut down.

The background item is named **tokenmaxx** in System Settings → General → Login Items & Extensions. A small launcher app provides that name; launching Bun directly can make macOS display Bun's signing-certificate owner, such as “Jarred Sumner,” instead.

```bash
tokenmaxx daemon status # manager health and login startup configuration
tokenmaxx daemon stop # stop now; startup remains installed for the next login
tokenmaxx daemon start # start again under macOS supervision
tokenmaxx daemon disable # remove login startup and stop the manager
```

Commands that need the manager, including opening the dashboard, start it again after `daemon stop`. `daemon disable` removes automatic startup while retaining your accounts and data for manual use.

### Uninstall completely

```bash
tokenmaxx uninstall
```

This stops the manager, restores Codex, Claude, and pi routing, removes the LaunchAgent and launcher app, deletes tokenmaxx's Keychain credentials (including orphaned entries), and removes its account database, usage history, preferences, logs, isolated profiles, and saved setup files. It then asks the owning global package manager—Bun, npm, pnpm, or Yarn—to remove the CLI package. The manager is not restarted.

Setup records the client settings it replaces. Uninstall restores the original files when they are unchanged, removes files and empty client directories created by setup, and preserves unrelated settings and subsequent user edits. Older installations without these records can have their managed routing removed, but previously overwritten settings cannot be recovered. Native client logins, unrelated files, and other packages are preserved. When run from a source checkout, the checkout is kept.

To restore routing while keeping tokenmaxx installed, use `tokenmaxx uninstall routing`. `tokenmaxx uninstall pi` restores only pi routing. These commands do not delete saved accounts or usage history.

If cleanup fails, the command reports the failed step and keeps the remaining recovery data for a retry. Package removal happens only after setup cleanup succeeds. If the package manager cannot be identified, the command reports that the package still needs removal.

### Startup files and troubleshooting

The installer creates:

- `~/Library/LaunchAgents/sh.tokenmaxx.daemon.plist`
- `~/Applications/tokenmaxx.app` (the background launcher)

Logs remain in `~/.tokenmaxx/runtime/daemon.log`. `tokenmaxx doctor` also reports whether login startup is installed.

Set `TOKENMAXX_HOME`, `TOKENMAXX_PROXY_PORT`, and any custom `CODEX_HOME`, `CLAUDE_CONFIG_DIR`, or `PI_CODING_AGENT_DIR` before installing startup. These settings and the executable search path are saved for the background service; unrelated environment variables and API keys are not copied. One startup configuration is supported per macOS user. Use the same `TOKENMAXX_HOME` when managing it, and remove the previous startup configuration before installing one for a different directory.

The launcher uses absolute paths to Bun and the installed tokenmaxx entrypoint. Re-run `tokenmaxx daemon install` after moving or reinstalling either tool to refresh those paths. Install from a permanent package location, rather than a temporary `bunx` download or development checkout that you intend to delete.

If startup fails, check `tokenmaxx daemon status`, `tokenmaxx doctor`, and the daemon log. Confirm that the tokenmaxx background item is allowed in System Settings. Running `tokenmaxx daemon install` again refreshes the configuration and retries startup; it may briefly interrupt requests while restarting the manager.

## What it does

You run a fleet of coding agents using multiple Codex or Claude accounts:
Expand Down Expand Up @@ -75,8 +137,11 @@ A single loopback proxy on `127.0.0.1:8459`, and the clients you already use.
```text
tokenmaxx live dashboard
tokenmaxx login <codex|claude> sign in; isolated, idempotent
tokenmaxx install route native codex & claude
tokenmaxx uninstall restore native config
tokenmaxx install [pi] [--autostart] route clients; optionally start at login
tokenmaxx uninstall remove all setup, data, credentials, and the global package
tokenmaxx uninstall <routing|pi> restore routing while keeping tokenmaxx installed
tokenmaxx daemon start | stop | status manage the background manager
tokenmaxx daemon install | disable add or remove macOS login startup
tokenmaxx switch <codex|claude> <email> make an account active
tokenmaxx logout [codex|claude] <email> sign out; the credential is deleted
tokenmaxx auto <both|codex|claude> <on|off> [--threshold N]
Expand Down
8 changes: 7 additions & 1 deletion src/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,20 @@ export async function removeClaudeProfile(
profilePath: string,
dependencies: ClaudeLoginDependencies = defaultClaudeLoginDependencies()
): Promise<void> {
await dependencies.captured([
const result = await dependencies.captured([
'security',
'delete-generic-password',
'-a',
currentUser(),
'-s',
cliKeychainService(profilePath)
])
if (result.exitCode !== 0 && result.exitCode !== 44) {
throw new ApplicationError(
'KEYCHAIN_DELETE_FAILED',
'Could not remove the isolated Claude profile credential'
)
}
await rm(profilePath, { force: true, recursive: true })
}

Expand Down
51 changes: 51 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, test } from 'bun:test'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { stripTerminalNoise } from './cli.ts'
import { managerAvailable } from './ipc.ts'
import { applicationPaths } from './paths.ts'

describe('stripTerminalNoise', () => {
test('terminal chatter never survives into a typed answer', () => {
Expand All @@ -9,3 +14,49 @@ describe('stripTerminalNoise', () => {
expect(stripTerminalNoise(' plain-key-42 ')).toBe('plain-key-42')
})
})

test('status and invalid setup arguments do not start a manager or configure clients', async () => {
const directory = await mkdtemp(join(tmpdir(), 'tmx-cli-'))
const reservation = Bun.listen({ hostname: '127.0.0.1', port: 0, socket: { data() {} } })
const paths = applicationPaths({
TOKENMAXX_HOME: join(directory, 'state'),
TOKENMAXX_PROXY_PORT: String(reservation.port)
})
reservation.stop()
try {
for (const args of [
['daemon', 'status'],
['install', 'unknown', '--autostart']
]) {
const child = Bun.spawn([process.execPath, join(import.meta.dir, 'index.ts'), ...args], {
env: {
...process.env,
CLAUDE_CONFIG_DIR: join(directory, 'claude'),
CODEX_HOME: join(directory, 'codex'),
TOKENMAXX_HOME: paths.root,
TOKENMAXX_PROXY_PORT: String(paths.proxyPort)
},
stderr: 'pipe',
stdout: 'pipe'
})
const [stdout, stderr, exitCode] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited
])
if (args[0] === 'daemon') {
expect(exitCode).toBe(0)
expect(stdout).toContain('Login startup: not installed')
expect(stdout).toContain('stopped')
} else {
expect(exitCode).toBe(1)
expect(stderr).toContain('Usage: tokenmaxx install [pi] [--autostart]')
}
expect(await managerAvailable(paths.managerSocket)).toBe(false)
expect(await Bun.file(join(directory, 'codex', 'config.toml')).exists()).toBe(false)
expect(await Bun.file(join(directory, 'claude', 'settings.json')).exists()).toBe(false)
}
} finally {
await rm(directory, { force: true, recursive: true })
}
})
Loading
Loading