nightmux 1.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.
nightmux-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohamed Raslan
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,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: nightmux
3
+ Version: 1.1.0
4
+ Summary: Run Claude Code, Codex or any terminal coding agent from Telegram — one forum topic per tmux session.
5
+ Author: Mohamed Raslan
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Mohamed Raslan
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/mmr710/nightmux
29
+ Project-URL: Changelog, https://github.com/mmr710/nightmux/blob/main/CHANGELOG.md
30
+ Project-URL: Issues, https://github.com/mmr710/nightmux/issues
31
+ Keywords: telegram,tmux,claude-code,codex,coding-agent,remote
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Topic :: Software Development :: Build Tools
38
+ Classifier: Topic :: Utilities
39
+ Requires-Python: >=3.8
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Dynamic: license-file
43
+
44
+ # nightmux
45
+
46
+ [![tests](https://github.com/mmr710/nightmux/actions/workflows/test.yml/badge.svg)](https://github.com/mmr710/nightmux/actions/workflows/test.yml)
47
+ [![Telegram](https://img.shields.io/badge/Telegram-Community-blue.svg?logo=telegram)](https://t.me/+SGmmExdMHTQ3OWVk)
48
+ [![PyPI](https://img.shields.io/pypi/v/nightmux.svg)](https://pypi.org/project/nightmux/)
49
+
50
+ ![nightmux — unified Telegram control for multi-agent AI workflows, quota monitoring, and automated recovery](docs/hero.jpg)
51
+
52
+ **Your night crew, on Telegram.**
53
+
54
+ ```
55
+ 02:14 ⏸ api hit the usage limit
56
+ 5-hour window spent — resumes 04:11, resuming itself with 'continue'
57
+ 04:11 ▶️ api resumed · sending queued prompt
58
+ 04:11 ⚙️ api
59
+ ```
60
+
61
+ A usage limit at 2am used to end the night. The turn dies mid-refactor, the
62
+ prompt that started it is already spent, and the session sits there until
63
+ someone awake types `continue`. nightmux reads the reset time, holds everything
64
+ you send, and puts the work back the moment the window reopens — including the
65
+ turn the limit cut off. You read the result at breakfast.
66
+
67
+ That is the part nobody else is doing. The rest is what makes it usable:
68
+
69
+ Run **Claude Code from your phone** — or Codex, Gemini, aider, anything with a
70
+ prompt. One Telegram forum topic per project, one tmux session behind it. Text
71
+ you send is typed into that session's prompt; what the session says comes back
72
+ to the topic. Approvals arrive as tap buttons.
73
+
74
+ No container, no DNS, no certificates, no ports open, no relay service. It
75
+ attaches to tmux sessions you already have, on the machine you already use.
76
+ Python stdlib only — one file, ~3,400 lines you can read in an afternoon.
77
+
78
+ <!-- TODO: 30s screen recording — the ⏸ / ▶️ pair above, on a real phone, then a
79
+ permission prompt answered from the buttons. -->
80
+
81
+ ```
82
+ Telegram group (Topics on) your machine
83
+ ┌───────────────────────┐ ┌──────────────────────────┐
84
+ │ #api ────────────┼──────────┼─► tmux: api → claude │
85
+ │ #frontend ────────────┼──────────┼─► tmux: web → claude │
86
+ │ #scratch ────────────┼──────────┼─► tmux: scratch→ claude │
87
+ └───────────────────────┘ └──────────────────────────┘
88
+ ▲ │
89
+ └──── output, approvals, usage ──────┘
90
+ ```
91
+
92
+ ## Why this one
93
+
94
+ There are plenty of ways to reach a coding agent from a phone. Most are one of
95
+ two shapes: a bot that drives the agent through its SDK and keeps the
96
+ conversation in its own database, or a mobile app that talks to a relay service
97
+ you don't run. Both work. Neither leaves you with a terminal session.
98
+
99
+ nightmux is the third shape — it drives the session you would have started
100
+ yourself:
101
+
102
+ **It works the hours you don't.** A status-line sidecar gives nightmux the real
103
+ context percentage and the real 5-hour / 7-day limit windows, so it can act on
104
+ them instead of discovering them:
105
+
106
+ - a turn the limit cut off **resumes itself** when the window reopens
107
+ (`"auto_continue": false` to wait for a human instead)
108
+ - a prompt sent during a lockout is **held**, not lost — replayed when the window
109
+ resets, surviving daemon restarts and reboots
110
+ - a prompt refused before it ever got a turn goes back on the queue whole
111
+ - `!at 03:00 <prompt>` and `!every 4h <prompt>` start work while you are asleep —
112
+ and they queue rather than type, so they wait behind a lockout too
113
+ - `/compact` automatically at a context threshold you set (`!autocompact 70`)
114
+ - warnings at 80% and 90% of a window, before the wall rather than at it
115
+ - `!ctx` shows what is actually filling the window; `!cost` weighs a session or
116
+ every project by token type
117
+
118
+ Long-running agent sessions cost money and stall in ways chat never does. That is
119
+ the part nobody else is watching.
120
+
121
+ **It attaches to sessions instead of owning them.** nightmux types into tmux. The
122
+ session is still yours — SSH in, attach, type directly, and the bot keeps working
123
+ mid-conversation. Nothing is wrapped, proxied, or re-hosted, so there is no state
124
+ to get out of sync and nothing to lose when the daemon restarts.
125
+
126
+ **It is not tied to one agent.** `!new` starts your default; `!codex`, `!aider`,
127
+ `!gemini` or anything you add to `agents` in the config starts that instead, and
128
+ `!resume` remembers which agent a topic belongs to. The hooks and the usage
129
+ numbers are Claude Code specific — every other agent degrades to reading the
130
+ terminal, which is how nightmux worked before the hooks existed.
131
+
132
+ **Not for you if** you want a polished app instead of a chat window, you're on
133
+ Windows or macOS (the service install is systemd, though everything else is
134
+ portable — [#2](https://github.com/mmr710/nightmux/issues/2)), or you want your
135
+ teammates in the same group: the allowlist is a list of people trusted with a
136
+ shell on your machine, which is not a thing to hand out. One person, their own
137
+ box, their own agents.
138
+
139
+ ## Install
140
+
141
+ Install from PyPI (recommended):
142
+ ```bash
143
+ pipx install nightmux
144
+ nightmux --setup
145
+ ```
146
+
147
+ Or install the latest development version directly from GitHub:
148
+ ```bash
149
+ pipx install git+https://github.com/mmr710/nightmux
150
+ nightmux --setup
151
+ ```
152
+
153
+ or clone it, which is the version to pick if you want the source where you can
154
+ read and edit it — there are only four files and no dependencies:
155
+
156
+ ```bash
157
+ git clone https://github.com/mmr710/nightmux ~/nightmux
158
+ python3 ~/nightmux/nightmux.py --setup
159
+ ```
160
+
161
+ Setup walks the whole thing: BotFather token, finding your group, writing the
162
+ allowlist, wiring the Claude Code hooks, installing the systemd user service. It
163
+ is idempotent — re-run it after an upgrade.
164
+
165
+ You will be asked to create a Telegram group with **Topics** turned on and add
166
+ the bot as an **admin**. Admin is not optional: without it the bot only receives
167
+ messages addressed to it, so most of what you type never arrives.
168
+
169
+ Then, in a new topic:
170
+
171
+ ```
172
+ !new api ~/code/api # start a session and bind this topic to it
173
+ ```
174
+
175
+ and type. `!help` lists the rest.
176
+
177
+ ## What it feels like
178
+
179
+ ```
180
+ you fix the failing auth test
181
+ bot ⚙️ api · Opus 5 · 34% ctx
182
+ bot 🔧 Bash pytest tests/test_auth.py -x
183
+ bot 🔧 Read src/auth.py
184
+ bot 🟠 needs input api
185
+ Bash(git commit -m "fix token expiry check")
186
+ [ 1. Yes ] [ 2. Yes, don't ask again ] [ 3. No ]
187
+ you (taps 1)
188
+ bot ✅ api
189
+ Token expiry used `<` instead of `<=`, so a token expiring exactly on the
190
+ boundary was rejected. Fixed and committed; the test passes.
191
+ ```
192
+
193
+ Approvals arrive the moment Claude Code asks, via its `Notification` hook —
194
+ before the terminal has finished redrawing.
195
+
196
+ ## Commands
197
+
198
+ Everything works as `!cmd`, and the common ones are registered as `/cmd` so
199
+ Telegram autocompletes them. Anything that is not a nightmux command — including
200
+ Claude's own `/compact`, `/clear`, `/model` — is typed into the session.
201
+
202
+ | | |
203
+ |---|---|
204
+ | `!new <name> [dir] [flags]` | start a session with the default agent, bind this topic to it |
205
+ | `!codex` / `!aider` / `!gemini` / `!agy` … | same, with that agent |
206
+ | `!resume [agent]` | relaunch this topic's directory, resuming the last conversation |
207
+ | `!bind <session>` / `!unbind` / `!kill` | attach, detach, stop (kill asks first) |
208
+ | `!sessions` / `!status` | tmux sessions; every topic and its state |
209
+ | `!pane [lines]` / `!ctl` | dump the terminal; button panel |
210
+ | `!git` / `!diff` / `!get <path>` | repo state and file upload from the session's cwd |
211
+ | `!ctx` / `!cost [days]` / `!usage` | context breakdown, token spend, limit windows |
212
+ | `!autocompact <pct\|off>` | auto-`/compact` at a context threshold |
213
+ | `!idlectx <pct\|off>` | flag parked sessions still holding a big context |
214
+ | `!queue [clear\|now]` | prompts held for a rate-limit reset |
215
+ | `!at 03:00 <prompt>`, `!at +90m …` | run a prompt later |
216
+ | `!every 4h <prompt>`, `!sched [clear]` | run it on a repeat, or list what is set |
217
+ | `!grep <text> [days]` | search every transcript on the machine |
218
+ | `!verbose` / `!raw <text>` / `!keys <keys>` | tool detail, type past a menu, raw tmux keys |
219
+ | `!1`..`!9` `!y` `!n` `!esc` `!int` `!enter` `!tab` | menu picks and keys |
220
+
221
+ A pick is checked, not assumed: `!1` sends the digit, looks at the pane, and adds
222
+ Enter only if the same question is still there — dialogs disagree about whether a
223
+ digit confirms or only moves the highlight. `!y`/`!n` answer a numbered menu with
224
+ the digit of its Yes/No option, because the letter does nothing to a list.
225
+ | `!version` | build, python, and which hooks are wired |
226
+ | `!tz <zone>` / `!reload` / `!log` / `!help` | timezone, re-read config, journal, this list |
227
+
228
+ Send a photo or file and it is saved, with the path typed into the session.
229
+
230
+ ## How it works
231
+
232
+ Four files, no framework:
233
+
234
+ | | |
235
+ |---|---|
236
+ | `nightmux.py` | the daemon: long-polls Telegram, watches tmux, everything above |
237
+ | `nightmux_state.py` | status-line sidecar — parks context %, limit windows and the transcript path where the daemon can read them |
238
+ | `nightmux_stop.py` | `Stop` hook — pushes the final answer as exact text, not scraped pixels |
239
+ | `nightmux_notify.py` | `Notification` hook — pushes permission prompts the instant they appear |
240
+
241
+ The daemon reads the session's JSONL transcript when the sidecar is installed,
242
+ which is why output arrives as clean text with a real tool trace. Without it,
243
+ nightmux falls back to scraping `tmux capture-pane` — everything still works, just
244
+ noisier and without the usage numbers.
245
+
246
+ One watcher thread polls every bound session; each topic gets its own worker
247
+ thread, so a slow command in one topic never blocks another. The polling offset
248
+ is only persisted past updates that have actually finished, so a crash replays
249
+ work rather than dropping it.
250
+
251
+ [ARCHITECTURE.md](ARCHITECTURE.md) has the rest: threads, what survives a
252
+ restart, how output is chosen, and the decisions that were rejected.
253
+
254
+ Run the tests: `python3 nightmux.py --selfcheck` (and the same flag on the three
255
+ hook scripts). No framework, no fixtures — asserts that fail loudly.
256
+
257
+ `python3 tests/test_panes.py` runs the pane corpus: captured terminal screens and
258
+ the state nightmux must read from each. Adding an agent whose TUI it misreads is
259
+ one file — drop the pane in `tests/panes/` as `<what>.<busy|idle|waiting>.txt` and
260
+ the classifier is held to it from then on.
261
+
262
+ ## Config
263
+
264
+ `~/.nightmux.json`, mode `0600`, written by setup:
265
+
266
+ ```json
267
+ {
268
+ "token": "<from @BotFather>",
269
+ "chat_id": -1001234567890,
270
+ "allow_users": [123456789],
271
+ "topics": {"12": "api"},
272
+ "agent": "claude",
273
+ "agents": {"opencode": ["opencode", "--continue"]},
274
+ "autostart": {"api": "~/code/api"},
275
+ "projects_root": "~/code",
276
+ "tz_offset": "Africa/Cairo",
277
+ "autocompact": 70,
278
+ "auto_continue": "continue",
279
+ "modes": {"115": "readonly"},
280
+ "poll": 2
281
+ }
282
+ ```
283
+
284
+ `agent` is what `!new` starts. `agents` adds or overrides entries in the
285
+ built-in table as `[command, resume-flags]` — those flags are the part most
286
+ likely to drift as these CLIs change, so they are config, not code.
287
+ `autostart` recreates sessions after a reboot. `projects_root` makes a new topic
288
+ named after a directory start that project on its first message. `!reload` picks
289
+ up hand edits without a restart.
290
+
291
+ ## Requirements
292
+
293
+ Python 3.8+ (CI runs 3.8 through 3.13), tmux, a terminal coding agent, and Linux
294
+ with systemd (the service is optional — `python3 nightmux.py` in a terminal works
295
+ fine). Claude Code gets the hooks and the usage numbers; everything else runs on
296
+ the terminal scrape.
297
+
298
+ ## Security
299
+
300
+ **The bot token is a shell on your machine, and `allow_users` is the only thing
301
+ between a stranger and your sessions.** Read [SECURITY.md](SECURITY.md) before
302
+ you add a second person or a second machine. It is short.
303
+
304
+ ## License
305
+
306
+ MIT. Changes are in [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,263 @@
1
+ # nightmux
2
+
3
+ [![tests](https://github.com/mmr710/nightmux/actions/workflows/test.yml/badge.svg)](https://github.com/mmr710/nightmux/actions/workflows/test.yml)
4
+ [![Telegram](https://img.shields.io/badge/Telegram-Community-blue.svg?logo=telegram)](https://t.me/+SGmmExdMHTQ3OWVk)
5
+ [![PyPI](https://img.shields.io/pypi/v/nightmux.svg)](https://pypi.org/project/nightmux/)
6
+
7
+ ![nightmux — unified Telegram control for multi-agent AI workflows, quota monitoring, and automated recovery](docs/hero.jpg)
8
+
9
+ **Your night crew, on Telegram.**
10
+
11
+ ```
12
+ 02:14 ⏸ api hit the usage limit
13
+ 5-hour window spent — resumes 04:11, resuming itself with 'continue'
14
+ 04:11 ▶️ api resumed · sending queued prompt
15
+ 04:11 ⚙️ api
16
+ ```
17
+
18
+ A usage limit at 2am used to end the night. The turn dies mid-refactor, the
19
+ prompt that started it is already spent, and the session sits there until
20
+ someone awake types `continue`. nightmux reads the reset time, holds everything
21
+ you send, and puts the work back the moment the window reopens — including the
22
+ turn the limit cut off. You read the result at breakfast.
23
+
24
+ That is the part nobody else is doing. The rest is what makes it usable:
25
+
26
+ Run **Claude Code from your phone** — or Codex, Gemini, aider, anything with a
27
+ prompt. One Telegram forum topic per project, one tmux session behind it. Text
28
+ you send is typed into that session's prompt; what the session says comes back
29
+ to the topic. Approvals arrive as tap buttons.
30
+
31
+ No container, no DNS, no certificates, no ports open, no relay service. It
32
+ attaches to tmux sessions you already have, on the machine you already use.
33
+ Python stdlib only — one file, ~3,400 lines you can read in an afternoon.
34
+
35
+ <!-- TODO: 30s screen recording — the ⏸ / ▶️ pair above, on a real phone, then a
36
+ permission prompt answered from the buttons. -->
37
+
38
+ ```
39
+ Telegram group (Topics on) your machine
40
+ ┌───────────────────────┐ ┌──────────────────────────┐
41
+ │ #api ────────────┼──────────┼─► tmux: api → claude │
42
+ │ #frontend ────────────┼──────────┼─► tmux: web → claude │
43
+ │ #scratch ────────────┼──────────┼─► tmux: scratch→ claude │
44
+ └───────────────────────┘ └──────────────────────────┘
45
+ ▲ │
46
+ └──── output, approvals, usage ──────┘
47
+ ```
48
+
49
+ ## Why this one
50
+
51
+ There are plenty of ways to reach a coding agent from a phone. Most are one of
52
+ two shapes: a bot that drives the agent through its SDK and keeps the
53
+ conversation in its own database, or a mobile app that talks to a relay service
54
+ you don't run. Both work. Neither leaves you with a terminal session.
55
+
56
+ nightmux is the third shape — it drives the session you would have started
57
+ yourself:
58
+
59
+ **It works the hours you don't.** A status-line sidecar gives nightmux the real
60
+ context percentage and the real 5-hour / 7-day limit windows, so it can act on
61
+ them instead of discovering them:
62
+
63
+ - a turn the limit cut off **resumes itself** when the window reopens
64
+ (`"auto_continue": false` to wait for a human instead)
65
+ - a prompt sent during a lockout is **held**, not lost — replayed when the window
66
+ resets, surviving daemon restarts and reboots
67
+ - a prompt refused before it ever got a turn goes back on the queue whole
68
+ - `!at 03:00 <prompt>` and `!every 4h <prompt>` start work while you are asleep —
69
+ and they queue rather than type, so they wait behind a lockout too
70
+ - `/compact` automatically at a context threshold you set (`!autocompact 70`)
71
+ - warnings at 80% and 90% of a window, before the wall rather than at it
72
+ - `!ctx` shows what is actually filling the window; `!cost` weighs a session or
73
+ every project by token type
74
+
75
+ Long-running agent sessions cost money and stall in ways chat never does. That is
76
+ the part nobody else is watching.
77
+
78
+ **It attaches to sessions instead of owning them.** nightmux types into tmux. The
79
+ session is still yours — SSH in, attach, type directly, and the bot keeps working
80
+ mid-conversation. Nothing is wrapped, proxied, or re-hosted, so there is no state
81
+ to get out of sync and nothing to lose when the daemon restarts.
82
+
83
+ **It is not tied to one agent.** `!new` starts your default; `!codex`, `!aider`,
84
+ `!gemini` or anything you add to `agents` in the config starts that instead, and
85
+ `!resume` remembers which agent a topic belongs to. The hooks and the usage
86
+ numbers are Claude Code specific — every other agent degrades to reading the
87
+ terminal, which is how nightmux worked before the hooks existed.
88
+
89
+ **Not for you if** you want a polished app instead of a chat window, you're on
90
+ Windows or macOS (the service install is systemd, though everything else is
91
+ portable — [#2](https://github.com/mmr710/nightmux/issues/2)), or you want your
92
+ teammates in the same group: the allowlist is a list of people trusted with a
93
+ shell on your machine, which is not a thing to hand out. One person, their own
94
+ box, their own agents.
95
+
96
+ ## Install
97
+
98
+ Install from PyPI (recommended):
99
+ ```bash
100
+ pipx install nightmux
101
+ nightmux --setup
102
+ ```
103
+
104
+ Or install the latest development version directly from GitHub:
105
+ ```bash
106
+ pipx install git+https://github.com/mmr710/nightmux
107
+ nightmux --setup
108
+ ```
109
+
110
+ or clone it, which is the version to pick if you want the source where you can
111
+ read and edit it — there are only four files and no dependencies:
112
+
113
+ ```bash
114
+ git clone https://github.com/mmr710/nightmux ~/nightmux
115
+ python3 ~/nightmux/nightmux.py --setup
116
+ ```
117
+
118
+ Setup walks the whole thing: BotFather token, finding your group, writing the
119
+ allowlist, wiring the Claude Code hooks, installing the systemd user service. It
120
+ is idempotent — re-run it after an upgrade.
121
+
122
+ You will be asked to create a Telegram group with **Topics** turned on and add
123
+ the bot as an **admin**. Admin is not optional: without it the bot only receives
124
+ messages addressed to it, so most of what you type never arrives.
125
+
126
+ Then, in a new topic:
127
+
128
+ ```
129
+ !new api ~/code/api # start a session and bind this topic to it
130
+ ```
131
+
132
+ and type. `!help` lists the rest.
133
+
134
+ ## What it feels like
135
+
136
+ ```
137
+ you fix the failing auth test
138
+ bot ⚙️ api · Opus 5 · 34% ctx
139
+ bot 🔧 Bash pytest tests/test_auth.py -x
140
+ bot 🔧 Read src/auth.py
141
+ bot 🟠 needs input api
142
+ Bash(git commit -m "fix token expiry check")
143
+ [ 1. Yes ] [ 2. Yes, don't ask again ] [ 3. No ]
144
+ you (taps 1)
145
+ bot ✅ api
146
+ Token expiry used `<` instead of `<=`, so a token expiring exactly on the
147
+ boundary was rejected. Fixed and committed; the test passes.
148
+ ```
149
+
150
+ Approvals arrive the moment Claude Code asks, via its `Notification` hook —
151
+ before the terminal has finished redrawing.
152
+
153
+ ## Commands
154
+
155
+ Everything works as `!cmd`, and the common ones are registered as `/cmd` so
156
+ Telegram autocompletes them. Anything that is not a nightmux command — including
157
+ Claude's own `/compact`, `/clear`, `/model` — is typed into the session.
158
+
159
+ | | |
160
+ |---|---|
161
+ | `!new <name> [dir] [flags]` | start a session with the default agent, bind this topic to it |
162
+ | `!codex` / `!aider` / `!gemini` / `!agy` … | same, with that agent |
163
+ | `!resume [agent]` | relaunch this topic's directory, resuming the last conversation |
164
+ | `!bind <session>` / `!unbind` / `!kill` | attach, detach, stop (kill asks first) |
165
+ | `!sessions` / `!status` | tmux sessions; every topic and its state |
166
+ | `!pane [lines]` / `!ctl` | dump the terminal; button panel |
167
+ | `!git` / `!diff` / `!get <path>` | repo state and file upload from the session's cwd |
168
+ | `!ctx` / `!cost [days]` / `!usage` | context breakdown, token spend, limit windows |
169
+ | `!autocompact <pct\|off>` | auto-`/compact` at a context threshold |
170
+ | `!idlectx <pct\|off>` | flag parked sessions still holding a big context |
171
+ | `!queue [clear\|now]` | prompts held for a rate-limit reset |
172
+ | `!at 03:00 <prompt>`, `!at +90m …` | run a prompt later |
173
+ | `!every 4h <prompt>`, `!sched [clear]` | run it on a repeat, or list what is set |
174
+ | `!grep <text> [days]` | search every transcript on the machine |
175
+ | `!verbose` / `!raw <text>` / `!keys <keys>` | tool detail, type past a menu, raw tmux keys |
176
+ | `!1`..`!9` `!y` `!n` `!esc` `!int` `!enter` `!tab` | menu picks and keys |
177
+
178
+ A pick is checked, not assumed: `!1` sends the digit, looks at the pane, and adds
179
+ Enter only if the same question is still there — dialogs disagree about whether a
180
+ digit confirms or only moves the highlight. `!y`/`!n` answer a numbered menu with
181
+ the digit of its Yes/No option, because the letter does nothing to a list.
182
+ | `!version` | build, python, and which hooks are wired |
183
+ | `!tz <zone>` / `!reload` / `!log` / `!help` | timezone, re-read config, journal, this list |
184
+
185
+ Send a photo or file and it is saved, with the path typed into the session.
186
+
187
+ ## How it works
188
+
189
+ Four files, no framework:
190
+
191
+ | | |
192
+ |---|---|
193
+ | `nightmux.py` | the daemon: long-polls Telegram, watches tmux, everything above |
194
+ | `nightmux_state.py` | status-line sidecar — parks context %, limit windows and the transcript path where the daemon can read them |
195
+ | `nightmux_stop.py` | `Stop` hook — pushes the final answer as exact text, not scraped pixels |
196
+ | `nightmux_notify.py` | `Notification` hook — pushes permission prompts the instant they appear |
197
+
198
+ The daemon reads the session's JSONL transcript when the sidecar is installed,
199
+ which is why output arrives as clean text with a real tool trace. Without it,
200
+ nightmux falls back to scraping `tmux capture-pane` — everything still works, just
201
+ noisier and without the usage numbers.
202
+
203
+ One watcher thread polls every bound session; each topic gets its own worker
204
+ thread, so a slow command in one topic never blocks another. The polling offset
205
+ is only persisted past updates that have actually finished, so a crash replays
206
+ work rather than dropping it.
207
+
208
+ [ARCHITECTURE.md](ARCHITECTURE.md) has the rest: threads, what survives a
209
+ restart, how output is chosen, and the decisions that were rejected.
210
+
211
+ Run the tests: `python3 nightmux.py --selfcheck` (and the same flag on the three
212
+ hook scripts). No framework, no fixtures — asserts that fail loudly.
213
+
214
+ `python3 tests/test_panes.py` runs the pane corpus: captured terminal screens and
215
+ the state nightmux must read from each. Adding an agent whose TUI it misreads is
216
+ one file — drop the pane in `tests/panes/` as `<what>.<busy|idle|waiting>.txt` and
217
+ the classifier is held to it from then on.
218
+
219
+ ## Config
220
+
221
+ `~/.nightmux.json`, mode `0600`, written by setup:
222
+
223
+ ```json
224
+ {
225
+ "token": "<from @BotFather>",
226
+ "chat_id": -1001234567890,
227
+ "allow_users": [123456789],
228
+ "topics": {"12": "api"},
229
+ "agent": "claude",
230
+ "agents": {"opencode": ["opencode", "--continue"]},
231
+ "autostart": {"api": "~/code/api"},
232
+ "projects_root": "~/code",
233
+ "tz_offset": "Africa/Cairo",
234
+ "autocompact": 70,
235
+ "auto_continue": "continue",
236
+ "modes": {"115": "readonly"},
237
+ "poll": 2
238
+ }
239
+ ```
240
+
241
+ `agent` is what `!new` starts. `agents` adds or overrides entries in the
242
+ built-in table as `[command, resume-flags]` — those flags are the part most
243
+ likely to drift as these CLIs change, so they are config, not code.
244
+ `autostart` recreates sessions after a reboot. `projects_root` makes a new topic
245
+ named after a directory start that project on its first message. `!reload` picks
246
+ up hand edits without a restart.
247
+
248
+ ## Requirements
249
+
250
+ Python 3.8+ (CI runs 3.8 through 3.13), tmux, a terminal coding agent, and Linux
251
+ with systemd (the service is optional — `python3 nightmux.py` in a terminal works
252
+ fine). Claude Code gets the hooks and the usage numbers; everything else runs on
253
+ the terminal scrape.
254
+
255
+ ## Security
256
+
257
+ **The bot token is a shell on your machine, and `allow_users` is the only thing
258
+ between a stranger and your sessions.** Read [SECURITY.md](SECURITY.md) before
259
+ you add a second person or a second machine. It is short.
260
+
261
+ ## License
262
+
263
+ MIT. Changes are in [CHANGELOG.md](CHANGELOG.md).