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.
Files changed (167) hide show
  1. harness_swap-0.2.0/.github/FUNDING.yml +1 -0
  2. harness_swap-0.2.0/.github/ISSUE_TEMPLATE/bug_report.yml +47 -0
  3. harness_swap-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. harness_swap-0.2.0/.github/ISSUE_TEMPLATE/feature_request.yml +16 -0
  5. harness_swap-0.2.0/.github/workflows/ci.yml +82 -0
  6. harness_swap-0.2.0/.github/workflows/publish.yml +30 -0
  7. harness_swap-0.2.0/.gitignore +19 -0
  8. harness_swap-0.2.0/.ignore +5 -0
  9. harness_swap-0.2.0/.python-version +1 -0
  10. harness_swap-0.2.0/.vscode/launch.json +60 -0
  11. harness_swap-0.2.0/LICENSE +21 -0
  12. harness_swap-0.2.0/PKG-INFO +269 -0
  13. harness_swap-0.2.0/README.md +238 -0
  14. harness_swap-0.2.0/assets/tui-watch.png +0 -0
  15. harness_swap-0.2.0/docs/adapter-development.md +310 -0
  16. harness_swap-0.2.0/docs/architecture.md +211 -0
  17. harness_swap-0.2.0/docs/claude.md +393 -0
  18. harness_swap-0.2.0/docs/harness-compatibility.md +255 -0
  19. harness_swap-0.2.0/docs/migration.md +81 -0
  20. harness_swap-0.2.0/docs/security.md +195 -0
  21. harness_swap-0.2.0/docs/testing.md +88 -0
  22. harness_swap-0.2.0/examples/acme_adapter.py +108 -0
  23. harness_swap-0.2.0/pyproject.toml +75 -0
  24. harness_swap-0.2.0/src/claude_swap/__init__.py +10 -0
  25. harness_swap-0.2.0/src/claude_swap/__main__.py +6 -0
  26. harness_swap-0.2.0/src/claude_swap/app_bundle.py +310 -0
  27. harness_swap-0.2.0/src/claude_swap/appearance.py +218 -0
  28. harness_swap-0.2.0/src/claude_swap/autoswitch.py +2737 -0
  29. harness_swap-0.2.0/src/claude_swap/cache.py +44 -0
  30. harness_swap-0.2.0/src/claude_swap/claude_locks.py +228 -0
  31. harness_swap-0.2.0/src/claude_swap/cli.py +1759 -0
  32. harness_swap-0.2.0/src/claude_swap/credentials.py +1968 -0
  33. harness_swap-0.2.0/src/claude_swap/exceptions.py +98 -0
  34. harness_swap-0.2.0/src/claude_swap/fsutil.py +12 -0
  35. harness_swap-0.2.0/src/claude_swap/json_output.py +370 -0
  36. harness_swap-0.2.0/src/claude_swap/launch_agent.py +321 -0
  37. harness_swap-0.2.0/src/claude_swap/lines.py +126 -0
  38. harness_swap-0.2.0/src/claude_swap/locking.py +17 -0
  39. harness_swap-0.2.0/src/claude_swap/logging_config.py +64 -0
  40. harness_swap-0.2.0/src/claude_swap/macos_keychain.py +228 -0
  41. harness_swap-0.2.0/src/claude_swap/mappings.py +127 -0
  42. harness_swap-0.2.0/src/claude_swap/menubar.py +1067 -0
  43. harness_swap-0.2.0/src/claude_swap/migrations.py +520 -0
  44. harness_swap-0.2.0/src/claude_swap/models.py +284 -0
  45. harness_swap-0.2.0/src/claude_swap/oauth.py +813 -0
  46. harness_swap-0.2.0/src/claude_swap/pace.py +161 -0
  47. harness_swap-0.2.0/src/claude_swap/paths.py +236 -0
  48. harness_swap-0.2.0/src/claude_swap/poll_policy.py +285 -0
  49. harness_swap-0.2.0/src/claude_swap/printer.py +232 -0
  50. harness_swap-0.2.0/src/claude_swap/process_detection.py +370 -0
  51. harness_swap-0.2.0/src/claude_swap/session.py +1522 -0
  52. harness_swap-0.2.0/src/claude_swap/settings.py +563 -0
  53. harness_swap-0.2.0/src/claude_swap/snapshot_source.py +100 -0
  54. harness_swap-0.2.0/src/claude_swap/spawn.py +11 -0
  55. harness_swap-0.2.0/src/claude_swap/switcher.py +7741 -0
  56. harness_swap-0.2.0/src/claude_swap/transfer.py +746 -0
  57. harness_swap-0.2.0/src/claude_swap/tui/__init__.py +43 -0
  58. harness_swap-0.2.0/src/claude_swap/tui/app.py +436 -0
  59. harness_swap-0.2.0/src/claude_swap/tui/autoview.py +369 -0
  60. harness_swap-0.2.0/src/claude_swap/tui/cswap.tcss +205 -0
  61. harness_swap-0.2.0/src/claude_swap/tui/dashboard.py +432 -0
  62. harness_swap-0.2.0/src/claude_swap/tui/data.py +194 -0
  63. harness_swap-0.2.0/src/claude_swap/tui/modals.py +159 -0
  64. harness_swap-0.2.0/src/claude_swap/tui/theme.py +141 -0
  65. harness_swap-0.2.0/src/claude_swap/tui/widgets.py +436 -0
  66. harness_swap-0.2.0/src/claude_swap/update_check.py +417 -0
  67. harness_swap-0.2.0/src/claude_swap/usage_store.py +1326 -0
  68. harness_swap-0.2.0/src/harness_swap/__init__.py +1 -0
  69. harness_swap-0.2.0/src/harness_swap/__main__.py +6 -0
  70. harness_swap-0.2.0/src/harness_swap/adapters/__init__.py +14 -0
  71. harness_swap-0.2.0/src/harness_swap/adapters/agy.py +336 -0
  72. harness_swap-0.2.0/src/harness_swap/adapters/base.py +317 -0
  73. harness_swap-0.2.0/src/harness_swap/adapters/claude.py +433 -0
  74. harness_swap-0.2.0/src/harness_swap/adapters/codex.py +492 -0
  75. harness_swap-0.2.0/src/harness_swap/adapters/grok.py +346 -0
  76. harness_swap-0.2.0/src/harness_swap/adapters/opencode.py +582 -0
  77. harness_swap-0.2.0/src/harness_swap/cli.py +684 -0
  78. harness_swap-0.2.0/src/harness_swap/core/__init__.py +0 -0
  79. harness_swap-0.2.0/src/harness_swap/core/accounts.py +158 -0
  80. harness_swap-0.2.0/src/harness_swap/core/adapter.py +196 -0
  81. harness_swap-0.2.0/src/harness_swap/core/autorotate.py +124 -0
  82. harness_swap-0.2.0/src/harness_swap/core/credentials.py +408 -0
  83. harness_swap-0.2.0/src/harness_swap/core/errors.py +58 -0
  84. harness_swap-0.2.0/src/harness_swap/core/models.py +255 -0
  85. harness_swap-0.2.0/src/harness_swap/core/paths.py +36 -0
  86. harness_swap-0.2.0/src/harness_swap/core/registry.py +233 -0
  87. harness_swap-0.2.0/src/harness_swap/core/rotation.py +83 -0
  88. harness_swap-0.2.0/src/harness_swap/core/sessions.py +142 -0
  89. harness_swap-0.2.0/src/harness_swap/core/settings.py +152 -0
  90. harness_swap-0.2.0/src/harness_swap/core/store.py +243 -0
  91. harness_swap-0.2.0/src/harness_swap/core/usage.py +193 -0
  92. harness_swap-0.2.0/src/harness_swap/infrastructure/__init__.py +0 -0
  93. harness_swap-0.2.0/src/harness_swap/infrastructure/fsutil.py +249 -0
  94. harness_swap-0.2.0/src/harness_swap/infrastructure/jsonrpc.py +148 -0
  95. harness_swap-0.2.0/src/harness_swap/infrastructure/locking.py +97 -0
  96. harness_swap-0.2.0/src/harness_swap/infrastructure/processes.py +65 -0
  97. harness_swap-0.2.0/src/harness_swap/infrastructure/secrets.py +23 -0
  98. harness_swap-0.2.0/src/harness_swap/infrastructure/spawn.py +135 -0
  99. harness_swap-0.2.0/src/harness_swap/output.py +82 -0
  100. harness_swap-0.2.0/src/harness_swap/ui/__init__.py +0 -0
  101. harness_swap-0.2.0/src/harness_swap/ui/tui.py +167 -0
  102. harness_swap-0.2.0/tests/__init__.py +1 -0
  103. harness_swap-0.2.0/tests/conftest.py +910 -0
  104. harness_swap-0.2.0/tests/fixtures/sample_config.json +10 -0
  105. harness_swap-0.2.0/tests/hswap_helpers.py +75 -0
  106. harness_swap-0.2.0/tests/test_account_policy_cli.py +236 -0
  107. harness_swap-0.2.0/tests/test_account_policy_store.py +470 -0
  108. harness_swap-0.2.0/tests/test_account_policy_stray_standby_key.py +118 -0
  109. harness_swap-0.2.0/tests/test_account_policy_values.py +254 -0
  110. harness_swap-0.2.0/tests/test_add_account_identity.py +720 -0
  111. harness_swap-0.2.0/tests/test_api_key_accounts.py +1153 -0
  112. harness_swap-0.2.0/tests/test_app_bundle.py +279 -0
  113. harness_swap-0.2.0/tests/test_appearance.py +331 -0
  114. harness_swap-0.2.0/tests/test_autoswitch.py +7202 -0
  115. harness_swap-0.2.0/tests/test_autoswitch_account_threshold.py +1318 -0
  116. harness_swap-0.2.0/tests/test_backup_read_pacing.py +494 -0
  117. harness_swap-0.2.0/tests/test_cache.py +72 -0
  118. harness_swap-0.2.0/tests/test_claude_locks.py +314 -0
  119. harness_swap-0.2.0/tests/test_cli.py +1968 -0
  120. harness_swap-0.2.0/tests/test_colour_cache_isolation.py +196 -0
  121. harness_swap-0.2.0/tests/test_config_cli.py +333 -0
  122. harness_swap-0.2.0/tests/test_credentials.py +274 -0
  123. harness_swap-0.2.0/tests/test_fsutil.py +566 -0
  124. harness_swap-0.2.0/tests/test_hswap_agy.py +284 -0
  125. harness_swap-0.2.0/tests/test_hswap_auto.py +165 -0
  126. harness_swap-0.2.0/tests/test_hswap_claude_adapter.py +328 -0
  127. harness_swap-0.2.0/tests/test_hswap_cli.py +469 -0
  128. harness_swap-0.2.0/tests/test_hswap_codex.py +682 -0
  129. harness_swap-0.2.0/tests/test_hswap_contract.py +208 -0
  130. harness_swap-0.2.0/tests/test_hswap_core.py +539 -0
  131. harness_swap-0.2.0/tests/test_hswap_credentials.py +347 -0
  132. harness_swap-0.2.0/tests/test_hswap_grok.py +462 -0
  133. harness_swap-0.2.0/tests/test_hswap_jsonrpc.py +84 -0
  134. harness_swap-0.2.0/tests/test_hswap_opencode.py +557 -0
  135. harness_swap-0.2.0/tests/test_hswap_sessions.py +74 -0
  136. harness_swap-0.2.0/tests/test_json_output.py +803 -0
  137. harness_swap-0.2.0/tests/test_launch_agent.py +447 -0
  138. harness_swap-0.2.0/tests/test_lines.py +211 -0
  139. harness_swap-0.2.0/tests/test_locking.py +162 -0
  140. harness_swap-0.2.0/tests/test_logging_config.py +59 -0
  141. harness_swap-0.2.0/tests/test_macos_keychain.py +212 -0
  142. harness_swap-0.2.0/tests/test_macos_keychain_contract.py +1213 -0
  143. harness_swap-0.2.0/tests/test_mappings.py +204 -0
  144. harness_swap-0.2.0/tests/test_menubar.py +619 -0
  145. harness_swap-0.2.0/tests/test_migrations.py +596 -0
  146. harness_swap-0.2.0/tests/test_move_accounts.py +578 -0
  147. harness_swap-0.2.0/tests/test_oauth.py +1524 -0
  148. harness_swap-0.2.0/tests/test_pace.py +169 -0
  149. harness_swap-0.2.0/tests/test_paths.py +349 -0
  150. harness_swap-0.2.0/tests/test_poll_policy.py +435 -0
  151. harness_swap-0.2.0/tests/test_printer.py +308 -0
  152. harness_swap-0.2.0/tests/test_priority_strategy.py +401 -0
  153. harness_swap-0.2.0/tests/test_process_detection.py +731 -0
  154. harness_swap-0.2.0/tests/test_real_store_guard.py +912 -0
  155. harness_swap-0.2.0/tests/test_session.py +3011 -0
  156. harness_swap-0.2.0/tests/test_settings.py +493 -0
  157. harness_swap-0.2.0/tests/test_spawn.py +306 -0
  158. harness_swap-0.2.0/tests/test_swap_accounts.py +726 -0
  159. harness_swap-0.2.0/tests/test_switcher.py +12908 -0
  160. harness_swap-0.2.0/tests/test_theme.py +67 -0
  161. harness_swap-0.2.0/tests/test_transfer.py +2226 -0
  162. harness_swap-0.2.0/tests/test_tui.py +1833 -0
  163. harness_swap-0.2.0/tests/test_tui_policy_display.py +220 -0
  164. harness_swap-0.2.0/tests/test_update_check.py +810 -0
  165. harness_swap-0.2.0/tests/test_usage_store.py +1873 -0
  166. harness_swap-0.2.0/tests/test_worker_gc_freeze.py +79 -0
  167. 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,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: 📖 Tips & how it works
4
+ url: https://github.com/realiti4/claude-swap#tips
5
+ about: Restart-after-switch behavior, macOS Keychain caching, and other common gotchas are covered in the README.
@@ -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,5 @@
1
+ # graft's cards are gitignored but should stay greppable: ripgrep reads
2
+ # .ignore before .gitignore, so this re-admits the tree to search only.
3
+ !graft/
4
+ graft/.cache/
5
+ graft/.graph/
@@ -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