altergo 0.1.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.
altergo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thepixelabs
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.
altergo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: altergo
3
+ Version: 0.1.0
4
+ Summary: Your other Claude — switch Claude Code identities without losing a thought
5
+ Author: thepixelabs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://altergo.pixelabs.net
8
+ Project-URL: Repository, https://github.com/thepixelabs/altergo
9
+ Project-URL: Issues, https://github.com/thepixelabs/altergo/issues
10
+ Keywords: claude,claude-code,multi-account,session-manager,cli
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Console :: Curses
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # altergo
31
+
32
+ > Your other Claude — switch Claude Code identities without losing a thought.
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/altergo)](https://pypi.org/project/altergo/)
35
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
37
+ [![CI](https://github.com/thepixelabs/altergo/actions/workflows/ci.yml/badge.svg)](https://github.com/thepixelabs/altergo/actions/workflows/ci.yml)
38
+
39
+ <!-- TODO: Add demo GIF here -->
40
+
41
+ ## What is this?
42
+
43
+ If you have multiple Claude Code subscriptions (personal + work, two orgs, etc.), switching between them means losing access to your session history. **Altergo** fixes that — it shares session data via symlinks while keeping credentials separate, so you can pick up any conversation from either account.
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ pip install altergo
49
+ altergo --setup
50
+ altergo
51
+ ```
52
+
53
+ ## Features
54
+
55
+ - **Interactive session picker** — curses TUI with arrow keys, j/k, page up/down
56
+ - **Session preview** — see project name, last modified, size, and last message
57
+ - **Zero dependencies** — Python standard library only
58
+ - **Cross-platform** — macOS and Linux
59
+ - **Automatic setup/teardown** — one command to configure, one to undo
60
+
61
+ ## How it works
62
+
63
+ ```
64
+ ~/.claude/ ← Your primary Claude Code account
65
+ ├── .credentials.json ← Primary credentials (untouched)
66
+ ├── projects/ ← Session files
67
+ ├── settings.json
68
+ └── ...
69
+
70
+ ~/.altergo/ ← Your alt account
71
+ ├── .claude/
72
+ │ ├── .credentials.json ← Alt credentials (separate)
73
+ │ ├── projects/ → symlink to ~/.claude/projects/
74
+ │ ├── settings.json → symlink
75
+ │ └── ... ← All session dirs are symlinked
76
+ └── ...
77
+ ```
78
+
79
+ Both accounts see the same sessions. Only credentials stay separate.
80
+
81
+ ## Usage
82
+
83
+ ```
84
+ altergo Interactive session picker
85
+ altergo new Start a new session with alt credentials
86
+ altergo --resume <id> Resume a specific session
87
+ altergo --list List all sessions
88
+ altergo --setup First-time setup (alt home + symlinks)
89
+ altergo --teardown Undo setup
90
+ altergo --version Show version
91
+ altergo --help Show help
92
+ ```
93
+
94
+ ### Keyboard shortcuts (interactive picker)
95
+
96
+ | Key | Action |
97
+ |-----|--------|
98
+ | `↑` / `k` | Move up |
99
+ | `↓` / `j` | Move down |
100
+ | `PgUp` / `PgDn` | Page scroll |
101
+ | `g` / `G` | Jump to top / bottom |
102
+ | `Enter` | Resume session |
103
+ | `q` / `Esc` | Quit |
104
+
105
+ ## Install
106
+
107
+ ### pip (recommended)
108
+
109
+ ```bash
110
+ pip install altergo
111
+ ```
112
+
113
+ ### Homebrew
114
+
115
+ ```bash
116
+ brew install thepixelabs/tap/altergo
117
+ ```
118
+
119
+ ### Manual
120
+
121
+ ```bash
122
+ curl -o ~/.local/bin/altergo https://raw.githubusercontent.com/thepixelabs/altergo/main/altergo.py
123
+ chmod +x ~/.local/bin/altergo
124
+ ```
125
+
126
+ ## Requirements
127
+
128
+ - Python 3.9+
129
+ - [Claude Code](https://claude.ai/code) CLI installed
130
+ - macOS or Linux
131
+
132
+ ## Contributing
133
+
134
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
135
+
136
+ ## Migrating from claude100-resume
137
+
138
+ If you used the previous `claude100-resume` tool with `~/claude100-home/`, your credentials and alias will not be picked up automatically. See [docs/migration.md](docs/migration.md) for step-by-step instructions.
139
+
140
+ ## License
141
+
142
+ [MIT](LICENSE)
@@ -0,0 +1,113 @@
1
+ # altergo
2
+
3
+ > Your other Claude — switch Claude Code identities without losing a thought.
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/altergo)](https://pypi.org/project/altergo/)
6
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+ [![CI](https://github.com/thepixelabs/altergo/actions/workflows/ci.yml/badge.svg)](https://github.com/thepixelabs/altergo/actions/workflows/ci.yml)
9
+
10
+ <!-- TODO: Add demo GIF here -->
11
+
12
+ ## What is this?
13
+
14
+ If you have multiple Claude Code subscriptions (personal + work, two orgs, etc.), switching between them means losing access to your session history. **Altergo** fixes that — it shares session data via symlinks while keeping credentials separate, so you can pick up any conversation from either account.
15
+
16
+ ## Quick start
17
+
18
+ ```bash
19
+ pip install altergo
20
+ altergo --setup
21
+ altergo
22
+ ```
23
+
24
+ ## Features
25
+
26
+ - **Interactive session picker** — curses TUI with arrow keys, j/k, page up/down
27
+ - **Session preview** — see project name, last modified, size, and last message
28
+ - **Zero dependencies** — Python standard library only
29
+ - **Cross-platform** — macOS and Linux
30
+ - **Automatic setup/teardown** — one command to configure, one to undo
31
+
32
+ ## How it works
33
+
34
+ ```
35
+ ~/.claude/ ← Your primary Claude Code account
36
+ ├── .credentials.json ← Primary credentials (untouched)
37
+ ├── projects/ ← Session files
38
+ ├── settings.json
39
+ └── ...
40
+
41
+ ~/.altergo/ ← Your alt account
42
+ ├── .claude/
43
+ │ ├── .credentials.json ← Alt credentials (separate)
44
+ │ ├── projects/ → symlink to ~/.claude/projects/
45
+ │ ├── settings.json → symlink
46
+ │ └── ... ← All session dirs are symlinked
47
+ └── ...
48
+ ```
49
+
50
+ Both accounts see the same sessions. Only credentials stay separate.
51
+
52
+ ## Usage
53
+
54
+ ```
55
+ altergo Interactive session picker
56
+ altergo new Start a new session with alt credentials
57
+ altergo --resume <id> Resume a specific session
58
+ altergo --list List all sessions
59
+ altergo --setup First-time setup (alt home + symlinks)
60
+ altergo --teardown Undo setup
61
+ altergo --version Show version
62
+ altergo --help Show help
63
+ ```
64
+
65
+ ### Keyboard shortcuts (interactive picker)
66
+
67
+ | Key | Action |
68
+ |-----|--------|
69
+ | `↑` / `k` | Move up |
70
+ | `↓` / `j` | Move down |
71
+ | `PgUp` / `PgDn` | Page scroll |
72
+ | `g` / `G` | Jump to top / bottom |
73
+ | `Enter` | Resume session |
74
+ | `q` / `Esc` | Quit |
75
+
76
+ ## Install
77
+
78
+ ### pip (recommended)
79
+
80
+ ```bash
81
+ pip install altergo
82
+ ```
83
+
84
+ ### Homebrew
85
+
86
+ ```bash
87
+ brew install thepixelabs/tap/altergo
88
+ ```
89
+
90
+ ### Manual
91
+
92
+ ```bash
93
+ curl -o ~/.local/bin/altergo https://raw.githubusercontent.com/thepixelabs/altergo/main/altergo.py
94
+ chmod +x ~/.local/bin/altergo
95
+ ```
96
+
97
+ ## Requirements
98
+
99
+ - Python 3.9+
100
+ - [Claude Code](https://claude.ai/code) CLI installed
101
+ - macOS or Linux
102
+
103
+ ## Contributing
104
+
105
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
106
+
107
+ ## Migrating from claude100-resume
108
+
109
+ If you used the previous `claude100-resume` tool with `~/claude100-home/`, your credentials and alias will not be picked up automatically. See [docs/migration.md](docs/migration.md) for step-by-step instructions.
110
+
111
+ ## License
112
+
113
+ [MIT](LICENSE)
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: altergo
3
+ Version: 0.1.0
4
+ Summary: Your other Claude — switch Claude Code identities without losing a thought
5
+ Author: thepixelabs
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://altergo.pixelabs.net
8
+ Project-URL: Repository, https://github.com/thepixelabs/altergo
9
+ Project-URL: Issues, https://github.com/thepixelabs/altergo/issues
10
+ Keywords: claude,claude-code,multi-account,session-manager,cli
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Console :: Curses
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # altergo
31
+
32
+ > Your other Claude — switch Claude Code identities without losing a thought.
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/altergo)](https://pypi.org/project/altergo/)
35
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://python.org)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
37
+ [![CI](https://github.com/thepixelabs/altergo/actions/workflows/ci.yml/badge.svg)](https://github.com/thepixelabs/altergo/actions/workflows/ci.yml)
38
+
39
+ <!-- TODO: Add demo GIF here -->
40
+
41
+ ## What is this?
42
+
43
+ If you have multiple Claude Code subscriptions (personal + work, two orgs, etc.), switching between them means losing access to your session history. **Altergo** fixes that — it shares session data via symlinks while keeping credentials separate, so you can pick up any conversation from either account.
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ pip install altergo
49
+ altergo --setup
50
+ altergo
51
+ ```
52
+
53
+ ## Features
54
+
55
+ - **Interactive session picker** — curses TUI with arrow keys, j/k, page up/down
56
+ - **Session preview** — see project name, last modified, size, and last message
57
+ - **Zero dependencies** — Python standard library only
58
+ - **Cross-platform** — macOS and Linux
59
+ - **Automatic setup/teardown** — one command to configure, one to undo
60
+
61
+ ## How it works
62
+
63
+ ```
64
+ ~/.claude/ ← Your primary Claude Code account
65
+ ├── .credentials.json ← Primary credentials (untouched)
66
+ ├── projects/ ← Session files
67
+ ├── settings.json
68
+ └── ...
69
+
70
+ ~/.altergo/ ← Your alt account
71
+ ├── .claude/
72
+ │ ├── .credentials.json ← Alt credentials (separate)
73
+ │ ├── projects/ → symlink to ~/.claude/projects/
74
+ │ ├── settings.json → symlink
75
+ │ └── ... ← All session dirs are symlinked
76
+ └── ...
77
+ ```
78
+
79
+ Both accounts see the same sessions. Only credentials stay separate.
80
+
81
+ ## Usage
82
+
83
+ ```
84
+ altergo Interactive session picker
85
+ altergo new Start a new session with alt credentials
86
+ altergo --resume <id> Resume a specific session
87
+ altergo --list List all sessions
88
+ altergo --setup First-time setup (alt home + symlinks)
89
+ altergo --teardown Undo setup
90
+ altergo --version Show version
91
+ altergo --help Show help
92
+ ```
93
+
94
+ ### Keyboard shortcuts (interactive picker)
95
+
96
+ | Key | Action |
97
+ |-----|--------|
98
+ | `↑` / `k` | Move up |
99
+ | `↓` / `j` | Move down |
100
+ | `PgUp` / `PgDn` | Page scroll |
101
+ | `g` / `G` | Jump to top / bottom |
102
+ | `Enter` | Resume session |
103
+ | `q` / `Esc` | Quit |
104
+
105
+ ## Install
106
+
107
+ ### pip (recommended)
108
+
109
+ ```bash
110
+ pip install altergo
111
+ ```
112
+
113
+ ### Homebrew
114
+
115
+ ```bash
116
+ brew install thepixelabs/tap/altergo
117
+ ```
118
+
119
+ ### Manual
120
+
121
+ ```bash
122
+ curl -o ~/.local/bin/altergo https://raw.githubusercontent.com/thepixelabs/altergo/main/altergo.py
123
+ chmod +x ~/.local/bin/altergo
124
+ ```
125
+
126
+ ## Requirements
127
+
128
+ - Python 3.9+
129
+ - [Claude Code](https://claude.ai/code) CLI installed
130
+ - macOS or Linux
131
+
132
+ ## Contributing
133
+
134
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
135
+
136
+ ## Migrating from claude100-resume
137
+
138
+ If you used the previous `claude100-resume` tool with `~/claude100-home/`, your credentials and alias will not be picked up automatically. See [docs/migration.md](docs/migration.md) for step-by-step instructions.
139
+
140
+ ## License
141
+
142
+ [MIT](LICENSE)
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ altergo.py
4
+ pyproject.toml
5
+ altergo.egg-info/PKG-INFO
6
+ altergo.egg-info/SOURCES.txt
7
+ altergo.egg-info/dependency_links.txt
8
+ altergo.egg-info/entry_points.txt
9
+ altergo.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ altergo = altergo:main
@@ -0,0 +1 @@
1
+ altergo
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Altergo — Your other Claude.
4
+
5
+ Multi-account session manager for Claude Code. Switch between Claude Code
6
+ identities without losing a thought. Uses symlinks to share session data
7
+ and a separate HOME for alt account credentials.
8
+
9
+ Usage:
10
+ altergo [claude flags...] Launch claude with alt credentials (pass-through)
11
+ altergo --resume Pick a session interactively (↑/↓/j/k, Enter, q)
12
+ altergo --resume <id> Resume a specific session directly
13
+ altergo --list List recent sessions
14
+ altergo --setup First-time setup (alt home, symlinks)
15
+ altergo --teardown Remove symlinks and undo setup
16
+ altergo --version Show version
17
+ altergo -h, --help Show this help
18
+
19
+ Examples:
20
+ altergo Start a new session (same as: claude)
21
+ altergo --resume Open session picker
22
+ altergo --dangerously-skip-permissions
23
+ Pass any claude flag straight through
24
+
25
+ Navigation (session picker):
26
+ ↑/k Move up PgUp/PgDn Page scroll
27
+ ↓/j Move down g/G Jump to top/bottom
28
+ Enter Resume session q/Esc Quit
29
+ """
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ import curses
34
+ import os
35
+ import pwd
36
+ import re
37
+ import sys
38
+ import subprocess
39
+ import json
40
+ from datetime import datetime
41
+ from pathlib import Path
42
+
43
+ # --- Config ---
44
+
45
+ # Resolve the real home even if HOME is overridden (e.g., running as altergo)
46
+ _pw_home = Path(pwd.getpwuid(os.getuid()).pw_dir)
47
+ if not _pw_home.exists():
48
+ _pw_home = Path(os.environ["HOME"])
49
+
50
+ MAIN_HOME = _pw_home
51
+ ALT_HOME = MAIN_HOME / ".altergo"
52
+ MAIN_CLAUDE = MAIN_HOME / ".claude"
53
+ ALT_CLAUDE = ALT_HOME / ".claude"
54
+
55
+ # Directories to symlink (shared between main and alt)
56
+ SYMLINK_DIRS = [
57
+ "projects",
58
+ "tasks",
59
+ "session-env",
60
+ "file-history",
61
+ "shell-snapshots",
62
+ "agents",
63
+ "plans",
64
+ "cache",
65
+ ]
66
+
67
+ # Files to symlink
68
+ SYMLINK_FILES = [
69
+ "settings.json",
70
+ "CLAUDE.md",
71
+ "keybindings.json",
72
+ ]
73
+
74
+ # --- Setup / Teardown ---
75
+
76
+
77
+ def do_setup():
78
+ print("=== Altergo — Setup ===\n")
79
+
80
+ # 1. Create alt home
81
+ if not ALT_HOME.exists():
82
+ ALT_HOME.mkdir(parents=True)
83
+ print(f"Created alt home: {ALT_HOME}")
84
+ else:
85
+ print(f" Alt home exists: {ALT_HOME}")
86
+
87
+ # Ensure alt .claude dir exists
88
+ ALT_CLAUDE.mkdir(parents=True, exist_ok=True)
89
+
90
+ # 2. Symlink directories
91
+ for name in SYMLINK_DIRS:
92
+ src = MAIN_CLAUDE / name
93
+ dst = ALT_CLAUDE / name
94
+
95
+ if not src.exists():
96
+ print(f" Skip {name}/ (not found in main)")
97
+ continue
98
+
99
+ if dst.is_symlink():
100
+ target = dst.resolve()
101
+ if target == src.resolve():
102
+ print(f" {name}/ already symlinked")
103
+ else:
104
+ print(f" Warning: {name}/ symlinked to {target} (expected {src})")
105
+ continue
106
+
107
+ if dst.exists():
108
+ print(f" Warning: {name}/ exists as real dir — remove it first to symlink")
109
+ continue
110
+
111
+ dst.symlink_to(src)
112
+ print(f" Symlinked {name}/")
113
+
114
+ # 3. Symlink files
115
+ for name in SYMLINK_FILES:
116
+ src = MAIN_CLAUDE / name
117
+ dst = ALT_CLAUDE / name
118
+
119
+ if not src.exists():
120
+ continue
121
+
122
+ if dst.is_symlink():
123
+ print(f" {name} already symlinked")
124
+ continue
125
+
126
+ if dst.exists():
127
+ dst.unlink()
128
+
129
+ dst.symlink_to(src)
130
+ print(f" Symlinked {name}")
131
+
132
+ # 4. Check credentials
133
+ creds = ALT_CLAUDE / ".credentials.json"
134
+ print()
135
+ if creds.exists():
136
+ print(" Alt account credentials found")
137
+ else:
138
+ print(" No alt account credentials found.")
139
+ print(" Run 'altergo new' to authenticate with your alt account.\n")
140
+
141
+ print("\nSetup complete!\n")
142
+ print("Usage:")
143
+ print(" altergo new Start a new session with alt credentials")
144
+ print(" altergo Interactive session picker")
145
+ print(" altergo --resume <session-id> Resume directly")
146
+ print(" altergo --list List all sessions")
147
+
148
+
149
+ def do_teardown():
150
+ print("=== Altergo — Teardown ===\n")
151
+
152
+ for name in SYMLINK_DIRS:
153
+ dst = ALT_CLAUDE / name
154
+ if dst.is_symlink():
155
+ dst.unlink()
156
+ print(f" Removed symlink: {name}/")
157
+
158
+ for name in SYMLINK_FILES:
159
+ dst = ALT_CLAUDE / name
160
+ if dst.is_symlink():
161
+ dst.unlink()
162
+ print(f" Removed symlink: {name}")
163
+
164
+ print("\nTeardown complete. Alt home and credentials left intact.")
165
+
166
+
167
+ # --- Session Discovery ---
168
+
169
+
170
+ def get_sessions():
171
+ """Find all sessions across all projects, return sorted by modification time."""
172
+ sessions = []
173
+ projects_dir = MAIN_CLAUDE / "projects"
174
+
175
+ if not projects_dir.exists():
176
+ return sessions
177
+
178
+ for project_dir in projects_dir.iterdir():
179
+ if not project_dir.is_dir():
180
+ continue
181
+
182
+ project_name = project_dir.name
183
+
184
+ for f in project_dir.iterdir():
185
+ if f.suffix != ".jsonl" or f.parent.name == "subagents":
186
+ continue
187
+
188
+ session_id = f.stem
189
+ mod_time = f.stat().st_mtime
190
+ mod_dt = datetime.fromtimestamp(mod_time)
191
+ size_mb = f.stat().st_size / (1024 * 1024)
192
+
193
+ # Try to extract last user message as a preview
194
+ preview = get_session_preview(f)
195
+
196
+ sessions.append({
197
+ "id": session_id,
198
+ "project": project_name,
199
+ "modified": mod_dt,
200
+ "size_mb": size_mb,
201
+ "path": f,
202
+ "preview": preview,
203
+ })
204
+
205
+ sessions.sort(key=lambda s: s["modified"], reverse=True)
206
+ return sessions
207
+
208
+
209
+ def get_session_preview(jsonl_path):
210
+ """Read the last few user messages from a session file for preview."""
211
+ try:
212
+ last_msg = ""
213
+ with open(jsonl_path, "rb") as f:
214
+ # Read last 8KB to find recent messages
215
+ f.seek(0, 2)
216
+ size = f.tell()
217
+ f.seek(max(0, size - 8192))
218
+ tail = f.read().decode("utf-8", errors="replace")
219
+
220
+ for line in tail.strip().split("\n"):
221
+ try:
222
+ obj = json.loads(line)
223
+ if obj.get("type") == "human" and isinstance(obj.get("message"), dict):
224
+ content = obj["message"].get("content", "")
225
+ if isinstance(content, str) and content.strip():
226
+ last_msg = content.strip()
227
+ elif isinstance(content, list):
228
+ for block in content:
229
+ if isinstance(block, dict) and block.get("type") == "text":
230
+ last_msg = block["text"].strip()
231
+ except (json.JSONDecodeError, KeyError):
232
+ continue
233
+
234
+ return last_msg[:80] if last_msg else ""
235
+ except Exception:
236
+ return ""
237
+
238
+
239
+ def format_project_name(encoded):
240
+ """Convert encoded project path back to readable name."""
241
+ # -Users-netz-Documents-git-dispatch → dispatch
242
+ parts = encoded.strip("-").split("-")
243
+ # Return last meaningful part
244
+ return parts[-1] if parts else encoded
245
+
246
+
247
+ # --- Interactive Menu ---
248
+
249
+
250
+ def interactive_picker(sessions):
251
+ """Arrow-key driven session picker using curses."""
252
+ if not sessions:
253
+ print("No sessions found.")
254
+ sys.exit(1)
255
+
256
+ selected = curses.wrapper(_draw_picker, sessions)
257
+ return selected
258
+
259
+
260
+ def _draw_picker(stdscr, sessions):
261
+ curses.curs_set(0)
262
+ curses.use_default_colors()
263
+
264
+ # Init color pairs
265
+ curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) # selected
266
+ curses.init_pair(2, curses.COLOR_CYAN, -1) # header
267
+ curses.init_pair(3, curses.COLOR_YELLOW, -1) # project name
268
+ curses.init_pair(4, curses.COLOR_WHITE, -1) # session id
269
+ curses.init_pair(5, curses.COLOR_GREEN, -1) # time
270
+
271
+ current = 0
272
+ scroll_offset = 0
273
+
274
+ while True:
275
+ stdscr.clear()
276
+ max_y, max_x = stdscr.getmaxyx()
277
+
278
+ # Header
279
+ header = " Altergo — Pick a session (↑/↓ navigate, Enter select, q quit)"
280
+ stdscr.attron(curses.color_pair(2) | curses.A_BOLD)
281
+ stdscr.addnstr(0, 0, header.ljust(max_x), max_x - 1)
282
+ stdscr.attroff(curses.color_pair(2) | curses.A_BOLD)
283
+
284
+ # Column headers
285
+ col_header = f" {'Project':<20} {'Modified':<18} {'Size':>6} {'Last message'}"
286
+ stdscr.attron(curses.A_DIM)
287
+ stdscr.addnstr(2, 0, col_header[:max_x - 1], max_x - 1)
288
+ stdscr.attroff(curses.A_DIM)
289
+
290
+ # Visible area
291
+ visible_rows = max_y - 5 # header + col header + footer + padding
292
+ if visible_rows < 1:
293
+ visible_rows = 1
294
+
295
+ # Adjust scroll
296
+ if current < scroll_offset:
297
+ scroll_offset = current
298
+ elif current >= scroll_offset + visible_rows:
299
+ scroll_offset = current - visible_rows + 1
300
+
301
+ # Draw sessions
302
+ for i in range(visible_rows):
303
+ idx = scroll_offset + i
304
+ if idx >= len(sessions):
305
+ break
306
+
307
+ s = sessions[idx]
308
+ row = i + 3 # after header rows
309
+
310
+ project = format_project_name(s["project"])
311
+ modified = s["modified"].strftime("%Y-%m-%d %H:%M")
312
+ size = f"{s['size_mb']:.1f}MB"
313
+ preview = s["preview"]
314
+
315
+ # Truncate preview to fit
316
+ preview_width = max(0, max_x - 50)
317
+ if len(preview) > preview_width:
318
+ preview = preview[:preview_width - 1] + "…"
319
+
320
+ line = f" {project:<20} {modified:<18} {size:>6} {preview}"
321
+
322
+ if idx == current:
323
+ stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
324
+ stdscr.addnstr(row, 0, f"▸ {line[2:]}"[:max_x - 1].ljust(max_x - 1), max_x - 1)
325
+ stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
326
+ else:
327
+ stdscr.addnstr(row, 0, line[:max_x - 1], max_x - 1)
328
+
329
+ # Footer — show session ID of current selection
330
+ footer_row = max_y - 2
331
+ if current < len(sessions):
332
+ sid = sessions[current]["id"]
333
+ footer = f" Session: {sid}"
334
+ stdscr.attron(curses.A_DIM)
335
+ stdscr.addnstr(footer_row, 0, footer[:max_x - 1], max_x - 1)
336
+ stdscr.attroff(curses.A_DIM)
337
+
338
+ count_info = f" {current + 1}/{len(sessions)} sessions"
339
+ stdscr.attron(curses.A_DIM)
340
+ stdscr.addnstr(footer_row + 1, 0, count_info[:max_x - 1], max_x - 1)
341
+ stdscr.attroff(curses.A_DIM)
342
+
343
+ stdscr.refresh()
344
+
345
+ # Input
346
+ key = stdscr.getch()
347
+
348
+ if key == curses.KEY_UP or key == ord("k"):
349
+ current = max(0, current - 1)
350
+ elif key == curses.KEY_DOWN or key == ord("j"):
351
+ current = min(len(sessions) - 1, current + 1)
352
+ elif key == curses.KEY_PPAGE: # Page Up
353
+ current = max(0, current - visible_rows)
354
+ elif key == curses.KEY_NPAGE: # Page Down
355
+ current = min(len(sessions) - 1, current + visible_rows)
356
+ elif key == ord("g"): # Home
357
+ current = 0
358
+ elif key == ord("G"): # End
359
+ current = len(sessions) - 1
360
+ elif key in (curses.KEY_ENTER, 10, 13):
361
+ return sessions[current]
362
+ elif key in (ord("q"), 27): # q or Escape
363
+ return None
364
+
365
+
366
+ # --- Launch ---
367
+
368
+
369
+ def launch_claude(args=None):
370
+ """Launch claude with alt HOME, passing args through unchanged."""
371
+ env = os.environ.copy()
372
+ env["HOME"] = str(ALT_HOME)
373
+ cmd = ["claude"] + (args or [])
374
+ os.execvpe("claude", cmd, env)
375
+
376
+
377
+ # --- Main ---
378
+
379
+
380
+ def main():
381
+ args = sys.argv[1:]
382
+
383
+ # ── Altergo-owned commands (not passed to claude) ──────────────────────────
384
+
385
+ if args and args[0] in ("-h", "--help"):
386
+ print(__doc__)
387
+ sys.exit(0)
388
+
389
+ if args and args[0] == "--version":
390
+ print(f"altergo {__version__}")
391
+ sys.exit(0)
392
+
393
+ if args and args[0] == "--setup":
394
+ do_setup()
395
+ sys.exit(0)
396
+
397
+ if args and args[0] == "--teardown":
398
+ do_teardown()
399
+ sys.exit(0)
400
+
401
+ if args and args[0] == "--list":
402
+ sessions = get_sessions()
403
+ if not sessions:
404
+ print("No sessions found.")
405
+ sys.exit(0)
406
+ print(f"{'Project':<20} {'Modified':<18} {'Size':>6} Session ID")
407
+ print("-" * 80)
408
+ for s in sessions[:30]:
409
+ project = format_project_name(s["project"])
410
+ modified = s["modified"].strftime("%Y-%m-%d %H:%M")
411
+ size = f"{s['size_mb']:.1f}MB"
412
+ print(f"{project:<20} {modified:<18} {size:>6} {s['id']}")
413
+ sys.exit(0)
414
+
415
+ # --resume with no ID → open interactive picker
416
+ if args and args[0] == "--resume" and len(args) == 1:
417
+ sessions = get_sessions()
418
+ selected = interactive_picker(sessions)
419
+ if selected:
420
+ launch_claude(["--resume", selected["id"]])
421
+ else:
422
+ print("Cancelled.")
423
+ sys.exit(0)
424
+
425
+ # ── Everything else → pass straight through to claude with alt HOME ────────
426
+ # altergo → claude
427
+ # altergo --resume x → claude --resume x
428
+ # altergo --dangerously-skip-permissions → claude --dangerously-skip-permissions
429
+ launch_claude(args)
430
+
431
+
432
+ if __name__ == "__main__":
433
+ main()
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "altergo"
7
+ version = "0.1.0"
8
+ description = "Your other Claude — switch Claude Code identities without losing a thought"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "thepixelabs" }]
13
+ keywords = ["claude", "claude-code", "multi-account", "session-manager", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Environment :: Console :: Curses",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: MacOS",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Topic :: Utilities",
29
+ ]
30
+
31
+ [project.scripts]
32
+ altergo = "altergo:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://altergo.pixelabs.net"
36
+ Repository = "https://github.com/thepixelabs/altergo"
37
+ Issues = "https://github.com/thepixelabs/altergo/issues"
38
+
39
+ [tool.ruff]
40
+ target-version = "py39"
41
+ line-length = 120
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+