harness-swap 0.2.0__tar.gz
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.
- harness_swap-0.2.0/.github/FUNDING.yml +1 -0
- harness_swap-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
- harness_swap-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- harness_swap-0.2.0/.github/ISSUE_TEMPLATE/feature_request.yml +16 -0
- harness_swap-0.2.0/.github/workflows/ci.yml +82 -0
- harness_swap-0.2.0/.github/workflows/publish.yml +30 -0
- harness_swap-0.2.0/.gitignore +19 -0
- harness_swap-0.2.0/.ignore +5 -0
- harness_swap-0.2.0/.python-version +1 -0
- harness_swap-0.2.0/.vscode/launch.json +60 -0
- harness_swap-0.2.0/LICENSE +21 -0
- harness_swap-0.2.0/PKG-INFO +269 -0
- harness_swap-0.2.0/README.md +238 -0
- harness_swap-0.2.0/assets/tui-watch.png +0 -0
- harness_swap-0.2.0/docs/adapter-development.md +310 -0
- harness_swap-0.2.0/docs/architecture.md +211 -0
- harness_swap-0.2.0/docs/claude.md +393 -0
- harness_swap-0.2.0/docs/harness-compatibility.md +255 -0
- harness_swap-0.2.0/docs/migration.md +81 -0
- harness_swap-0.2.0/docs/security.md +195 -0
- harness_swap-0.2.0/docs/testing.md +88 -0
- harness_swap-0.2.0/examples/acme_adapter.py +108 -0
- harness_swap-0.2.0/pyproject.toml +75 -0
- harness_swap-0.2.0/src/claude_swap/__init__.py +10 -0
- harness_swap-0.2.0/src/claude_swap/__main__.py +6 -0
- harness_swap-0.2.0/src/claude_swap/app_bundle.py +310 -0
- harness_swap-0.2.0/src/claude_swap/appearance.py +218 -0
- harness_swap-0.2.0/src/claude_swap/autoswitch.py +2737 -0
- harness_swap-0.2.0/src/claude_swap/cache.py +44 -0
- harness_swap-0.2.0/src/claude_swap/claude_locks.py +228 -0
- harness_swap-0.2.0/src/claude_swap/cli.py +1759 -0
- harness_swap-0.2.0/src/claude_swap/credentials.py +1968 -0
- harness_swap-0.2.0/src/claude_swap/exceptions.py +98 -0
- harness_swap-0.2.0/src/claude_swap/fsutil.py +12 -0
- harness_swap-0.2.0/src/claude_swap/json_output.py +370 -0
- harness_swap-0.2.0/src/claude_swap/launch_agent.py +321 -0
- harness_swap-0.2.0/src/claude_swap/lines.py +126 -0
- harness_swap-0.2.0/src/claude_swap/locking.py +17 -0
- harness_swap-0.2.0/src/claude_swap/logging_config.py +64 -0
- harness_swap-0.2.0/src/claude_swap/macos_keychain.py +228 -0
- harness_swap-0.2.0/src/claude_swap/mappings.py +127 -0
- harness_swap-0.2.0/src/claude_swap/menubar.py +1067 -0
- harness_swap-0.2.0/src/claude_swap/migrations.py +520 -0
- harness_swap-0.2.0/src/claude_swap/models.py +284 -0
- harness_swap-0.2.0/src/claude_swap/oauth.py +813 -0
- harness_swap-0.2.0/src/claude_swap/pace.py +161 -0
- harness_swap-0.2.0/src/claude_swap/paths.py +236 -0
- harness_swap-0.2.0/src/claude_swap/poll_policy.py +285 -0
- harness_swap-0.2.0/src/claude_swap/printer.py +232 -0
- harness_swap-0.2.0/src/claude_swap/process_detection.py +370 -0
- harness_swap-0.2.0/src/claude_swap/session.py +1522 -0
- harness_swap-0.2.0/src/claude_swap/settings.py +563 -0
- harness_swap-0.2.0/src/claude_swap/snapshot_source.py +100 -0
- harness_swap-0.2.0/src/claude_swap/spawn.py +11 -0
- harness_swap-0.2.0/src/claude_swap/switcher.py +7741 -0
- harness_swap-0.2.0/src/claude_swap/transfer.py +746 -0
- harness_swap-0.2.0/src/claude_swap/tui/__init__.py +43 -0
- harness_swap-0.2.0/src/claude_swap/tui/app.py +436 -0
- harness_swap-0.2.0/src/claude_swap/tui/autoview.py +369 -0
- harness_swap-0.2.0/src/claude_swap/tui/cswap.tcss +205 -0
- harness_swap-0.2.0/src/claude_swap/tui/dashboard.py +432 -0
- harness_swap-0.2.0/src/claude_swap/tui/data.py +194 -0
- harness_swap-0.2.0/src/claude_swap/tui/modals.py +159 -0
- harness_swap-0.2.0/src/claude_swap/tui/theme.py +141 -0
- harness_swap-0.2.0/src/claude_swap/tui/widgets.py +436 -0
- harness_swap-0.2.0/src/claude_swap/update_check.py +417 -0
- harness_swap-0.2.0/src/claude_swap/usage_store.py +1326 -0
- harness_swap-0.2.0/src/harness_swap/__init__.py +1 -0
- harness_swap-0.2.0/src/harness_swap/__main__.py +6 -0
- harness_swap-0.2.0/src/harness_swap/adapters/__init__.py +14 -0
- harness_swap-0.2.0/src/harness_swap/adapters/agy.py +336 -0
- harness_swap-0.2.0/src/harness_swap/adapters/base.py +317 -0
- harness_swap-0.2.0/src/harness_swap/adapters/claude.py +433 -0
- harness_swap-0.2.0/src/harness_swap/adapters/codex.py +492 -0
- harness_swap-0.2.0/src/harness_swap/adapters/grok.py +346 -0
- harness_swap-0.2.0/src/harness_swap/adapters/opencode.py +582 -0
- harness_swap-0.2.0/src/harness_swap/cli.py +684 -0
- harness_swap-0.2.0/src/harness_swap/core/__init__.py +0 -0
- harness_swap-0.2.0/src/harness_swap/core/accounts.py +158 -0
- harness_swap-0.2.0/src/harness_swap/core/adapter.py +196 -0
- harness_swap-0.2.0/src/harness_swap/core/autorotate.py +124 -0
- harness_swap-0.2.0/src/harness_swap/core/credentials.py +408 -0
- harness_swap-0.2.0/src/harness_swap/core/errors.py +58 -0
- harness_swap-0.2.0/src/harness_swap/core/models.py +255 -0
- harness_swap-0.2.0/src/harness_swap/core/paths.py +36 -0
- harness_swap-0.2.0/src/harness_swap/core/registry.py +233 -0
- harness_swap-0.2.0/src/harness_swap/core/rotation.py +83 -0
- harness_swap-0.2.0/src/harness_swap/core/sessions.py +142 -0
- harness_swap-0.2.0/src/harness_swap/core/settings.py +152 -0
- harness_swap-0.2.0/src/harness_swap/core/store.py +243 -0
- harness_swap-0.2.0/src/harness_swap/core/usage.py +193 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/__init__.py +0 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/fsutil.py +249 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/jsonrpc.py +148 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/locking.py +97 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/processes.py +65 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/secrets.py +23 -0
- harness_swap-0.2.0/src/harness_swap/infrastructure/spawn.py +135 -0
- harness_swap-0.2.0/src/harness_swap/output.py +82 -0
- harness_swap-0.2.0/src/harness_swap/ui/__init__.py +0 -0
- harness_swap-0.2.0/src/harness_swap/ui/tui.py +167 -0
- harness_swap-0.2.0/tests/__init__.py +1 -0
- harness_swap-0.2.0/tests/conftest.py +910 -0
- harness_swap-0.2.0/tests/fixtures/sample_config.json +10 -0
- harness_swap-0.2.0/tests/hswap_helpers.py +75 -0
- harness_swap-0.2.0/tests/test_account_policy_cli.py +236 -0
- harness_swap-0.2.0/tests/test_account_policy_store.py +470 -0
- harness_swap-0.2.0/tests/test_account_policy_stray_standby_key.py +118 -0
- harness_swap-0.2.0/tests/test_account_policy_values.py +254 -0
- harness_swap-0.2.0/tests/test_add_account_identity.py +720 -0
- harness_swap-0.2.0/tests/test_api_key_accounts.py +1153 -0
- harness_swap-0.2.0/tests/test_app_bundle.py +279 -0
- harness_swap-0.2.0/tests/test_appearance.py +331 -0
- harness_swap-0.2.0/tests/test_autoswitch.py +7202 -0
- harness_swap-0.2.0/tests/test_autoswitch_account_threshold.py +1318 -0
- harness_swap-0.2.0/tests/test_backup_read_pacing.py +494 -0
- harness_swap-0.2.0/tests/test_cache.py +72 -0
- harness_swap-0.2.0/tests/test_claude_locks.py +314 -0
- harness_swap-0.2.0/tests/test_cli.py +1968 -0
- harness_swap-0.2.0/tests/test_colour_cache_isolation.py +196 -0
- harness_swap-0.2.0/tests/test_config_cli.py +333 -0
- harness_swap-0.2.0/tests/test_credentials.py +274 -0
- harness_swap-0.2.0/tests/test_fsutil.py +566 -0
- harness_swap-0.2.0/tests/test_hswap_agy.py +284 -0
- harness_swap-0.2.0/tests/test_hswap_auto.py +165 -0
- harness_swap-0.2.0/tests/test_hswap_claude_adapter.py +328 -0
- harness_swap-0.2.0/tests/test_hswap_cli.py +469 -0
- harness_swap-0.2.0/tests/test_hswap_codex.py +682 -0
- harness_swap-0.2.0/tests/test_hswap_contract.py +208 -0
- harness_swap-0.2.0/tests/test_hswap_core.py +539 -0
- harness_swap-0.2.0/tests/test_hswap_credentials.py +347 -0
- harness_swap-0.2.0/tests/test_hswap_grok.py +462 -0
- harness_swap-0.2.0/tests/test_hswap_jsonrpc.py +84 -0
- harness_swap-0.2.0/tests/test_hswap_opencode.py +557 -0
- harness_swap-0.2.0/tests/test_hswap_sessions.py +74 -0
- harness_swap-0.2.0/tests/test_json_output.py +803 -0
- harness_swap-0.2.0/tests/test_launch_agent.py +447 -0
- harness_swap-0.2.0/tests/test_lines.py +211 -0
- harness_swap-0.2.0/tests/test_locking.py +162 -0
- harness_swap-0.2.0/tests/test_logging_config.py +59 -0
- harness_swap-0.2.0/tests/test_macos_keychain.py +212 -0
- harness_swap-0.2.0/tests/test_macos_keychain_contract.py +1213 -0
- harness_swap-0.2.0/tests/test_mappings.py +204 -0
- harness_swap-0.2.0/tests/test_menubar.py +619 -0
- harness_swap-0.2.0/tests/test_migrations.py +596 -0
- harness_swap-0.2.0/tests/test_move_accounts.py +578 -0
- harness_swap-0.2.0/tests/test_oauth.py +1524 -0
- harness_swap-0.2.0/tests/test_pace.py +169 -0
- harness_swap-0.2.0/tests/test_paths.py +349 -0
- harness_swap-0.2.0/tests/test_poll_policy.py +435 -0
- harness_swap-0.2.0/tests/test_printer.py +308 -0
- harness_swap-0.2.0/tests/test_priority_strategy.py +401 -0
- harness_swap-0.2.0/tests/test_process_detection.py +731 -0
- harness_swap-0.2.0/tests/test_real_store_guard.py +912 -0
- harness_swap-0.2.0/tests/test_session.py +3011 -0
- harness_swap-0.2.0/tests/test_settings.py +493 -0
- harness_swap-0.2.0/tests/test_spawn.py +306 -0
- harness_swap-0.2.0/tests/test_swap_accounts.py +726 -0
- harness_swap-0.2.0/tests/test_switcher.py +12908 -0
- harness_swap-0.2.0/tests/test_theme.py +67 -0
- harness_swap-0.2.0/tests/test_transfer.py +2226 -0
- harness_swap-0.2.0/tests/test_tui.py +1833 -0
- harness_swap-0.2.0/tests/test_tui_policy_display.py +220 -0
- harness_swap-0.2.0/tests/test_update_check.py +810 -0
- harness_swap-0.2.0/tests/test_usage_store.py +1873 -0
- harness_swap-0.2.0/tests/test_worker_gc_freeze.py +79 -0
- harness_swap-0.2.0/uv.lock +361 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: realiti4
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something in cswap isn't working right
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: what-happened
|
|
7
|
+
attributes:
|
|
8
|
+
label: What happened?
|
|
9
|
+
description: What did you do, what did you see, and what did you expect instead?
|
|
10
|
+
placeholder: Ran `cswap switch 2`, got ... — expected ...
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: input
|
|
14
|
+
id: version
|
|
15
|
+
attributes:
|
|
16
|
+
label: cswap version and install method
|
|
17
|
+
description: Output of `cswap --version`, and whether you installed with uv, pipx, or from source.
|
|
18
|
+
placeholder: cswap 0.20.0, installed with uv
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
- type: dropdown
|
|
22
|
+
id: os
|
|
23
|
+
attributes:
|
|
24
|
+
label: Operating system
|
|
25
|
+
description: Credentials are stored differently per platform, so this matters a lot.
|
|
26
|
+
options:
|
|
27
|
+
- macOS
|
|
28
|
+
- Linux
|
|
29
|
+
- Windows
|
|
30
|
+
- WSL
|
|
31
|
+
validations:
|
|
32
|
+
required: true
|
|
33
|
+
- type: dropdown
|
|
34
|
+
id: claude-code
|
|
35
|
+
attributes:
|
|
36
|
+
label: How do you run Claude Code?
|
|
37
|
+
multiple: true
|
|
38
|
+
options:
|
|
39
|
+
- CLI (terminal)
|
|
40
|
+
- VS Code extension
|
|
41
|
+
- Desktop app
|
|
42
|
+
- type: textarea
|
|
43
|
+
id: logs
|
|
44
|
+
attributes:
|
|
45
|
+
label: Command output or logs
|
|
46
|
+
description: Paste the failing command's output — `cswap list` for account-state issues, `cswap auto` log lines for auto-switch issues. Formatted automatically, no backticks needed. Please redact emails/account identifiers, and never paste access/refresh tokens, API keys, setup tokens, or credential-file/Keychain contents.
|
|
47
|
+
render: shell
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest an improvement or new capability
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: textarea
|
|
6
|
+
id: problem
|
|
7
|
+
attributes:
|
|
8
|
+
label: What problem would this solve?
|
|
9
|
+
description: Describe the situation where you miss this feature — the workflow matters more than the mechanism.
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: solution
|
|
14
|
+
attributes:
|
|
15
|
+
label: What would you like to happen?
|
|
16
|
+
description: A rough sketch is fine — commands, flags, behavior.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
# uv (not pip) so the PEP 735 dev dependency-group — pytest,
|
|
17
|
+
# pytest-asyncio — installs from the committed lockfile, same as local
|
|
18
|
+
# development.
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: '3.12'
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync --locked
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: uv run pytest
|
|
29
|
+
|
|
30
|
+
test-windows:
|
|
31
|
+
runs-on: windows-latest
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: Install uv
|
|
37
|
+
uses: astral-sh/setup-uv@v6
|
|
38
|
+
with:
|
|
39
|
+
python-version: '3.12'
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: uv sync --locked
|
|
43
|
+
|
|
44
|
+
- name: Run tests
|
|
45
|
+
# Includes the TUI tests: the Textual TUI (unlike the old curses one)
|
|
46
|
+
# runs on stock Windows, and this job is the proof.
|
|
47
|
+
run: uv run pytest
|
|
48
|
+
|
|
49
|
+
# The id stays `macos-keychain` although the job now runs everything: it is
|
|
50
|
+
# the name branch protection matches on, and renaming it silently drops the
|
|
51
|
+
# required check.
|
|
52
|
+
macos-keychain:
|
|
53
|
+
runs-on: macos-latest
|
|
54
|
+
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v6
|
|
60
|
+
with:
|
|
61
|
+
python-version: '3.12'
|
|
62
|
+
|
|
63
|
+
- name: Install dependencies
|
|
64
|
+
run: uv sync --locked
|
|
65
|
+
|
|
66
|
+
- name: Run the whole suite (not only the Keychain files)
|
|
67
|
+
# THE NARROW SELECTION WAS A BLIND SPOT, not a saving. Three cases
|
|
68
|
+
# failed on macOS -- one asserting the Linux XDG layout, two because a
|
|
69
|
+
# usable Keychain takes the write and leaves no `.enc` to chmod --
|
|
70
|
+
# while every job stayed green, because this job never ran them. A merge that
|
|
71
|
+
# restores a two-file selection here re-opens that hole invisibly:
|
|
72
|
+
# those cases pass now, they would simply stop running.
|
|
73
|
+
#
|
|
74
|
+
# faulthandler dumps every thread's stack past the budget, the only
|
|
75
|
+
# clue when a `security` call blocks on an invisible SecurityAgent
|
|
76
|
+
# dialog. While pyproject sets no faulthandler keys this `-o` is the
|
|
77
|
+
# only thing setting the budget and the dump does not abort; PR 287
|
|
78
|
+
# adds them repo-wide at the same 600, after which this is a no-op
|
|
79
|
+
# and the dump aborts the worker. `timeout-minutes` is the backstop
|
|
80
|
+
# that holds under either ordering.
|
|
81
|
+
timeout-minutes: 15
|
|
82
|
+
run: uv run pytest -o faulthandler_timeout=600
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
# Re-run a release's upload by hand (e.g. after configuring PyPI trusted publishing).
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: '3.12'
|
|
22
|
+
|
|
23
|
+
- name: Install build dependencies
|
|
24
|
+
run: pip install build
|
|
25
|
+
|
|
26
|
+
- name: Build package
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# superpowers session planning artifacts (not part of the project)
|
|
13
|
+
docs/superpowers/
|
|
14
|
+
|
|
15
|
+
# graft's local graph cache — regenerable, not committed (run `graft build`).
|
|
16
|
+
/graft/
|
|
17
|
+
|
|
18
|
+
# oh-my-claudecode session state (local operational artifacts)
|
|
19
|
+
.omc/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"name": "cswap --list",
|
|
6
|
+
"type": "debugpy",
|
|
7
|
+
"request": "launch",
|
|
8
|
+
"module": "claude_swap.cli",
|
|
9
|
+
"args": ["--list"],
|
|
10
|
+
"cwd": "${workspaceFolder}",
|
|
11
|
+
"env": {
|
|
12
|
+
"PYTHONPATH": "${workspaceFolder}/src"
|
|
13
|
+
},
|
|
14
|
+
"console": "integratedTerminal"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "cswap --switch",
|
|
18
|
+
"type": "debugpy",
|
|
19
|
+
"request": "launch",
|
|
20
|
+
"module": "claude_swap.cli",
|
|
21
|
+
"args": ["--switch"],
|
|
22
|
+
"cwd": "${workspaceFolder}",
|
|
23
|
+
"env": {
|
|
24
|
+
"PYTHONPATH": "${workspaceFolder}/src"
|
|
25
|
+
},
|
|
26
|
+
"console": "integratedTerminal"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "cswap --purge",
|
|
30
|
+
"type": "debugpy",
|
|
31
|
+
"request": "launch",
|
|
32
|
+
"module": "claude_swap.cli",
|
|
33
|
+
"args": ["--purge"],
|
|
34
|
+
"cwd": "${workspaceFolder}",
|
|
35
|
+
"env": {
|
|
36
|
+
"PYTHONPATH": "${workspaceFolder}/src"
|
|
37
|
+
},
|
|
38
|
+
"console": "integratedTerminal"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "cswap (custom args)",
|
|
42
|
+
"type": "debugpy",
|
|
43
|
+
"request": "launch",
|
|
44
|
+
"module": "claude_swap.cli",
|
|
45
|
+
"args": ["${input:cliArgs}"],
|
|
46
|
+
"cwd": "${workspaceFolder}",
|
|
47
|
+
"env": {
|
|
48
|
+
"PYTHONPATH": "${workspaceFolder}/src"
|
|
49
|
+
},
|
|
50
|
+
"console": "integratedTerminal"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"inputs": [
|
|
54
|
+
{
|
|
55
|
+
"id": "cliArgs",
|
|
56
|
+
"type": "promptString",
|
|
57
|
+
"description": "CLI arguments (e.g., --list, --status, --switch)"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Onur Cetinkol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: harness-swap
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Switch between multiple accounts across AI coding harnesses, with automatic rate-limit rotation, a unified usage dashboard, and parallel sessions.
|
|
5
|
+
Project-URL: Homepage, https://github.com/realiti4/claude-swap
|
|
6
|
+
Project-URL: Repository, https://github.com/realiti4/claude-swap
|
|
7
|
+
Project-URL: Issues, https://github.com/realiti4/claude-swap/issues
|
|
8
|
+
Author-email: Onur Cetinkol <onurcetinkol@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: account-switcher,claude,claude-code,cli,codex,grok,opencode
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Requires-Dist: keyring>=25.0.0; sys_platform == 'win32'
|
|
26
|
+
Requires-Dist: textual<9,>=8.2.8
|
|
27
|
+
Requires-Dist: truststore>=0.10.4
|
|
28
|
+
Provides-Extra: menubar
|
|
29
|
+
Requires-Dist: rumps>=0.4.0; extra == 'menubar'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Harness Swap (`hswap`)
|
|
33
|
+
|
|
34
|
+
Switch between multiple accounts across AI coding harnesses, with automatic
|
|
35
|
+
rate-limit rotation, a unified usage dashboard, and parallel sessions.
|
|
36
|
+
|
|
37
|
+
`hswap` grew out of [claude-swap](https://github.com/realiti4/claude-swap). Its
|
|
38
|
+
Claude Code support *is* the claude-swap engine (unchanged apart from a small
|
|
39
|
+
embedding hook); Codex CLI, Grok Build, OpenCode and Antigravity are added through the same adapter contract any other
|
|
40
|
+
harness can implement.
|
|
41
|
+
|
|
42
|
+
| | Claude Code | Codex CLI | Grok Build | OpenCode | Antigravity (`agy`) |
|
|
43
|
+
|---|---|---|---|---|---|
|
|
44
|
+
| list / add / remove accounts | yes | yes | yes | yes (native) | yes⁵ |
|
|
45
|
+
| switch the default login | yes | yes¹ | yes | yes (native `auth switch`) | yes⁵ |
|
|
46
|
+
| running sessions pick up a switch | yes² | no, restart | yes | undocumented | no, restart |
|
|
47
|
+
| usage | 5h / 7d / per-model | experimental³ | experimental³ | experimental³ (Go plan) | experimental³ |
|
|
48
|
+
| automatic rotation | `hswap claude auto` | experimental⁴ | experimental⁴ | experimental⁴ | experimental⁴ |
|
|
49
|
+
| parallel isolated sessions | experimental | yes | experimental | no | experimental |
|
|
50
|
+
|
|
51
|
+
¹ file credential store only (`cli_auth_credentials_store = "file"`, Codex's default).
|
|
52
|
+
² on macOS once the Keychain cache expires.
|
|
53
|
+
³ opt-in: `hswap config set usage.remote <ids>`. hswap asks the harness itself
|
|
54
|
+
(Codex `app-server`, Grok's ACP agent, OpenCode `api experimental.usage.limits`,
|
|
55
|
+
`agy -p /usage`) and never calls a provider API or refreshes a token. Without the
|
|
56
|
+
opt-in Codex still shows the last snapshot it logged locally. Each harness's
|
|
57
|
+
answer for a real, active paid login was read once (2026-11-03, read-only) and
|
|
58
|
+
mapped correctly; inactive-account reads, keep-alive and automatic switching are
|
|
59
|
+
verified against fake harnesses only. OpenCode reports Go-plan limits only for OpenCode Console logins, not
|
|
60
|
+
API keys. Turning the opt-in on or off takes effect on the next read.
|
|
61
|
+
`list --cached` / `usage --cached` read the cache: no usage read, no token
|
|
62
|
+
refresh (hswap still runs each harness's version probe, and OpenCode's
|
|
63
|
+
`auth list`, to list accounts).
|
|
64
|
+
⁴ opt-in per harness: `hswap config set auto.enabled <ids>`, then
|
|
65
|
+
`hswap <harness> auto`. Needs live usage, and every provider's terms are
|
|
66
|
+
unclear or restrictive about switching accounts to get past limits (Google's
|
|
67
|
+
are the strictest); `hswap capabilities` and `config set` print the warning. Claude keeps
|
|
68
|
+
`hswap claude auto`, with its own threshold, JSON lines and exit codes
|
|
69
|
+
([docs/claude.md](docs/claude.md)).
|
|
70
|
+
⁵ file-backed login only (Linux; not the macOS/Windows keychain); `add --login`
|
|
71
|
+
is unavailable. Cursor and Gemini CLI are not supported. See
|
|
72
|
+
[docs/harness-compatibility.md](docs/harness-compatibility.md) for the evidence
|
|
73
|
+
and the reasons behind every cell. `hswap capabilities` prints the live answer
|
|
74
|
+
for *your* install.
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
hswap is not on PyPI yet. From a clone:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv tool install . # or: pipx install .
|
|
82
|
+
hswap harnesses # what is installed, which versions
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Python 3.12+. hswap does not install a `cswap` command, so it never collides
|
|
86
|
+
with an installed upstream claude-swap; `hswap claude ...` takes every cswap
|
|
87
|
+
command (and `python -m claude_swap` still runs the engine directly).
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
hswap # dashboard (when run in a terminal)
|
|
93
|
+
hswap list # every harness, every account, usage where available
|
|
94
|
+
hswap status # the active account per harness
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Claude Code
|
|
98
|
+
|
|
99
|
+
Exactly the claude-swap workflow, spelled `hswap claude`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
hswap claude add # add the account Claude Code is logged into
|
|
103
|
+
hswap claude switch 2 # or an email / alias
|
|
104
|
+
hswap claude auto # rotate before rate limits (threshold, priority, cooldown)
|
|
105
|
+
hswap claude run 2 # this terminal only, other sessions unaffected
|
|
106
|
+
hswap claude help # everything else: tui, map, alias, threshold, export, ...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Full guide: [docs/claude.md](docs/claude.md). Existing claude-swap data is used
|
|
110
|
+
in place — nothing to import.
|
|
111
|
+
|
|
112
|
+
### Codex CLI
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
hswap codex add # store the current login
|
|
116
|
+
hswap codex add --login --alias work # sign in another account in a throwaway CODEX_HOME;
|
|
117
|
+
# the current login is never replaced or logged out
|
|
118
|
+
hswap codex switch work # swaps $CODEX_HOME/auth.json; restart running Codex
|
|
119
|
+
hswap codex usage # last logged snapshot; live after `config set usage.remote codex`
|
|
120
|
+
hswap codex switch --strategy best # one-off: the account with the most headroom
|
|
121
|
+
hswap codex run personal -- resume # isolated per-account CODEX_HOME, this terminal only
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`run` on the account that is already the default login runs in the default
|
|
125
|
+
`CODEX_HOME` (a second copy of the token would mean two refreshers); hswap still
|
|
126
|
+
holds a session lock there, so `switch` and `remove` wait until it exits.
|
|
127
|
+
|
|
128
|
+
### Grok Build
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
hswap grok add --login # isolated GROK_HOME sign-in
|
|
132
|
+
hswap grok switch 1 # leaves that login as the only scope; Grok hot-reloads it
|
|
133
|
+
hswap grok run 2 # experimental isolated GROK_HOME session
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Antigravity (`agy`)
|
|
137
|
+
|
|
138
|
+
File-backed login only (`~/.gemini/antigravity-cli/antigravity-oauth-token`).
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
hswap agy add # the current login; sign in the next account in agy, add again
|
|
142
|
+
hswap agy switch 2 # swaps the token file; restart running agy sessions
|
|
143
|
+
hswap agy run 2 -- --continue # experimental: per-account --gemini_dir
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Usage and automatic switching (opt-in)
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
hswap config set usage.remote codex,grok,opencode,agy # ask each harness for live usage
|
|
150
|
+
hswap list --cached # no network, no token refresh
|
|
151
|
+
hswap config set auto.enabled codex # allow automatic switching
|
|
152
|
+
hswap codex auto [--once | --interval S] [--dry-run] [--json]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Keep logins alive
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
hswap codex refresh [ACCOUNT] [--json] # also: grok, agy
|
|
159
|
+
# crontab: keep stored logins fresh on a machine you rarely use
|
|
160
|
+
0 */6 * * * hswap codex refresh
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`refresh` asks the harness (in each account's own home) so the harness refreshes
|
|
164
|
+
a due token itself; hswap never calls a provider. It needs no opt-in, skips an
|
|
165
|
+
account a running session owns and the active login while that harness runs (any
|
|
166
|
+
process whose command line names the harness executable counts, e.g. a sandbox
|
|
167
|
+
that binds it), and
|
|
168
|
+
prints `token refreshed` / `token still valid` (agy: `login used`), or
|
|
169
|
+
`sign in again: ...` with exit 1. OpenCode is unsupported: switch to an idle
|
|
170
|
+
account (`hswap opencode switch`) before its token lapses. Claude: the engine
|
|
171
|
+
refreshes tokens when it reads usage, so run `hswap claude list` or
|
|
172
|
+
`hswap claude auto --once` from cron. Verified against fake harnesses only, never
|
|
173
|
+
a live account. Why it is safe with several devices:
|
|
174
|
+
[docs/security.md](docs/security.md#keep-alive-hswap-id-refresh).
|
|
175
|
+
|
|
176
|
+
`auto` switches only on fresh usage at or over `rotation.threshold`, at most once
|
|
177
|
+
per `auto.cooldown` (default 600 s), and never on missing data. Read the terms
|
|
178
|
+
warning in `hswap capabilities` first.
|
|
179
|
+
|
|
180
|
+
### OpenCode
|
|
181
|
+
|
|
182
|
+
OpenCode 2.x keeps several saved accounts per provider itself; hswap drives its
|
|
183
|
+
`opencode auth` commands and never edits its database.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
hswap opencode list
|
|
187
|
+
hswap opencode add --provider openai # runs `opencode auth login openai`
|
|
188
|
+
hswap opencode switch 3 # runs `opencode auth switch <provider> <id>`
|
|
189
|
+
hswap opencode switch work --provider anthropic
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
OpenCode 1.x (legacy `auth.json`) is handled by swapping one provider's entry at
|
|
193
|
+
a time (experimental, mock-verified).
|
|
194
|
+
|
|
195
|
+
## Commands
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
hswap harnesses | list [H] | status [H] | capabilities [H] | doctor | migrate [--dry-run]
|
|
199
|
+
hswap config [list|get|set|unset|path] plugins.trusted, rotation.threshold, usage.remote, auto.enabled, auto.cooldown
|
|
200
|
+
hswap tui
|
|
201
|
+
|
|
202
|
+
hswap <harness> list [--json] [--no-usage] [--cached]
|
|
203
|
+
hswap <harness> status | usage [ACCOUNT] [--json] [--cached]
|
|
204
|
+
hswap <harness> auto [--once|--interval S] [--dry-run] [--json] # opt-in
|
|
205
|
+
hswap <harness> add [--alias A] [--provider P] [--login]
|
|
206
|
+
hswap <harness> switch ACCOUNT [--provider P] [--force] [--json]
|
|
207
|
+
hswap <harness> switch --strategy best|next-available
|
|
208
|
+
hswap <harness> run ACCOUNT [-- harness args...]
|
|
209
|
+
hswap <harness> alias ACCOUNT NAME | --unset
|
|
210
|
+
hswap <harness> remove ACCOUNT [--yes]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
These generic verbs apply to every harness except Claude Code, whose
|
|
214
|
+
`hswap claude ...` takes the full claude-swap command set instead (`hswap claude
|
|
215
|
+
help`); the cross-harness commands (`hswap list claude`, `hswap capabilities
|
|
216
|
+
claude`, ...) work for Claude too.
|
|
217
|
+
|
|
218
|
+
`ACCOUNT` is a slot number, an alias, or an email/label. Unsupported operations
|
|
219
|
+
fail with the reason (`hswap capabilities` lists them all). Every `--json`
|
|
220
|
+
payload carries `schemaVersion: 1`; Claude's own `--json` keeps claude-swap's
|
|
221
|
+
schema. Cross-harness commands (`list`, `status`, `capabilities`, `doctor`,
|
|
222
|
+
`harnesses`) still show every harness that works when one fails, report the
|
|
223
|
+
failure (`errors` in JSON), and exit 1.
|
|
224
|
+
|
|
225
|
+
- `remove` forgets the stored login. The account's isolated-session history
|
|
226
|
+
stays in `<hswap home>/<harness>/accounts/<slot>/`; delete that directory to
|
|
227
|
+
discard it.
|
|
228
|
+
- `switch --force` saves the login it replaces to
|
|
229
|
+
`<hswap home>/<harness>/stash/<file>.<timestamp>` (0600) and prints the path.
|
|
230
|
+
To get it back, copy it over the harness's login file (e.g.
|
|
231
|
+
`$CODEX_HOME/auth.json`) and `hswap <harness> add` it.
|
|
232
|
+
- `rotation.threshold` (default 95%) is the cut-off for `switch --strategy` on
|
|
233
|
+
the generic harnesses. Claude's auto-switch has its own settings
|
|
234
|
+
(`hswap claude config`, default `autoswitch.threshold` 90%, per-account
|
|
235
|
+
`hswap claude threshold`).
|
|
236
|
+
|
|
237
|
+
## How it stays safe
|
|
238
|
+
|
|
239
|
+
- Credentials never appear in output: core models carry no secrets, and hswap
|
|
240
|
+
refuses to print JSON that looks like it contains a token. The one exception
|
|
241
|
+
is the explicit, plaintext `hswap claude export -`.
|
|
242
|
+
- Before overwriting a login hswap copies the harness's freshly rotated tokens
|
|
243
|
+
back into the stored account, refuses to overwrite a login it does not manage
|
|
244
|
+
(`--force` stashes it first, 0600), leaves other providers' entries alone
|
|
245
|
+
(Grok: the target becomes the only login scope, the others are saved first),
|
|
246
|
+
writes atomically at 0600 under hswap's lock and the harness's own lock, re-reads
|
|
247
|
+
the result and restores the original on any failure.
|
|
248
|
+
- For Codex and Grok, an account running in a `run` session cannot be
|
|
249
|
+
activated globally (or removed) until that session exits — two processes
|
|
250
|
+
would rotate one refresh token — and a `run` on the default login blocks
|
|
251
|
+
switching away from it. Claude's `run` keeps the claude-swap rules
|
|
252
|
+
(see docs/claude.md).
|
|
253
|
+
- Plugins (third-party adapters) load only after you trust that id from that
|
|
254
|
+
package: `hswap config set plugins.trusted <id>@<distribution>` (the command
|
|
255
|
+
`hswap harnesses` prints). See [docs/adapter-development.md](docs/adapter-development.md).
|
|
256
|
+
|
|
257
|
+
Details: [docs/security.md](docs/security.md).
|
|
258
|
+
|
|
259
|
+
## Documentation
|
|
260
|
+
|
|
261
|
+
- [Architecture](docs/architecture.md) — core, adapters, capability model
|
|
262
|
+
- [Writing an adapter](docs/adapter-development.md) — with a runnable example
|
|
263
|
+
- [Harness compatibility](docs/harness-compatibility.md) — what is verified, how
|
|
264
|
+
- [Security](docs/security.md) · [Migration from claude-swap](docs/migration.md) · [Testing](docs/testing.md)
|
|
265
|
+
- [Claude Code guide](docs/claude.md)
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|