@event4u/agent-switch 2.1.0 → 2.3.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/README.md +166 -73
- package/dist/activity.js +99 -0
- package/dist/args.js +23 -3
- package/dist/auth-status.js +159 -0
- package/dist/balance.js +55 -0
- package/dist/browsers.js +90 -0
- package/dist/daemon.js +95 -42
- package/dist/desktop-sessions.js +115 -0
- package/dist/desktop-settings.js +90 -0
- package/dist/dir-resolve.js +178 -0
- package/dist/doctor.js +239 -69
- package/dist/gui-settings.js +67 -0
- package/dist/history.js +20 -0
- package/dist/hooks.js +21 -0
- package/dist/index.js +879 -68
- package/dist/launchers.js +228 -0
- package/dist/locks.js +61 -0
- package/dist/mappings.js +48 -9
- package/dist/pools.js +157 -0
- package/dist/profiles.js +181 -67
- package/dist/protection.js +100 -0
- package/dist/rebind.js +115 -12
- package/dist/service.js +40 -9
- package/dist/sessions.js +82 -13
- package/dist/share.js +92 -13
- package/dist/shellenv.js +122 -21
- package/dist/single-load.js +5 -0
- package/dist/token-freshen.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,31 +1,88 @@
|
|
|
1
|
-
<p align="center"><a href="https://event4u.app"><img alt="
|
|
1
|
+
<p align="center"><a href="https://event4u.app"><img alt="event4u Agent Switch" src=".github/assets/info-banner.png"></a></p>
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# <img src=".github/assets/icon.png" alt="" width="44" align="top"> Agent Switch — the right account for every session
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
+
<a href="https://github.com/event4u-app/agent-switch/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/event4u-app/agent-switch/actions/workflows/ci.yml/badge.svg"></a>
|
|
6
7
|
<a href="https://www.npmjs.com/package/@event4u/agent-switch"><img alt="npm" src="https://img.shields.io/npm/v/@event4u/agent-switch?style=flat-square&color=orange"></a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@event4u/agent-switch"><img alt="downloads" src="https://img.shields.io/npm/dm/@event4u/agent-switch?style=flat-square&label=downloads%2Fmonth&color=orange"></a>
|
|
7
9
|
<a href="https://event4u-app.github.io/agent-switch/"><img alt="Documentation" src="https://img.shields.io/badge/docs-event4u--app.github.io-orange?style=flat-square"></a>
|
|
8
10
|
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-orange?style=flat-square"></a>
|
|
9
11
|
</p>
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Keep several **Claude Code, Codex and Antigravity** accounts signed in side by side on
|
|
14
|
+
macOS, Linux and Windows — each in its own folder, each with its own login. Start a
|
|
15
|
+
session on the account you mean, see what every account has left, and get warned
|
|
16
|
+
before one runs out. Your tools keep starting exactly as before.
|
|
12
17
|
|
|
13
|
-
📖 **[Read the documentation →](https://event4u-app.github.io/agent-switch/)**
|
|
18
|
+
📖 **[Read the documentation →](https://event4u-app.github.io/agent-switch/)** · **Try it in 30 seconds:** [quickstart ↓](#try-it-in-30-seconds)
|
|
14
19
|
|
|
20
|
+
### See what needs you, what runs where, and what every account has left
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
One window for every account and session: which session runs on which account and
|
|
25
|
+
why, what each account has left in its 5-hour and weekly window (with how old that
|
|
26
|
+
number is), and the one thing that needs you next — a session waiting for your
|
|
27
|
+
answer, a sign-in, an account near its limit. Claude Desktop sessions are filed
|
|
28
|
+
under the account that ran them. The menu-bar popover shows the same at a glance
|
|
29
|
+
and opens Claude Desktop (or Codex, Antigravity) as any account in one click.
|
|
30
|
+
|
|
31
|
+
<details><summary><b>More views</b></summary>
|
|
32
|
+
|
|
33
|
+
|  |  |
|
|
34
|
+
|---|---|
|
|
35
|
+
|  |  |
|
|
36
|
+
|  |  |
|
|
37
|
+
|  |  |
|
|
38
|
+
|
|
39
|
+
<p align="center"><img alt="Menu-bar popover" src=".github/assets/screenshots/tray.png" width="360"></p>
|
|
40
|
+
|
|
41
|
+
</details>
|
|
42
|
+
|
|
43
|
+
## Try it in 30 seconds
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g @event4u/agent-switch # Node ≥ 20, macOS / Linux / Windows
|
|
47
|
+
agent-switch shellenv install # once: `claude` now starts on the right account
|
|
48
|
+
agent-switch add work # sign in each account once (opens the browser)
|
|
49
|
+
agent-switch add personal
|
|
50
|
+
agent-switch # open the desktop app
|
|
15
51
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
|
|
53
|
+
Open a new terminal and run `claude` as always — Agent Switch picks the account.
|
|
54
|
+
`agent-switch doctor` checks the setup; details in [Install](#install).
|
|
55
|
+
|
|
56
|
+
### Or stay in the terminal
|
|
57
|
+
|
|
22
58
|
```
|
|
59
|
+
$ claude # starts on the account with the most to lose before its reset
|
|
60
|
+
$ agent-switch run work # this one session on "work", whatever the balance says
|
|
61
|
+
$ asw work # star "work": it takes plain starts when accounts are level
|
|
62
|
+
$ asw # list all profiles, the starred one marked *
|
|
63
|
+
* work work@example.com
|
|
64
|
+
privat me@example.com
|
|
65
|
+
event4u dev@example.com
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### What it does — and what it deliberately doesn't
|
|
69
|
+
|
|
70
|
+
- **Isolated logins** — every account gets its own config folder and keychain entry; nothing is snapshotted, and a running tool's login is only swapped when you ask for it (`agent-switch rebind`, undone with `rebind --restore`).
|
|
71
|
+
- **Usage per account** — 5-hour and weekly windows for Claude and Codex, always with their age; unknown is shown as unknown, never as free.
|
|
72
|
+
- **Session protection** — on by default at 98 % for every Claude and Codex account; it **warns** at the threshold and does not pause a running session. The warning comes from the background service (`agent-switch service install`), which reads every enabled account at least every 5 minutes. Antigravity has no usage readout and gets no warning.
|
|
73
|
+
- **Sessions that wait for you** — a session asking a question (or waiting on a permission prompt) shows in *Needs you*, and the desktop pet tells you, even while the window is in the background. Claude Desktop sessions (Code tab and agent mode) appear under the account they ran on.
|
|
74
|
+
- **Disable instead of delete** — a disabled account keeps its login and history but is never picked and not polled; refresh it by hand when you want to look.
|
|
75
|
+
- **One prompt history** — with the shared setup (`agent-switch share on`, the app's default) ↑ and Ctrl+R in Claude Code find your old prompts on every account.
|
|
76
|
+
- **Explicit switching only** — a running session moves to another account only when you choose it (live switch, take over or hand off). Nothing switches on its own.
|
|
77
|
+
- **Balanced starts, pools and project rules** — a plain `claude` with no folder rule is spread across your enabled accounts: it starts on the one that would lose the most at its next reset (its rest divided by the hours until its weekly window resets), so every account gets used up by its reset instead of one running dry while another resets unused. An account at its protection limit is never picked; when the best accounts are level, or nothing can be weighed, the account set for plain starts (the star) is used. A folder can also point at one account, or at a pool that is balanced the same way — a pool start never leaves the pool: without fresh usage it still takes a member, and a pool that is gone or has no usable member refuses the start. `dir --json` tells you why. The weighing uses cached usage only — written by the background service (`agent-switch service install`, which refreshes every enabled account every 5 minutes) or by every refresh the desktop app makes (kept unthrottled in `usage-latest.json`); usage older than 10 minutes is not trusted and `dir --json` says so. Under `guard block` an account that already has a live session is skipped.
|
|
78
|
+
- **Health, activity, sign-in state** — `doctor --json`, `activity`, `auth-status` — readouts without tokens.
|
|
79
|
+
- **Switch, the desktop pet** — introduces itself once, on the very first start. Never again.
|
|
23
80
|
|
|
24
81
|
## Why not keychain snapshot swapping?
|
|
25
82
|
|
|
26
83
|
Most naive switchers snapshot the macOS Keychain entry (`Claude Code-credentials`) per account and restore it on switch. This breaks: Claude Code's OAuth **refresh tokens rotate on every refresh**, so a stored snapshot goes stale within hours and you're back to browser logins.
|
|
27
84
|
|
|
28
|
-
`agent-switch` uses **`CLAUDE_CONFIG_DIR` isolation** instead. Each profile gets its own config directory under `~/.agent-switch/<name>/config`. Claude Code derives a *separate* keychain entry per config dir, so every account stays logged in **live and independently**. Switching only changes which directory new `claude` invocations point at — nothing is snapshotted, nothing goes stale, and each account logs in via browser exactly once.
|
|
85
|
+
`agent-switch` uses **`CLAUDE_CONFIG_DIR` isolation** instead. Each profile gets its own config directory under `~/.agent-switch/claude/<name>/config`. Claude Code derives a *separate* keychain entry per config dir, so every account stays logged in **live and independently**. Switching only changes which directory new `claude` invocations point at — nothing is snapshotted, nothing goes stale, and each account logs in via browser exactly once.
|
|
29
86
|
|
|
30
87
|
The same restraint governs the whole tool: no proxy, no traffic interception, no fingerprinting, no prompt rewriting — see [NON-GOALS.md](NON-GOALS.md) for the explicit list and the alternatives this ecosystem offers instead.
|
|
31
88
|
|
|
@@ -36,8 +93,10 @@ nothing used to stop two terminals from starting `claude` on the same account
|
|
|
36
93
|
sharing one set of rate limits and both refreshing one rotating OAuth token,
|
|
37
94
|
while each terminal looked like a separate account.
|
|
38
95
|
|
|
39
|
-
The single-load guard
|
|
40
|
-
holds the profile and which accounts are free
|
|
96
|
+
The single-load guard notices that second start and says which session already
|
|
97
|
+
holds the profile and which accounts are free. New installs start on `warn`
|
|
98
|
+
(it reports, then lets the session start); with `agent-switch guard block` it
|
|
99
|
+
refuses instead, as shown here:
|
|
41
100
|
|
|
42
101
|
```
|
|
43
102
|
$ claude
|
|
@@ -72,7 +131,7 @@ project):
|
|
|
72
131
|
npm install -g @event4u/agent-switch # puts `agent-switch` on your PATH
|
|
73
132
|
# or run without installing:
|
|
74
133
|
npx @event4u/agent-switch --help
|
|
75
|
-
npx @event4u/agent-switch
|
|
134
|
+
npx @event4u/agent-switch doctor # self-check: CLI, shell integration, logins
|
|
76
135
|
```
|
|
77
136
|
|
|
78
137
|
`npx` is handy for a one-off or first run; for daily use install globally so the
|
|
@@ -89,8 +148,11 @@ npm link # puts `agent-switch` on your PATH (npm creates a .cmd shim
|
|
|
89
148
|
Native package managers (Homebrew / Scoop / winget) are planned; `npm i -g`
|
|
90
149
|
above is the supported install path on every OS in the meantime.
|
|
91
150
|
|
|
92
|
-
Then add the shell integration
|
|
93
|
-
|
|
151
|
+
Then add the shell integration. `agent-switch shellenv install` does it for you:
|
|
152
|
+
it detects your shell and adds the line below to its startup file (once — a second
|
|
153
|
+
run changes nothing). The desktop app shows a red banner until it is set up and
|
|
154
|
+
offers the same step. To do it by hand — `agent-switch shellenv` auto-detects your
|
|
155
|
+
shell, or pass `--shell`:
|
|
94
156
|
|
|
95
157
|
| Shell | One-time setup |
|
|
96
158
|
|---|---|
|
|
@@ -127,40 +189,48 @@ agent-switch add event4u
|
|
|
127
189
|
|
|
128
190
|
| Command | Effect |
|
|
129
191
|
|---|---|
|
|
130
|
-
| `asw work` |
|
|
192
|
+
| `asw work` | star an account: it takes plain starts when the accounts are level (or nothing can be weighed) |
|
|
131
193
|
| `asw` | list profiles (live sessions shown) |
|
|
132
194
|
| `agent-switch run event4u` | one-shot session on another account **without** switching — all accounts can run in parallel |
|
|
133
195
|
| `agent-switch run work -- --resume` | pass any flags through to `claude` |
|
|
134
196
|
| `agent-switch status` | identity + 5h/7d usage for every profile |
|
|
197
|
+
| `agent-switch login work` | sign a profile in again after a sign-out or an expired login (`--browser arc` opens the Claude sign-in in a specific browser; `agent-switch browsers` lists them) |
|
|
135
198
|
| `agent-switch map work ~/projects/kunde-a` | `claude` in that repo always uses this account — no switching |
|
|
136
|
-
| `agent-switch share on` | one settings/skills/commands/agents tree for all profiles |
|
|
199
|
+
| `agent-switch share on` | one settings/skills/commands/agents tree and one prompt history for all profiles |
|
|
137
200
|
| `agent-switch sessions` | recent + live Claude sessions per profile |
|
|
138
201
|
| `agent-switch takeover <id> --to work` | move a session to another account and resume it there (see below) |
|
|
139
202
|
| `agent-switch web work` | claude.ai in a persistent per-profile browser (see below) |
|
|
140
|
-
| `agent-switch remove old --force` | delete a profile incl. its keychain entry |
|
|
203
|
+
| `agent-switch remove old --force` | delete a profile incl. its keychain entry (asks on a terminal; `--yes` skips) |
|
|
141
204
|
| `agent-switch label work Work` | tag a profile (`Work` / `Personal` / `Other`, or `none` to clear) |
|
|
205
|
+
| `agent-switch disable work` | keep the profile but stop using it (`enable` turns it back on) |
|
|
206
|
+
| `agent-switch color work teal` | pick the profile's colour in the app (`none` clears it) |
|
|
142
207
|
| `agent-switch providers status` | which providers are enabled (default: Claude + Codex; others off) |
|
|
143
|
-
| `agent-switch providers enable --provider
|
|
144
|
-
| `agent-switch autoswitch on --threshold 90` |
|
|
208
|
+
| `agent-switch providers enable --provider antigravity` | enable a provider (or `disable`; `--surface cli\|ui` for one surface) |
|
|
209
|
+
| `agent-switch autoswitch on --threshold 90` | near-limit notification that suggests another profile (off by default; never switches) |
|
|
145
210
|
| `agent-switch uninstall --force` | remove all agent-switch data, keychain entries, and the daemon |
|
|
146
|
-
| `agent-switch doctor` |
|
|
211
|
+
| `agent-switch doctor [--json]` | self-check (CLI on PATH, shell integration, start at login, API-key overrides, logins, share links) |
|
|
212
|
+
| `agent-switch auth-status --json` | per-profile login state from the provider's store — never a token |
|
|
213
|
+
| `agent-switch activity` | what agent-switch itself did, and why |
|
|
214
|
+
| `agent-switch protection set claude/work --threshold 95` | per-profile limit warning (default on, 98 %); warns, never pauses |
|
|
215
|
+
| `agent-switch pool create Work` · `pool add work claude/a` · `map pool:work ~/x` | optional pools (also in the app: Accounts › Projects & pools › New pool): a folder rule starts on the member that would lose the most at its next reset (`dir --json` says why) |
|
|
147
216
|
|
|
148
217
|
## Per-repo accounts (directory mappings)
|
|
149
218
|
|
|
150
219
|
```bash
|
|
151
220
|
agent-switch map work ~/projects/firma
|
|
152
221
|
agent-switch map privat ~/projects/hobby
|
|
222
|
+
agent-switch map --pause ~/projects/hobby # keep the rule, skip it for now (--resume turns it back on)
|
|
153
223
|
```
|
|
154
224
|
|
|
155
225
|
The `claude` shell wrapper resolves the current directory to the nearest mapped
|
|
156
|
-
ancestor. Precedence: **mapping > active profile > default**. `agent-switch mappings`
|
|
226
|
+
ancestor. Precedence: **mapping > balanced pick across your accounts > active profile (the star) > default**. `agent-switch mappings`
|
|
157
227
|
lists, `agent-switch unmap` removes.
|
|
158
228
|
|
|
159
229
|
## Shared settings across accounts
|
|
160
230
|
|
|
161
231
|
```bash
|
|
162
|
-
agent-switch share on # settings.json, CLAUDE.md, skills/, commands/, agents/
|
|
163
|
-
agent-switch share on --history # additionally share
|
|
232
|
+
agent-switch share on # settings.json, CLAUDE.md, skills/, commands/, agents/ + prompt history (history.jsonl)
|
|
233
|
+
agent-switch share on --history # additionally share conversations + plans/ (--resume, POSIX only)
|
|
164
234
|
agent-switch share sync # re-link any file a /config edit forked (see below)
|
|
165
235
|
agent-switch share off # removes only agent-switch-created links
|
|
166
236
|
```
|
|
@@ -257,9 +327,33 @@ profile. Two strategies, picked per app:
|
|
|
257
327
|
|
|
258
328
|
```bash
|
|
259
329
|
agent-switch apps # list launchable GUI apps + installed state
|
|
260
|
-
agent-switch open <app> [profile] # launch an app on a profile (active if omitted)
|
|
330
|
+
agent-switch open <app> [profile] # launch an app on a profile (active if omitted);
|
|
331
|
+
# if that profile already runs, its window comes to the front
|
|
332
|
+
agent-switch launcher add <app> <profile> # a Dock/Spotlight app for that account
|
|
333
|
+
agent-switch launcher remove <app> <profile> # remove it again (list / repair as well)
|
|
334
|
+
agent-switch app-settings copy claude-desktop --from default --to <profile>
|
|
335
|
+
# MCP servers, preferences, extensions — never logins
|
|
261
336
|
```
|
|
262
337
|
|
|
338
|
+
**Launchers.** `launcher add` puts a small app into `~/Applications/Agent Switch/`
|
|
339
|
+
(e.g. `Claude Desktop – Work.app`) that runs `agent-switch open` for that
|
|
340
|
+
account — drag it into the Dock or start it from Spotlight. It is built locally
|
|
341
|
+
with `osacompile` and signed ad hoc, so Gatekeeper does not prompt; the
|
|
342
|
+
original app is never copied, patched or re-signed. The launcher carries the
|
|
343
|
+
app's icon; the open window still shows the app's own icon and name. Removing
|
|
344
|
+
or renaming a profile removes or rebuilds its launchers, `uninstall` removes
|
|
345
|
+
them all, and `launcher repair` rebuilds them after Node or agent-switch moved.
|
|
346
|
+
macOS only: Claude Desktop on Windows is an MSIX package whose executable
|
|
347
|
+
cannot be started with its own arguments.
|
|
348
|
+
|
|
349
|
+
**Copying Claude Desktop settings.** Chats, Projects and memory belong to the
|
|
350
|
+
account on Anthropic's servers and cannot be moved. What lives on the Mac can:
|
|
351
|
+
`app-settings copy` takes the MCP servers, preferences and installed extensions
|
|
352
|
+
from the default install (`default`) or another profile. It never copies
|
|
353
|
+
`config.json` (it holds the login's token cache), cookies, web storage,
|
|
354
|
+
session folders or preferences scoped to one account; replaced files are kept
|
|
355
|
+
as `*.pre-copy`. Quit Claude Desktop on the target profile first.
|
|
356
|
+
|
|
263
357
|
**Supported now:**
|
|
264
358
|
|
|
265
359
|
- **Claude Desktop** (`agent-switch open claude-desktop <profile>`) —
|
|
@@ -273,8 +367,8 @@ agent-switch open <app> [profile] # launch an app on a profile (active if omitte
|
|
|
273
367
|
More clients land via their roadmaps in `agents/roadmaps/`.
|
|
274
368
|
|
|
275
369
|
Caveats (why this is experimental):
|
|
276
|
-
- **macOS-only for now**;
|
|
277
|
-
|
|
370
|
+
- **macOS-only for now**; a plain Finder/Dock start of the app itself doesn't
|
|
371
|
+
carry the flag/env (→ no isolation) — use `agent-switch open` or a launcher.
|
|
278
372
|
- **Unofficial / version-fragile** — no vendor ships a built-in account
|
|
279
373
|
switcher; an app update can change launcher behaviour. Re-verify per app.
|
|
280
374
|
- The profile's data dir is never swapped on a live app (that risks
|
|
@@ -334,8 +428,10 @@ Full per-mechanism contract (verified/degraded/broken with sources) lives in
|
|
|
334
428
|
The CLI is fully usable on its own. Two optional layers add convenience:
|
|
335
429
|
|
|
336
430
|
- **Usage daemon** — a background service polls each profile's **own** usage
|
|
337
|
-
(
|
|
338
|
-
|
|
431
|
+
(every enabled account at least every 5 minutes, the active one and accounts
|
|
432
|
+
with a live session more often; disabled accounts never), caches it, and
|
|
433
|
+
notifies on the active profile's threshold crossings (75% / 90% by default).
|
|
434
|
+
Balanced starts (`dir`) and session protection warnings depend on it running:
|
|
339
435
|
|
|
340
436
|
```bash
|
|
341
437
|
agent-switch service install # launchd (macOS) / systemd --user (Linux) / Task Scheduler (Windows)
|
|
@@ -343,15 +439,16 @@ The CLI is fully usable on its own. Two optional layers add convenience:
|
|
|
343
439
|
agent-switch service start|stop # manual control (no install)
|
|
344
440
|
```
|
|
345
441
|
|
|
346
|
-
- **
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
442
|
+
- **Desktop app** (`gui/`, Tauri) — Overview, Sessions, Accounts and Tools in one
|
|
443
|
+
window, Health / Activity / Notifications / Settings on the right of the top bar,
|
|
444
|
+
and a menu-bar popover with the accounts that need you. It is a **client of the
|
|
445
|
+
CLI** (`agent-switch <cmd> --json`) and never re-implements profile logic; the CLI
|
|
446
|
+
core stays dependency-free. See [`gui/README.md`](gui/README.md). Running
|
|
447
|
+
`agent-switch` **with no command** launches it (same as `agent-switch gui`); if
|
|
448
|
+
the app is already running, no second copy starts.
|
|
352
449
|
|
|
353
|
-
Usage readout is per-provider: Claude
|
|
354
|
-
**
|
|
450
|
+
Usage readout is per-provider: Claude and Codex report their own usage windows;
|
|
451
|
+
**Antigravity has no usage readout**, so it shows identity only (never a
|
|
355
452
|
fabricated number).
|
|
356
453
|
|
|
357
454
|
### Run it all automatically (on login)
|
|
@@ -403,46 +500,42 @@ To remove these prompts entirely, the builds need Apple Developer-ID signing +
|
|
|
403
500
|
notarization (macOS) and an Authenticode certificate (Windows) — wired via
|
|
404
501
|
release secrets when certificates are available.
|
|
405
502
|
|
|
406
|
-
###
|
|
503
|
+
### Behaviour changes in this release
|
|
504
|
+
|
|
505
|
+
- **Codex resets are no longer redeemed automatically.** Up to 2.1.0, with
|
|
506
|
+
`autoswitch on` for Codex, the daemon redeemed a banked reset on its own once
|
|
507
|
+
the active profile crossed the threshold. It now only notifies you that a
|
|
508
|
+
reset is available (once per reset cycle); redeeming is your action —
|
|
509
|
+
`agent-switch reset <profile> --provider codex`, or the confirmed button in
|
|
510
|
+
the app.
|
|
511
|
+
- **The single-load guard starts on `warn` for new installs.** Existing installs
|
|
512
|
+
(a `state.json` or any profile) keep `block`.
|
|
513
|
+
|
|
514
|
+
### Near-limit notifications (opt-in, off by default)
|
|
407
515
|
|
|
408
516
|
By default `agent-switch` only switches when **you** ask, and shows **your own**
|
|
409
|
-
usage per profile
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
> and this notice states it plainly. Plain per-context profile switching
|
|
416
|
-
> (private / work / client) and viewing your own usage are **not** affected;
|
|
417
|
-
> this warning is specific to automated quota-driven rotation.
|
|
418
|
-
|
|
419
|
-
Auto-switch is **globally off by default** and must be explicitly enabled. It is
|
|
420
|
-
available **only for Claude** — the one provider with a usage readout to trigger
|
|
421
|
-
on; Codex and Gemini have none, so they are profile-switch only and never show an
|
|
422
|
-
auto-switch control. When enabled (`agent-switch autoswitch on [--threshold N]`),
|
|
423
|
-
the background daemon moves the active Claude profile to the account with the most
|
|
424
|
-
headroom once the active one hits the threshold. Switching only affects **new**
|
|
425
|
-
sessions; running ones keep their environment.
|
|
426
|
-
|
|
427
|
-
**On the record:** this capability was reviewed and **rejected** — first in the
|
|
428
|
-
v1 adoption analysis, then in a two-round AI-council review that was **unanimous**
|
|
429
|
-
that quota-triggered cross-account rotation crosses the providers' usage policies
|
|
430
|
-
and recommended removal. It ships here anyway, off by default, as a deliberate
|
|
431
|
-
decision that **overrode** that recommendation — not a relitigation that resolved
|
|
432
|
-
it. The full rejection rationale is preserved verbatim in
|
|
433
|
-
[`agents/roadmaps/skipped/road-to-agent-switch-autoswitch-rejected.md`](agents/roadmaps/skipped/road-to-agent-switch-autoswitch-rejected.md);
|
|
434
|
-
read it before enabling.
|
|
517
|
+
usage per profile — the same information the vendors' own `/usage` surfaces show.
|
|
518
|
+
`agent-switch autoswitch on [--threshold N]` adds a notification when the active
|
|
519
|
+
profile nears its limit, with a suggested profile to switch to. It **never
|
|
520
|
+
switches by itself**: accepting the suggestion is your action. The earlier
|
|
521
|
+
automatic quota rotation was removed; its review record stays in
|
|
522
|
+
[`agents/roadmaps/skipped/road-to-agent-switch-autoswitch-rejected.md`](agents/roadmaps/skipped/road-to-agent-switch-autoswitch-rejected.md).
|
|
435
523
|
|
|
436
524
|
## Layout
|
|
437
525
|
|
|
438
526
|
```
|
|
439
527
|
~/.agent-switch/
|
|
440
|
-
state.json #
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
528
|
+
state.json # active profile per provider, labels, colours, disabled
|
|
529
|
+
mappings.json # folder rules (and which are paused)
|
|
530
|
+
claude/
|
|
531
|
+
work/
|
|
532
|
+
config/ # CLAUDE_CONFIG_DIR for this account (.claude.json, settings, history)
|
|
533
|
+
browser/ # persistent Playwright profile for claude.ai
|
|
534
|
+
gui/claude-desktop/ # this account's own Claude Desktop data
|
|
535
|
+
usage-history.json # the usage samples behind the history charts
|
|
536
|
+
privat/
|
|
537
|
+
config/
|
|
538
|
+
codex/
|
|
539
|
+
work/
|
|
540
|
+
config/ # CODEX_HOME for this account
|
|
448
541
|
```
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity log — what agent-switch itself did, and why. Append-only JSONL,
|
|
3
|
+
* size-capped to the newest {@link ACTIVITY_MAX_LINES} entries, read by
|
|
4
|
+
* `agent-switch activity [--json]` and the GUI.
|
|
5
|
+
*
|
|
6
|
+
* Only agent-switch's own actions land here (switch, rebind, login, redeem,
|
|
7
|
+
* autostart, guard, update, protection, pool, remove) — never provider traffic.
|
|
8
|
+
* Every string field passes {@link redactSecrets} on the way in, so a token that
|
|
9
|
+
* leaked into an error message never reaches the file.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { ROOT, ensureRoot } from "./profiles.js";
|
|
14
|
+
export const ACTIVITY_FILE = path.join(ROOT, "activity.jsonl");
|
|
15
|
+
export const ACTIVITY_MAX_LINES = 2000;
|
|
16
|
+
/** Trim only once the file grows past this, so a normal append stays O(1). */
|
|
17
|
+
const TRIM_BYTES = 512 * 1024;
|
|
18
|
+
const MAX_FIELD = 300;
|
|
19
|
+
export const ACTIVITY_KINDS = [
|
|
20
|
+
"switch",
|
|
21
|
+
"rebind",
|
|
22
|
+
"login",
|
|
23
|
+
"redeem",
|
|
24
|
+
"autostart",
|
|
25
|
+
"guard",
|
|
26
|
+
"update",
|
|
27
|
+
"protection",
|
|
28
|
+
"pool",
|
|
29
|
+
"remove",
|
|
30
|
+
"launcher",
|
|
31
|
+
"settings",
|
|
32
|
+
];
|
|
33
|
+
/** Strip anything shaped like a credential: OAuth/API key prefixes, JWTs, and
|
|
34
|
+
* long opaque runs. Over-redaction is fine here; a leaked token is not. */
|
|
35
|
+
export function redactSecrets(s) {
|
|
36
|
+
return s
|
|
37
|
+
.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted]")
|
|
38
|
+
.replace(/\b(sk|rt|at|sess|ghp|gho|xox[abp])[-_][A-Za-z0-9_-]{8,}/g, "[redacted]")
|
|
39
|
+
.replace(/[A-Za-z0-9_+=-]{40,}/g, "[redacted]");
|
|
40
|
+
}
|
|
41
|
+
function clean(s) {
|
|
42
|
+
const one = redactSecrets(String(s)).replace(/[\r\n\t]+/g, " ");
|
|
43
|
+
return one.length > MAX_FIELD ? one.slice(0, MAX_FIELD - 1) + "…" : one;
|
|
44
|
+
}
|
|
45
|
+
export function appendActivity(e, file = ACTIVITY_FILE) {
|
|
46
|
+
const entry = {
|
|
47
|
+
at: e.at ?? new Date().toISOString(),
|
|
48
|
+
kind: e.kind,
|
|
49
|
+
target: e.target === null ? null : clean(e.target),
|
|
50
|
+
action: clean(e.action),
|
|
51
|
+
reason: clean(e.reason),
|
|
52
|
+
ok: e.ok,
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
if (file === ACTIVITY_FILE)
|
|
56
|
+
ensureRoot();
|
|
57
|
+
fs.appendFileSync(file, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
58
|
+
if (fs.statSync(file).size > TRIM_BYTES)
|
|
59
|
+
trimActivity(file);
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null; // the log is a record, never a reason for a command to fail
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function trimActivity(file) {
|
|
67
|
+
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
68
|
+
if (lines.length <= ACTIVITY_MAX_LINES)
|
|
69
|
+
return;
|
|
70
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
71
|
+
fs.writeFileSync(tmp, lines.slice(-ACTIVITY_MAX_LINES).join("\n") + "\n", { mode: 0o600 });
|
|
72
|
+
fs.renameSync(tmp, file);
|
|
73
|
+
}
|
|
74
|
+
/** Entries oldest → newest; malformed lines are skipped. `limit` keeps the newest N. */
|
|
75
|
+
export function readActivity(limit, file = ACTIVITY_FILE) {
|
|
76
|
+
let raw = "";
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(file, "utf8");
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const line of raw.split("\n")) {
|
|
85
|
+
if (!line)
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
const e = JSON.parse(line);
|
|
89
|
+
if (e && typeof e.at === "string" && ACTIVITY_KINDS.includes(e.kind))
|
|
90
|
+
out.push(e);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* skip */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (limit === undefined || limit < 0)
|
|
97
|
+
return out;
|
|
98
|
+
return limit === 0 ? [] : out.slice(-limit);
|
|
99
|
+
}
|
package/dist/args.js
CHANGED
|
@@ -15,6 +15,7 @@ import { PROVIDER_IDS, isProviderId } from "./providers.js";
|
|
|
15
15
|
* (the bug behind `notify`/`providers --surface` silently losing their args). */
|
|
16
16
|
const VALUE_FLAGS = new Set([
|
|
17
17
|
"provider",
|
|
18
|
+
"browser",
|
|
18
19
|
"shell",
|
|
19
20
|
"source",
|
|
20
21
|
"threshold",
|
|
@@ -29,6 +30,9 @@ const VALUE_FLAGS = new Set([
|
|
|
29
30
|
"ack",
|
|
30
31
|
"brief",
|
|
31
32
|
"profile",
|
|
33
|
+
"by",
|
|
34
|
+
"reason",
|
|
35
|
+
"limit",
|
|
32
36
|
]);
|
|
33
37
|
/** Resolve a `--provider` value to a ProviderId; throws (not exits) on invalid
|
|
34
38
|
* so callers can test it. Undefined → claude. */
|
|
@@ -71,16 +75,32 @@ export function parseArgs(argv) {
|
|
|
71
75
|
/**
|
|
72
76
|
* `run` is special: everything after the profile name is passed THROUGH to the
|
|
73
77
|
* provider binary (including its own `--flags`), so it can't go through the
|
|
74
|
-
* general parser. Strip only `--provider <val
|
|
78
|
+
* general parser. Strip only `--provider <val>` and `--cwd <dir>` (before any
|
|
79
|
+
* bare `--`), take the name, pass the rest.
|
|
75
80
|
*/
|
|
76
81
|
export function parseRun(rest) {
|
|
77
82
|
const args = [...rest];
|
|
83
|
+
// Everything after a bare `--` belongs to the provider binary, so our own
|
|
84
|
+
// flags are only looked for before it.
|
|
85
|
+
const own = (flag) => {
|
|
86
|
+
const end = args.indexOf("--");
|
|
87
|
+
const i = args.indexOf(flag);
|
|
88
|
+
return i >= 0 && (end < 0 || i < end) ? i : -1;
|
|
89
|
+
};
|
|
78
90
|
let providerId = "claude";
|
|
79
|
-
const pi =
|
|
91
|
+
const pi = own("--provider");
|
|
80
92
|
if (pi >= 0) {
|
|
81
93
|
providerId = resolveProviderValue(args[pi + 1]);
|
|
82
94
|
args.splice(pi, 2);
|
|
83
95
|
}
|
|
96
|
+
let cwd;
|
|
97
|
+
const ci = own("--cwd");
|
|
98
|
+
if (ci >= 0) {
|
|
99
|
+
cwd = args[ci + 1];
|
|
100
|
+
if (!cwd)
|
|
101
|
+
throw new Error("--cwd needs a folder");
|
|
102
|
+
args.splice(ci, 2);
|
|
103
|
+
}
|
|
84
104
|
const name = args.shift();
|
|
85
|
-
return { providerId, name, args };
|
|
105
|
+
return cwd === undefined ? { providerId, name, args } : { providerId, name, args, cwd };
|
|
86
106
|
}
|