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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """bund — Telegram pings for coding agents. Approve from your phone."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -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