bund 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.
- bund-0.1.0/LICENSE +21 -0
- bund-0.1.0/PKG-INFO +183 -0
- bund-0.1.0/README.md +165 -0
- bund-0.1.0/pyproject.toml +30 -0
- bund-0.1.0/setup.cfg +4 -0
- bund-0.1.0/src/bund/__init__.py +3 -0
- bund-0.1.0/src/bund/__main__.py +3 -0
- bund-0.1.0/src/bund/ask.py +35 -0
- bund-0.1.0/src/bund/cli.py +118 -0
- bund-0.1.0/src/bund/config.py +51 -0
- bund-0.1.0/src/bund/hook.py +189 -0
- bund-0.1.0/src/bund/install.py +125 -0
- bund-0.1.0/src/bund/poller.py +171 -0
- bund-0.1.0/src/bund/rules.py +54 -0
- bund-0.1.0/src/bund/setup_chat.py +149 -0
- bund-0.1.0/src/bund/telegram.py +78 -0
- bund-0.1.0/src/bund/templates/plain.json +37 -0
- bund-0.1.0/src/bund/templates/sarcasm.json +50 -0
- bund-0.1.0/src/bund/templates.py +47 -0
- bund-0.1.0/src/bund.egg-info/PKG-INFO +183 -0
- bund-0.1.0/src/bund.egg-info/SOURCES.txt +25 -0
- bund-0.1.0/src/bund.egg-info/dependency_links.txt +1 -0
- bund-0.1.0/src/bund.egg-info/entry_points.txt +2 -0
- bund-0.1.0/src/bund.egg-info/top_level.txt +1 -0
- bund-0.1.0/tests/test_config_hook.py +103 -0
- bund-0.1.0/tests/test_rules.py +68 -0
- bund-0.1.0/tests/test_templates_install.py +125 -0
bund-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bund contributors
|
|
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.
|
bund-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bund
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Telegram pings for coding agents. Approve from your phone. Free. Zero dependencies.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: telegram,claude,coding-agent,hooks,notifications,approval
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
<h1 align="center">bund</h1>
|
|
20
|
+
|
|
21
|
+
<p align="center"><b>Your AI agent is waiting for you. Again.</b></p>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="https://api.memegen.link/images/fine/claude_has_been_waiting_for_2_hours/this_is_fine.png?width=450" width="450" alt="this is fine">
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
Telegram pings for coding agents.<br>
|
|
29
|
+
Approve from your phone. Free. Zero dependencies.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## The problem
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="https://api.memegen.link/images/db/you/agent_needing_approval/just_one_more_meeting.png?width=450" width="450" alt="distracted boyfriend">
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="https://api.memegen.link/images/gru/claude_asks_permission/bund_pings_your_telegram/you_tap_approve_from_anywhere/claude_keeps_working.png?width=450" width="450" alt="gru plan">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
## Setup (2 minutes)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install bund
|
|
50
|
+
|
|
51
|
+
bund set --token <botfather-token>
|
|
52
|
+
bund set --chat auto
|
|
53
|
+
bund install
|
|
54
|
+
bund test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`bund install` wires Claude Code's `PermissionRequest` and `Stop` hooks. On any
|
|
58
|
+
other agent or script, use `bund ask` (below) — no install needed.
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<img src="https://api.memegen.link/images/success/set_up_in_two_minutes/phone_buzzed_immediately.png?width=400" width="400" alt="success kid">
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
## What you get
|
|
65
|
+
|
|
66
|
+
| Button | Does |
|
|
67
|
+
|---|---|
|
|
68
|
+
| ✅ Approve | Lets it run, once |
|
|
69
|
+
| ❌ Deny | Nope |
|
|
70
|
+
| ♾ Approve all similar | Never asks again for that kind of command |
|
|
71
|
+
|
|
72
|
+
And a ping when the task is done:
|
|
73
|
+
|
|
74
|
+
<p align="center">
|
|
75
|
+
<img src="https://api.memegen.link/images/fry/not_sure_if_agent_finished/or_waiting_for_my_approval.png?width=450" width="450" alt="fry">
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
Now you know which one.
|
|
79
|
+
|
|
80
|
+
## Sample message
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
🙄 Your AI intern wants to run Bash. Please supervise.
|
|
84
|
+
|
|
85
|
+
📁 my-project
|
|
86
|
+
npm run build
|
|
87
|
+
|
|
88
|
+
[ ✅ Approve ] [ ❌ Deny ]
|
|
89
|
+
[ ♾ Approve all similar: Bash(npm run *) ]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The joke is on top. The real command is always shown in full below it.
|
|
93
|
+
|
|
94
|
+
## Works with any agent
|
|
95
|
+
|
|
96
|
+
`bund ask` is the universal protocol: any CLI, script, or agent asks a
|
|
97
|
+
question, your phone answers.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bund ask "deploy to prod?" && echo let's go
|
|
101
|
+
# stdout: allow | deny | timeout
|
|
102
|
+
# exit: 0 1 2
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import subprocess
|
|
107
|
+
r = subprocess.run(["bund", "ask", "run the migration?"], capture_output=True, text=True)
|
|
108
|
+
if r.stdout.strip() == "allow":
|
|
109
|
+
...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
No config files to wire. If bund isn't configured, `bund ask` fails fast with
|
|
113
|
+
exit code 2 instead of hanging your agent.
|
|
114
|
+
|
|
115
|
+
## Safety
|
|
116
|
+
|
|
117
|
+
<p align="center">
|
|
118
|
+
<img src="https://api.memegen.link/images/drake/approve_all_for_rm_rf/no_button_for_dangerous_commands.png?width=400" width="400" alt="drake">
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
- Only **your** Telegram chat can approve anything
|
|
122
|
+
- Every button works once (random request ids, no replaying old taps)
|
|
123
|
+
- Dangerous commands (`rm -rf`, `sudo`, force pushes, `curl | sh`, …) get a
|
|
124
|
+
serious warning and **no** "approve all" button
|
|
125
|
+
- No answer? The agent falls back to the normal terminal prompt
|
|
126
|
+
- bund can never block your work — any error means silent fallback
|
|
127
|
+
- Token and chat id live in `~/.config/bund/config.json` with `chmod 600`
|
|
128
|
+
|
|
129
|
+
## Dependencies
|
|
130
|
+
|
|
131
|
+
<p align="center">
|
|
132
|
+
<img src="https://api.memegen.link/images/buzz/zero_dependencies/zero_dependencies_everywhere.png?width=400" width="400" alt="buzz">
|
|
133
|
+
</p>
|
|
134
|
+
|
|
135
|
+
Python 3.9+. Standard library only. That's it.
|
|
136
|
+
|
|
137
|
+
## Cost
|
|
138
|
+
|
|
139
|
+
<p align="center">
|
|
140
|
+
<img src="https://api.memegen.link/images/oprah/you_get_a_free_bot/and_you_get_free_approvals.gif?width=400" width="400" alt="oprah">
|
|
141
|
+
</p>
|
|
142
|
+
|
|
143
|
+
## Tone
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
bund set --tone sarcasm # default
|
|
147
|
+
bund set --tone plain # for people who have no joy
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Add your own lines in `~/.config/bund/templates.json`.
|
|
151
|
+
|
|
152
|
+
## Also
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bund -m "build finished, come back"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Works with any script or agent.
|
|
159
|
+
|
|
160
|
+
<p align="center">
|
|
161
|
+
<img src="https://api.memegen.link/images/pigeon/claude/_/is_this_a_permission_prompt~q.png?width=400" width="400" alt="is this a pigeon">
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
**Commands**
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
bund set --token <token> # botfather token
|
|
170
|
+
bund set --chat auto|<id> # pick your chat
|
|
171
|
+
bund set --tone sarcasm|plain # message tone
|
|
172
|
+
bund install # add Claude Code hooks
|
|
173
|
+
bund uninstall # remove only bund's hooks
|
|
174
|
+
bund test # send a test message
|
|
175
|
+
bund ask "<question>" # ask anything (allow|deny|timeout)
|
|
176
|
+
bund -m "<text>" # send any message
|
|
177
|
+
bund hook # internal, called by hooks
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
<p align="center">
|
|
181
|
+
<sub>Memes by <a href="https://memegen.link">memegen.link</a>. If they don't load, that's on them, not us.</sub><br>
|
|
182
|
+
<sub>MIT License</sub>
|
|
183
|
+
</p>
|
bund-0.1.0/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<h1 align="center">bund</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><b>Your AI agent is waiting for you. Again.</b></p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://api.memegen.link/images/fine/claude_has_been_waiting_for_2_hours/this_is_fine.png?width=450" width="450" alt="this is fine">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
Telegram pings for coding agents.<br>
|
|
11
|
+
Approve from your phone. Free. Zero dependencies.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The problem
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="https://api.memegen.link/images/db/you/agent_needing_approval/just_one_more_meeting.png?width=450" width="450" alt="distracted boyfriend">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://api.memegen.link/images/gru/claude_asks_permission/bund_pings_your_telegram/you_tap_approve_from_anywhere/claude_keeps_working.png?width=450" width="450" alt="gru plan">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
## Setup (2 minutes)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install bund
|
|
32
|
+
|
|
33
|
+
bund set --token <botfather-token>
|
|
34
|
+
bund set --chat auto
|
|
35
|
+
bund install
|
|
36
|
+
bund test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`bund install` wires Claude Code's `PermissionRequest` and `Stop` hooks. On any
|
|
40
|
+
other agent or script, use `bund ask` (below) — no install needed.
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="https://api.memegen.link/images/success/set_up_in_two_minutes/phone_buzzed_immediately.png?width=400" width="400" alt="success kid">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
## What you get
|
|
47
|
+
|
|
48
|
+
| Button | Does |
|
|
49
|
+
|---|---|
|
|
50
|
+
| ✅ Approve | Lets it run, once |
|
|
51
|
+
| ❌ Deny | Nope |
|
|
52
|
+
| ♾ Approve all similar | Never asks again for that kind of command |
|
|
53
|
+
|
|
54
|
+
And a ping when the task is done:
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<img src="https://api.memegen.link/images/fry/not_sure_if_agent_finished/or_waiting_for_my_approval.png?width=450" width="450" alt="fry">
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
Now you know which one.
|
|
61
|
+
|
|
62
|
+
## Sample message
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
🙄 Your AI intern wants to run Bash. Please supervise.
|
|
66
|
+
|
|
67
|
+
📁 my-project
|
|
68
|
+
npm run build
|
|
69
|
+
|
|
70
|
+
[ ✅ Approve ] [ ❌ Deny ]
|
|
71
|
+
[ ♾ Approve all similar: Bash(npm run *) ]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The joke is on top. The real command is always shown in full below it.
|
|
75
|
+
|
|
76
|
+
## Works with any agent
|
|
77
|
+
|
|
78
|
+
`bund ask` is the universal protocol: any CLI, script, or agent asks a
|
|
79
|
+
question, your phone answers.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bund ask "deploy to prod?" && echo let's go
|
|
83
|
+
# stdout: allow | deny | timeout
|
|
84
|
+
# exit: 0 1 2
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import subprocess
|
|
89
|
+
r = subprocess.run(["bund", "ask", "run the migration?"], capture_output=True, text=True)
|
|
90
|
+
if r.stdout.strip() == "allow":
|
|
91
|
+
...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
No config files to wire. If bund isn't configured, `bund ask` fails fast with
|
|
95
|
+
exit code 2 instead of hanging your agent.
|
|
96
|
+
|
|
97
|
+
## Safety
|
|
98
|
+
|
|
99
|
+
<p align="center">
|
|
100
|
+
<img src="https://api.memegen.link/images/drake/approve_all_for_rm_rf/no_button_for_dangerous_commands.png?width=400" width="400" alt="drake">
|
|
101
|
+
</p>
|
|
102
|
+
|
|
103
|
+
- Only **your** Telegram chat can approve anything
|
|
104
|
+
- Every button works once (random request ids, no replaying old taps)
|
|
105
|
+
- Dangerous commands (`rm -rf`, `sudo`, force pushes, `curl | sh`, …) get a
|
|
106
|
+
serious warning and **no** "approve all" button
|
|
107
|
+
- No answer? The agent falls back to the normal terminal prompt
|
|
108
|
+
- bund can never block your work — any error means silent fallback
|
|
109
|
+
- Token and chat id live in `~/.config/bund/config.json` with `chmod 600`
|
|
110
|
+
|
|
111
|
+
## Dependencies
|
|
112
|
+
|
|
113
|
+
<p align="center">
|
|
114
|
+
<img src="https://api.memegen.link/images/buzz/zero_dependencies/zero_dependencies_everywhere.png?width=400" width="400" alt="buzz">
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
Python 3.9+. Standard library only. That's it.
|
|
118
|
+
|
|
119
|
+
## Cost
|
|
120
|
+
|
|
121
|
+
<p align="center">
|
|
122
|
+
<img src="https://api.memegen.link/images/oprah/you_get_a_free_bot/and_you_get_free_approvals.gif?width=400" width="400" alt="oprah">
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
## Tone
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
bund set --tone sarcasm # default
|
|
129
|
+
bund set --tone plain # for people who have no joy
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Add your own lines in `~/.config/bund/templates.json`.
|
|
133
|
+
|
|
134
|
+
## Also
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bund -m "build finished, come back"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Works with any script or agent.
|
|
141
|
+
|
|
142
|
+
<p align="center">
|
|
143
|
+
<img src="https://api.memegen.link/images/pigeon/claude/_/is_this_a_permission_prompt~q.png?width=400" width="400" alt="is this a pigeon">
|
|
144
|
+
</p>
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
**Commands**
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
bund set --token <token> # botfather token
|
|
152
|
+
bund set --chat auto|<id> # pick your chat
|
|
153
|
+
bund set --tone sarcasm|plain # message tone
|
|
154
|
+
bund install # add Claude Code hooks
|
|
155
|
+
bund uninstall # remove only bund's hooks
|
|
156
|
+
bund test # send a test message
|
|
157
|
+
bund ask "<question>" # ask anything (allow|deny|timeout)
|
|
158
|
+
bund -m "<text>" # send any message
|
|
159
|
+
bund hook # internal, called by hooks
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
<p align="center">
|
|
163
|
+
<sub>Memes by <a href="https://memegen.link">memegen.link</a>. If they don't load, that's on them, not us.</sub><br>
|
|
164
|
+
<sub>MIT License</sub>
|
|
165
|
+
</p>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bund"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Telegram pings for coding agents. Approve from your phone. Free. Zero dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["telegram", "claude", "coding-agent", "hooks", "notifications", "approval"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
bund = "bund.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
bund = ["templates/*.json"]
|
bund-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import secrets
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from . import config, hook, templates
|
|
6
|
+
from .telegram import TelegramError, send
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ask(question: str, timeout: int = None) -> int:
|
|
10
|
+
"""Universal protocol: prints allow|deny|timeout, exit 0/1/2."""
|
|
11
|
+
cfg = config.load()
|
|
12
|
+
if not config.ready(cfg):
|
|
13
|
+
print("bund is not configured. Run: bund set --token <token> && bund set --chat auto",
|
|
14
|
+
file=sys.stderr)
|
|
15
|
+
print("timeout", flush=True)
|
|
16
|
+
return 2
|
|
17
|
+
request_id = secrets.token_hex(6)
|
|
18
|
+
text = f"{templates.say('permission', {'tool': 'a question', 'command': question})}\n\n<pre>{html.escape(question)}</pre>"
|
|
19
|
+
rows = [[("✅ Approve", f"{request_id}:a"), ("❌ Deny", f"{request_id}:d")]]
|
|
20
|
+
try:
|
|
21
|
+
send(text, rows, cfg=cfg)
|
|
22
|
+
except TelegramError as e:
|
|
23
|
+
print(f"Telegram error: {e}", file=sys.stderr)
|
|
24
|
+
print("timeout", flush=True)
|
|
25
|
+
return 2
|
|
26
|
+
dec = hook.wait_for(request_id, cfg, timeout or config.timeout_sec(cfg))
|
|
27
|
+
action = (dec or {}).get("action")
|
|
28
|
+
if action in ("approve", "approve_all"):
|
|
29
|
+
print("allow", flush=True)
|
|
30
|
+
return 0
|
|
31
|
+
if action == "deny":
|
|
32
|
+
print("deny", flush=True)
|
|
33
|
+
return 1
|
|
34
|
+
print("timeout", flush=True)
|
|
35
|
+
return 2
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from . import __version__, config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(argv=None) -> None:
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
prog="bund",
|
|
10
|
+
description="Telegram pings for coding agents. Approve from your phone.",
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument("-m", "--message", metavar="TEXT", help="send a Telegram message")
|
|
13
|
+
parser.add_argument("--version", action="version", version=f"bund {__version__}")
|
|
14
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
15
|
+
|
|
16
|
+
p_set = sub.add_parser("set", help="configure token, chat, or tone")
|
|
17
|
+
group = p_set.add_mutually_exclusive_group(required=True)
|
|
18
|
+
group.add_argument("--token", metavar="BOT_TOKEN", help="Telegram bot token from @BotFather")
|
|
19
|
+
group.add_argument("--chat", metavar="ID|auto", help="chat id, or 'auto' to pick yourself")
|
|
20
|
+
group.add_argument("--tone", choices=["sarcasm", "plain"], help="message tone")
|
|
21
|
+
|
|
22
|
+
sub.add_parser("install", help="add bund hooks to ~/.claude/settings.json")
|
|
23
|
+
sub.add_parser("uninstall", help="remove only bund's hooks")
|
|
24
|
+
sub.add_parser("test", help="send a test message to your chat")
|
|
25
|
+
|
|
26
|
+
p_ask = sub.add_parser("ask", help="ask a question over Telegram; prints allow|deny|timeout")
|
|
27
|
+
p_ask.add_argument("question", nargs="*", help="the question to ask")
|
|
28
|
+
p_ask.add_argument("--timeout", type=int, default=None, help="seconds to wait (default 600)")
|
|
29
|
+
|
|
30
|
+
sub.add_parser("hook", help="internal: called by agent hooks, reads JSON from stdin")
|
|
31
|
+
|
|
32
|
+
args = parser.parse_args(argv)
|
|
33
|
+
|
|
34
|
+
if args.message:
|
|
35
|
+
sys.exit(_send_message(args.message))
|
|
36
|
+
|
|
37
|
+
if args.cmd == "set":
|
|
38
|
+
sys.exit(_cmd_set(args))
|
|
39
|
+
if args.cmd == "install":
|
|
40
|
+
from . import install as install_mod
|
|
41
|
+
sys.exit(install_mod.install())
|
|
42
|
+
if args.cmd == "uninstall":
|
|
43
|
+
from . import install as install_mod
|
|
44
|
+
sys.exit(install_mod.uninstall())
|
|
45
|
+
if args.cmd == "test":
|
|
46
|
+
sys.exit(_cmd_test())
|
|
47
|
+
if args.cmd == "ask":
|
|
48
|
+
from . import ask as ask_mod
|
|
49
|
+
question = " ".join(args.question).strip()
|
|
50
|
+
if not question and not sys.stdin.isatty():
|
|
51
|
+
question = sys.stdin.read().strip()
|
|
52
|
+
if not question:
|
|
53
|
+
print("Give a question: bund ask \"deploy to prod?\"", file=sys.stderr)
|
|
54
|
+
sys.exit(2)
|
|
55
|
+
sys.exit(ask_mod.ask(question, args.timeout))
|
|
56
|
+
if args.cmd == "hook":
|
|
57
|
+
from . import hook as hook_mod
|
|
58
|
+
hook_mod.main()
|
|
59
|
+
|
|
60
|
+
parser.print_help()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _cmd_set(args) -> int:
|
|
64
|
+
from . import setup_chat
|
|
65
|
+
|
|
66
|
+
cfg = config.load()
|
|
67
|
+
if args.token is not None:
|
|
68
|
+
return setup_chat.set_token(cfg, args.token)
|
|
69
|
+
if args.chat is not None:
|
|
70
|
+
if args.chat.strip().lower() == "auto":
|
|
71
|
+
return setup_chat.set_chat_auto(cfg)
|
|
72
|
+
return setup_chat.set_chat_id(cfg, args.chat)
|
|
73
|
+
if args.tone:
|
|
74
|
+
cfg["tone"] = args.tone
|
|
75
|
+
config.save(cfg)
|
|
76
|
+
from . import templates
|
|
77
|
+
print(f"Tone set to {args.tone}. Sample:")
|
|
78
|
+
print(" " + templates.say("permission", {"tool": "Bash", "project": "my-project"}))
|
|
79
|
+
return 0
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cmd_test() -> int:
|
|
84
|
+
from .telegram import TelegramError, send
|
|
85
|
+
|
|
86
|
+
cfg = config.load()
|
|
87
|
+
if not config.ready(cfg):
|
|
88
|
+
print("Not configured yet. Run: bund set --token <token> && bund set --chat auto")
|
|
89
|
+
return 1
|
|
90
|
+
try:
|
|
91
|
+
from . import templates
|
|
92
|
+
|
|
93
|
+
send(templates.say("test", {}), cfg=cfg)
|
|
94
|
+
except TelegramError as e:
|
|
95
|
+
print(f"Telegram error: {e}")
|
|
96
|
+
return 1
|
|
97
|
+
print(f"Test message sent to chat {cfg['chatId']}.")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _send_message(text: str) -> int:
|
|
102
|
+
from .telegram import TelegramError, send
|
|
103
|
+
|
|
104
|
+
cfg = config.load()
|
|
105
|
+
if not config.ready(cfg):
|
|
106
|
+
print("Not configured yet. Run: bund set --token <token> && bund set --chat auto")
|
|
107
|
+
return 1
|
|
108
|
+
try:
|
|
109
|
+
send(text, cfg=cfg)
|
|
110
|
+
except TelegramError as e:
|
|
111
|
+
print(f"Telegram error: {e}")
|
|
112
|
+
return 1
|
|
113
|
+
print("Sent.")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import stat
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
DEFAULT_TIMEOUT_SEC = 600
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def config_dir() -> Path:
|
|
10
|
+
base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(str(Path.home()), ".config")
|
|
11
|
+
return Path(base) / "bund"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def config_path() -> Path:
|
|
15
|
+
return config_dir() / "config.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load() -> dict:
|
|
19
|
+
try:
|
|
20
|
+
with open(config_path(), encoding="utf-8") as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
return data if isinstance(data, dict) else {}
|
|
23
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save(cfg: dict) -> dict:
|
|
28
|
+
d = config_dir()
|
|
29
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
p = config_path()
|
|
31
|
+
tmp = p.with_name("config.json.tmp")
|
|
32
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
33
|
+
json.dump(cfg, f, indent=2)
|
|
34
|
+
f.write("\n")
|
|
35
|
+
os.replace(tmp, p)
|
|
36
|
+
try:
|
|
37
|
+
os.chmod(p, stat.S_IRUSR | stat.S_IWUSR)
|
|
38
|
+
except OSError:
|
|
39
|
+
pass
|
|
40
|
+
return cfg
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ready(cfg: dict) -> bool:
|
|
44
|
+
return bool(cfg.get("token") and cfg.get("chatId"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def timeout_sec(cfg: dict) -> int:
|
|
48
|
+
try:
|
|
49
|
+
return int(cfg.get("timeoutSec") or DEFAULT_TIMEOUT_SEC)
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
return DEFAULT_TIMEOUT_SEC
|