aitextaroo 0.2.0__tar.gz → 0.2.2__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.
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/PKG-INFO +19 -11
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/README.md +18 -10
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/__init__.py +1 -1
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/agent_cli.py +48 -13
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/client.py +35 -16
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo.egg-info/PKG-INFO +19 -11
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/pyproject.toml +1 -1
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_agent_cli.py +32 -2
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_client_replies.py +26 -1
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/LICENSE +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/agents.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/bridge.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/cli.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/commands.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/conversation.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/credentials.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/formatting.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/models.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo/setup.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo.egg-info/SOURCES.txt +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo.egg-info/dependency_links.txt +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo.egg-info/entry_points.txt +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo.egg-info/requires.txt +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/aitextaroo.egg-info/top_level.txt +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/setup.cfg +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_agents.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_bridge.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_client.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_commands.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_conversation.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_formatting.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_models.py +0 -0
- {aitextaroo-0.2.0 → aitextaroo-0.2.2}/tests/test_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aitextaroo
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Client library for AI Text-a-roo — give any AI agent a phone number
|
|
5
5
|
Author-email: AI Text-a-roo <hello@aitextaroo.com>
|
|
6
6
|
License: MIT
|
|
@@ -74,13 +74,16 @@ aitextaroo send "Hello from my agent!"
|
|
|
74
74
|
|
|
75
75
|
`--save` stores the API key in `~/.aitextaroo/credentials.json` with owner-only permissions.
|
|
76
76
|
`aitextaroo` and `aitextaroo-bridge` find it there, or in `AITEXTAROO_API_KEY`, or in `--api-key`.
|
|
77
|
+
Verifying a phone that already has an account makes a new key: apps connected before are
|
|
78
|
+
signed out and webhook forwarding is turned off.
|
|
77
79
|
|
|
78
80
|
## The `aitextaroo` command
|
|
79
81
|
|
|
80
82
|
Made for agents with a shell. Output is JSON; errors go to stderr with exit code 1.
|
|
81
83
|
`aitextaroo verify-pin` exits 2 when the PIN is wrong, so `aitextaroo verify-pin <<< 4821 && …`
|
|
82
84
|
only continues on a match. `verify-pin` and `set-pin` read the PIN from stdin when it isn't
|
|
83
|
-
an argument, which keeps it out of shell
|
|
85
|
+
an argument. At a terminal they ask for it without echoing, which keeps it out of your shell
|
|
86
|
+
history (a command with the PIN written in it, like the `<<<` form, can still be saved there).
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
89
|
aitextaroo send "Build finished: 318 tests passing."
|
|
@@ -88,11 +91,13 @@ aitextaroo ask "Deploy to production? Reply YES or NO" --timeout 120 # text, t
|
|
|
88
91
|
aitextaroo wait --timeout 60 # wait for the next text
|
|
89
92
|
aitextaroo recent # texts from the last 5 minutes
|
|
90
93
|
aitextaroo relay --to +12125550100 --name Mom "Happy birthday!" # Pro: text a contact
|
|
91
|
-
aitextaroo verify-pin
|
|
92
|
-
aitextaroo set-pin
|
|
94
|
+
aitextaroo verify-pin # check a PIN (hidden prompt)
|
|
95
|
+
aitextaroo set-pin # set the user's PIN (4-8 digits)
|
|
96
|
+
aitextaroo set-pin --current # change it: current PIN, then the new one
|
|
97
|
+
aitextaroo remove-pin # needs the current PIN
|
|
93
98
|
aitextaroo account
|
|
94
99
|
aitextaroo upgrade # Stripe link for Pro
|
|
95
|
-
aitextaroo webhook set https://example.com/hook --auth-header "Bearer <token>"
|
|
100
|
+
aitextaroo webhook set https://example.com/hook --auth-header "Bearer <token>" # add --pin if a PIN is set
|
|
96
101
|
aitextaroo connect [claude-code|codex|gemini|cursor|muse-code|claude|chatgpt|grok]
|
|
97
102
|
```
|
|
98
103
|
|
|
@@ -162,8 +167,8 @@ asyncio.run(main())
|
|
|
162
167
|
| `listen()` | Async iterator over inbound texts via SSE, with automatic reconnect and replay. |
|
|
163
168
|
| `text_contact(to, text, recipient_name=None)` | Pro: text someone else; their replies go to the user's phone. |
|
|
164
169
|
| `verify_pin(pin)` | Check a PIN the user texted: `{"verified": bool, "attempts_remaining": int \| None}`. |
|
|
165
|
-
| `set_pin(pin)` / `remove_pin()` | Set
|
|
166
|
-
| `set_webhook(url, auth_header=None)` / `remove_webhook()` | Forward inbound texts to a URL (signed with `X-Signature`). |
|
|
170
|
+
| `set_pin(pin, current_pin=None)` / `remove_pin(current_pin)` | Set the security PIN the user chose (4–8 digits), change it (needs `current_pin`), or remove it (needs the current PIN). The user is texted before any change. |
|
|
171
|
+
| `set_webhook(url, auth_header=None, pin=None)` / `remove_webhook()` | Forward inbound texts to a URL (signed with `X-Signature`). `pin` is required if the user has one; the user is texted where their texts go and can reply `UNHOOK` to stop it. |
|
|
167
172
|
| `account()` | Plan, assigned number, usage and daily limit. |
|
|
168
173
|
| `upgrade_link()` | Stripe checkout link for Pro. |
|
|
169
174
|
| `TextarooClient.request_verification(phone)` / `confirm_verification(phone, code)` | Sign-up, no key needed. |
|
|
@@ -174,8 +179,10 @@ asyncio.run(main())
|
|
|
174
179
|
## MCP server
|
|
175
180
|
|
|
176
181
|
`https://api.aitextaroo.com/mcp` (streamable HTTP). Tools: `send_text`, `wait_for_reply`,
|
|
177
|
-
`get_recent_messages`, `text_contact`, `verify_pin`, `get_account`, `get_upgrade_link
|
|
178
|
-
|
|
182
|
+
`get_recent_messages`, `text_contact`, `verify_pin`, `get_account`, `get_upgrade_link`,
|
|
183
|
+
`set_webhook`, `remove_webhook`, and `get_help` (how it works and how it's secured, for
|
|
184
|
+
answering the user's questions). Clients sign in with OAuth, or send
|
|
185
|
+
`Authorization: Bearer <API key>`:
|
|
179
186
|
|
|
180
187
|
```bash
|
|
181
188
|
claude mcp add --transport http --scope user aitextaroo https://api.aitextaroo.com/mcp \
|
|
@@ -217,8 +224,9 @@ Details: <https://aitextaroo.com/security>.
|
|
|
217
224
|
|
|
218
225
|
## Releasing
|
|
219
226
|
|
|
220
|
-
Bump
|
|
221
|
-
`
|
|
227
|
+
Bump the version in `pyproject.toml`, `aitextaroo/__init__.py`, `plugin.json`,
|
|
228
|
+
`.claude-plugin/plugin.json` and `gemini-extension.json`, merge to `main`, then push a
|
|
229
|
+
matching tag: `git tag v0.2.2 && git push origin v0.2.2`. The
|
|
222
230
|
[Release workflow](.github/workflows/release.yml) tests, builds and publishes to PyPI with
|
|
223
231
|
Trusted Publishing.
|
|
224
232
|
|
|
@@ -41,13 +41,16 @@ aitextaroo send "Hello from my agent!"
|
|
|
41
41
|
|
|
42
42
|
`--save` stores the API key in `~/.aitextaroo/credentials.json` with owner-only permissions.
|
|
43
43
|
`aitextaroo` and `aitextaroo-bridge` find it there, or in `AITEXTAROO_API_KEY`, or in `--api-key`.
|
|
44
|
+
Verifying a phone that already has an account makes a new key: apps connected before are
|
|
45
|
+
signed out and webhook forwarding is turned off.
|
|
44
46
|
|
|
45
47
|
## The `aitextaroo` command
|
|
46
48
|
|
|
47
49
|
Made for agents with a shell. Output is JSON; errors go to stderr with exit code 1.
|
|
48
50
|
`aitextaroo verify-pin` exits 2 when the PIN is wrong, so `aitextaroo verify-pin <<< 4821 && …`
|
|
49
51
|
only continues on a match. `verify-pin` and `set-pin` read the PIN from stdin when it isn't
|
|
50
|
-
an argument, which keeps it out of shell
|
|
52
|
+
an argument. At a terminal they ask for it without echoing, which keeps it out of your shell
|
|
53
|
+
history (a command with the PIN written in it, like the `<<<` form, can still be saved there).
|
|
51
54
|
|
|
52
55
|
```bash
|
|
53
56
|
aitextaroo send "Build finished: 318 tests passing."
|
|
@@ -55,11 +58,13 @@ aitextaroo ask "Deploy to production? Reply YES or NO" --timeout 120 # text, t
|
|
|
55
58
|
aitextaroo wait --timeout 60 # wait for the next text
|
|
56
59
|
aitextaroo recent # texts from the last 5 minutes
|
|
57
60
|
aitextaroo relay --to +12125550100 --name Mom "Happy birthday!" # Pro: text a contact
|
|
58
|
-
aitextaroo verify-pin
|
|
59
|
-
aitextaroo set-pin
|
|
61
|
+
aitextaroo verify-pin # check a PIN (hidden prompt)
|
|
62
|
+
aitextaroo set-pin # set the user's PIN (4-8 digits)
|
|
63
|
+
aitextaroo set-pin --current # change it: current PIN, then the new one
|
|
64
|
+
aitextaroo remove-pin # needs the current PIN
|
|
60
65
|
aitextaroo account
|
|
61
66
|
aitextaroo upgrade # Stripe link for Pro
|
|
62
|
-
aitextaroo webhook set https://example.com/hook --auth-header "Bearer <token>"
|
|
67
|
+
aitextaroo webhook set https://example.com/hook --auth-header "Bearer <token>" # add --pin if a PIN is set
|
|
63
68
|
aitextaroo connect [claude-code|codex|gemini|cursor|muse-code|claude|chatgpt|grok]
|
|
64
69
|
```
|
|
65
70
|
|
|
@@ -129,8 +134,8 @@ asyncio.run(main())
|
|
|
129
134
|
| `listen()` | Async iterator over inbound texts via SSE, with automatic reconnect and replay. |
|
|
130
135
|
| `text_contact(to, text, recipient_name=None)` | Pro: text someone else; their replies go to the user's phone. |
|
|
131
136
|
| `verify_pin(pin)` | Check a PIN the user texted: `{"verified": bool, "attempts_remaining": int \| None}`. |
|
|
132
|
-
| `set_pin(pin)` / `remove_pin()` | Set
|
|
133
|
-
| `set_webhook(url, auth_header=None)` / `remove_webhook()` | Forward inbound texts to a URL (signed with `X-Signature`). |
|
|
137
|
+
| `set_pin(pin, current_pin=None)` / `remove_pin(current_pin)` | Set the security PIN the user chose (4–8 digits), change it (needs `current_pin`), or remove it (needs the current PIN). The user is texted before any change. |
|
|
138
|
+
| `set_webhook(url, auth_header=None, pin=None)` / `remove_webhook()` | Forward inbound texts to a URL (signed with `X-Signature`). `pin` is required if the user has one; the user is texted where their texts go and can reply `UNHOOK` to stop it. |
|
|
134
139
|
| `account()` | Plan, assigned number, usage and daily limit. |
|
|
135
140
|
| `upgrade_link()` | Stripe checkout link for Pro. |
|
|
136
141
|
| `TextarooClient.request_verification(phone)` / `confirm_verification(phone, code)` | Sign-up, no key needed. |
|
|
@@ -141,8 +146,10 @@ asyncio.run(main())
|
|
|
141
146
|
## MCP server
|
|
142
147
|
|
|
143
148
|
`https://api.aitextaroo.com/mcp` (streamable HTTP). Tools: `send_text`, `wait_for_reply`,
|
|
144
|
-
`get_recent_messages`, `text_contact`, `verify_pin`, `get_account`, `get_upgrade_link
|
|
145
|
-
|
|
149
|
+
`get_recent_messages`, `text_contact`, `verify_pin`, `get_account`, `get_upgrade_link`,
|
|
150
|
+
`set_webhook`, `remove_webhook`, and `get_help` (how it works and how it's secured, for
|
|
151
|
+
answering the user's questions). Clients sign in with OAuth, or send
|
|
152
|
+
`Authorization: Bearer <API key>`:
|
|
146
153
|
|
|
147
154
|
```bash
|
|
148
155
|
claude mcp add --transport http --scope user aitextaroo https://api.aitextaroo.com/mcp \
|
|
@@ -184,8 +191,9 @@ Details: <https://aitextaroo.com/security>.
|
|
|
184
191
|
|
|
185
192
|
## Releasing
|
|
186
193
|
|
|
187
|
-
Bump
|
|
188
|
-
`
|
|
194
|
+
Bump the version in `pyproject.toml`, `aitextaroo/__init__.py`, `plugin.json`,
|
|
195
|
+
`.claude-plugin/plugin.json` and `gemini-extension.json`, merge to `main`, then push a
|
|
196
|
+
matching tag: `git tag v0.2.2 && git push origin v0.2.2`. The
|
|
189
197
|
[Release workflow](.github/workflows/release.yml) tests, builds and publishes to PyPI with
|
|
190
198
|
Trusted Publishing.
|
|
191
199
|
|
|
@@ -8,9 +8,11 @@ Usage:
|
|
|
8
8
|
aitextaroo relay --to +12125550100 --name Mom "Happy birthday!"
|
|
9
9
|
aitextaroo verify-pin <<< 4821
|
|
10
10
|
aitextaroo set-pin <<< 4821
|
|
11
|
+
aitextaroo set-pin --current # change it: current PIN, then the new one
|
|
12
|
+
aitextaroo remove-pin # asks for the current PIN
|
|
11
13
|
aitextaroo account
|
|
12
14
|
aitextaroo upgrade
|
|
13
|
-
aitextaroo webhook set https://example.com/hook [--auth-header "Bearer ..."]
|
|
15
|
+
aitextaroo webhook set https://example.com/hook [--auth-header "Bearer ..."] [--pin]
|
|
14
16
|
aitextaroo webhook remove
|
|
15
17
|
aitextaroo connect [claude-code|codex|gemini|cursor|muse-code|claude|chatgpt|grok]
|
|
16
18
|
|
|
@@ -18,8 +20,9 @@ Results are JSON on stdout (except `connect`, which prints instructions);
|
|
|
18
20
|
errors go to stderr with exit code 1. `verify-pin` prints its result and exits
|
|
19
21
|
with code 2 when the PIN is wrong, so `aitextaroo verify-pin <<< 4821 && ...` only
|
|
20
22
|
proceeds on a match. `verify-pin` and `set-pin` read the PIN from stdin when
|
|
21
|
-
it isn't given as an argument
|
|
22
|
-
|
|
23
|
+
it isn't given as an argument; at a terminal they ask for it without echoing.
|
|
24
|
+
`webhook set` needs the user's PIN if they set one: pass bare `--pin` to read
|
|
25
|
+
it the same way. The API key comes from --api-key, AITEXTAROO_API_KEY, or
|
|
23
26
|
~/.aitextaroo/credentials.json (aitextaroo-setup --save).
|
|
24
27
|
"""
|
|
25
28
|
|
|
@@ -104,8 +107,11 @@ CONNECT: dict[str, str] = {
|
|
|
104
107
|
"grok": (
|
|
105
108
|
"Grok Bot — say in chat:\n"
|
|
106
109
|
f" Add a custom MCP server called AI Text-a-roo at {MCP_URL}\n"
|
|
107
|
-
"To text it back anytime, create a webhook routine and
|
|
108
|
-
|
|
110
|
+
"To text it back anytime, ask Grok Bot to create a webhook routine and\n"
|
|
111
|
+
"register it with the set_webhook tool, or register it yourself:\n"
|
|
112
|
+
' aitextaroo webhook set <routine URL> --auth-header "Bearer <routine key>" [--pin]\n'
|
|
113
|
+
"(--pin asks for your PIN, needed if you set one. You'll get a text saying\n"
|
|
114
|
+
"where your texts now go; reply UNHOOK anytime to stop forwarding.)"
|
|
109
115
|
),
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -121,8 +127,16 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
121
127
|
"No API key. Run `aitextaroo-setup --phone +1... --save`, "
|
|
122
128
|
"set AITEXTAROO_API_KEY, or pass --api-key."
|
|
123
129
|
)
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
# Changing an existing PIN: the current one comes first (on stdin, the
|
|
131
|
+
# first line), then the new one.
|
|
132
|
+
if args.command == "set-pin" and args.current is not None:
|
|
133
|
+
args.current = _read_pin(args.current, "Current PIN: ")
|
|
134
|
+
# webhook set only needs the PIN when --pin was given (accounts with a PIN).
|
|
135
|
+
needs_pin = args.command in ("verify-pin", "set-pin", "remove-pin") or (
|
|
136
|
+
args.command == "webhook" and args.action == "set" and args.pin is not None
|
|
137
|
+
)
|
|
138
|
+
if needs_pin:
|
|
139
|
+
args.pin = _read_pin(args.pin, "New PIN: " if args.command == "set-pin" else "PIN: ")
|
|
126
140
|
try:
|
|
127
141
|
result = asyncio.run(_run(args, key))
|
|
128
142
|
except TextarooError as exc:
|
|
@@ -159,14 +173,18 @@ async def _run(args: argparse.Namespace, key: str) -> Any:
|
|
|
159
173
|
if args.command == "verify-pin":
|
|
160
174
|
return await client.verify_pin(args.pin)
|
|
161
175
|
if args.command == "set-pin":
|
|
162
|
-
return await client.set_pin(args.pin)
|
|
176
|
+
return await client.set_pin(args.pin, current_pin=args.current)
|
|
177
|
+
if args.command == "remove-pin":
|
|
178
|
+
return await client.remove_pin(args.pin)
|
|
163
179
|
if args.command == "account":
|
|
164
180
|
return await client.account()
|
|
165
181
|
if args.command == "upgrade":
|
|
166
182
|
return {"checkout_url": await client.upgrade_link()}
|
|
167
183
|
if args.command == "webhook":
|
|
168
184
|
if args.action == "set":
|
|
169
|
-
return await client.set_webhook(
|
|
185
|
+
return await client.set_webhook(
|
|
186
|
+
args.url, auth_header=args.auth_header, pin=args.pin
|
|
187
|
+
)
|
|
170
188
|
return await client.remove_webhook()
|
|
171
189
|
raise AssertionError(f"unhandled command {args.command}")
|
|
172
190
|
finally:
|
|
@@ -220,7 +238,17 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
|
220
238
|
p.add_argument("pin", nargs="?", help="The PIN (default: read from stdin)")
|
|
221
239
|
|
|
222
240
|
p = sub.add_parser("set-pin", help="Set or change the user's security PIN (4-8 digits)")
|
|
223
|
-
p.add_argument("pin", nargs="?", help="The PIN (default: read from stdin)")
|
|
241
|
+
p.add_argument("pin", nargs="?", help="The new PIN (default: read from stdin)")
|
|
242
|
+
p.add_argument(
|
|
243
|
+
"--current",
|
|
244
|
+
nargs="?",
|
|
245
|
+
const="-",
|
|
246
|
+
default=None,
|
|
247
|
+
help="The current PIN, needed to change one. Bare --current reads it from stdin first",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
p = sub.add_parser("remove-pin", help="Remove the user's security PIN (needs the current one)")
|
|
251
|
+
p.add_argument("pin", nargs="?", help="The current PIN (default: read from stdin)")
|
|
224
252
|
|
|
225
253
|
sub.add_parser("account", help="Plan, usage and the AI's phone number")
|
|
226
254
|
sub.add_parser("upgrade", help="Stripe checkout link for Pro")
|
|
@@ -234,6 +262,13 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
|
234
262
|
default=None,
|
|
235
263
|
help='Authorization header value sent with each delivery, e.g. "Bearer <token>"',
|
|
236
264
|
)
|
|
265
|
+
s.add_argument(
|
|
266
|
+
"--pin",
|
|
267
|
+
nargs="?",
|
|
268
|
+
const="-",
|
|
269
|
+
default=None,
|
|
270
|
+
help="The user's PIN, if they set one. Bare --pin reads it from stdin or a hidden prompt",
|
|
271
|
+
)
|
|
237
272
|
hook.add_parser("remove", help="Stop webhook delivery")
|
|
238
273
|
|
|
239
274
|
p = sub.add_parser("connect", help="Show how to add the MCP server to an agent")
|
|
@@ -242,15 +277,15 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
|
242
277
|
return parser.parse_args(argv)
|
|
243
278
|
|
|
244
279
|
|
|
245
|
-
def _read_pin(arg: str | None) -> str:
|
|
280
|
+
def _read_pin(arg: str | None, prompt: str = "PIN: ") -> str:
|
|
246
281
|
"""The PIN from the argument, or from stdin when it's omitted or "-".
|
|
247
282
|
|
|
248
|
-
At a terminal it's a hidden prompt; otherwise the
|
|
283
|
+
At a terminal it's a hidden prompt; otherwise the next line of stdin.
|
|
249
284
|
"""
|
|
250
285
|
if arg not in (None, "-"):
|
|
251
286
|
pin = arg
|
|
252
287
|
elif sys.stdin.isatty():
|
|
253
|
-
pin = getpass.getpass(
|
|
288
|
+
pin = getpass.getpass(prompt)
|
|
254
289
|
else:
|
|
255
290
|
pin = sys.stdin.readline()
|
|
256
291
|
pin = pin.strip()
|
|
@@ -368,55 +368,74 @@ class TextarooClient:
|
|
|
368
368
|
response = await client.post("/v1/verify-pin", json={"pin": pin})
|
|
369
369
|
return _parse_json_response(response, "PIN verification failed")
|
|
370
370
|
|
|
371
|
-
async def set_pin(self, pin: str) -> dict[str, Any]:
|
|
372
|
-
"""Set
|
|
371
|
+
async def set_pin(self, pin: str, current_pin: str | None = None) -> dict[str, Any]:
|
|
372
|
+
"""Set the user's security PIN (4–8 digits), or change it.
|
|
373
373
|
|
|
374
|
-
Ask the user to choose it; don't store a
|
|
374
|
+
Ask the user to choose it; never pick one for them, and don't store a
|
|
375
|
+
copy. Changing an existing PIN needs current_pin. The user is texted
|
|
376
|
+
before the change.
|
|
375
377
|
|
|
376
378
|
Returns:
|
|
377
|
-
{"status": "pin_set"}
|
|
379
|
+
{"status": "pin_set"} or {"status": "pin_changed"}
|
|
378
380
|
|
|
379
381
|
Raises:
|
|
380
|
-
TextarooError:
|
|
382
|
+
TextarooError: "current_pin_required", "pin_incorrect", "pin_locked",
|
|
383
|
+
"owner_notice_failed" (nothing changed), or another API error.
|
|
381
384
|
"""
|
|
385
|
+
body: dict[str, Any] = {"pin": pin}
|
|
386
|
+
if current_pin:
|
|
387
|
+
body["current_pin"] = current_pin
|
|
382
388
|
client = await self._get_client()
|
|
383
|
-
response = await client.post("/v1/account/pin", json=
|
|
389
|
+
response = await client.post("/v1/account/pin", json=body)
|
|
384
390
|
return _parse_json_response(response, "Setting the PIN failed")
|
|
385
391
|
|
|
386
|
-
async def remove_pin(self) -> dict[str, Any]:
|
|
387
|
-
"""Remove the user's security PIN.
|
|
392
|
+
async def remove_pin(self, current_pin: str) -> dict[str, Any]:
|
|
393
|
+
"""Remove the user's security PIN, which needs the current one.
|
|
394
|
+
|
|
395
|
+
The user is texted before the change.
|
|
388
396
|
|
|
389
397
|
Returns:
|
|
390
|
-
{"status": "pin_removed"}
|
|
398
|
+
{"status": "pin_removed"} or {"status": "pin_not_set"}
|
|
391
399
|
|
|
392
400
|
Raises:
|
|
393
|
-
TextarooError:
|
|
401
|
+
TextarooError: "current_pin_required", "pin_incorrect", or another API error.
|
|
394
402
|
"""
|
|
395
403
|
client = await self._get_client()
|
|
396
|
-
response = await client.
|
|
404
|
+
response = await client.request(
|
|
405
|
+
"DELETE", "/v1/account/pin", json={"current_pin": current_pin}
|
|
406
|
+
)
|
|
397
407
|
return _parse_json_response(response, "Removing the PIN failed")
|
|
398
408
|
|
|
399
409
|
# ── Webhooks ────────────────────────────────────────────────
|
|
400
410
|
|
|
401
|
-
async def set_webhook(
|
|
411
|
+
async def set_webhook(
|
|
412
|
+
self, url: str, auth_header: str | None = None, pin: str | None = None
|
|
413
|
+
) -> dict[str, Any]:
|
|
402
414
|
"""Forward each text the user sends to a URL as a signed JSON POST.
|
|
403
415
|
|
|
416
|
+
The user is texted the destination host before forwarding starts, and
|
|
417
|
+
can text UNHOOK to stop it.
|
|
418
|
+
|
|
404
419
|
Args:
|
|
405
420
|
url: Public https URL that receives the deliveries.
|
|
406
421
|
auth_header: Optional Authorization header value sent with each
|
|
407
422
|
delivery (e.g., "Bearer <token>" for a Grok Bot webhook routine).
|
|
423
|
+
pin: The user's security PIN. Required if they have one.
|
|
408
424
|
|
|
409
425
|
Returns:
|
|
410
|
-
{"status": "registered", "url": ..., "secret": ..., "has_auth_header": bool
|
|
411
|
-
Verify deliveries with the secret:
|
|
412
|
-
HMAC-SHA256 of the raw body.
|
|
426
|
+
{"status": "registered", "url": ..., "secret": ..., "has_auth_header": bool,
|
|
427
|
+
"owner_notified": bool}. Verify deliveries with the secret:
|
|
428
|
+
X-Signature is a hex HMAC-SHA256 of the raw body.
|
|
413
429
|
|
|
414
430
|
Raises:
|
|
415
|
-
TextarooError:
|
|
431
|
+
TextarooError: "pin_required" or "pin_incorrect", "owner_notice_failed"
|
|
432
|
+
(nothing changed), "rate_limit_exceeded", or another API error.
|
|
416
433
|
"""
|
|
417
434
|
body: dict[str, Any] = {"url": url}
|
|
418
435
|
if auth_header:
|
|
419
436
|
body["auth_header"] = auth_header
|
|
437
|
+
if pin:
|
|
438
|
+
body["pin"] = pin
|
|
420
439
|
client = await self._get_client()
|
|
421
440
|
response = await client.post("/v1/webhook", json=body)
|
|
422
441
|
return _parse_json_response(response, "Webhook registration failed")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aitextaroo
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Client library for AI Text-a-roo — give any AI agent a phone number
|
|
5
5
|
Author-email: AI Text-a-roo <hello@aitextaroo.com>
|
|
6
6
|
License: MIT
|
|
@@ -74,13 +74,16 @@ aitextaroo send "Hello from my agent!"
|
|
|
74
74
|
|
|
75
75
|
`--save` stores the API key in `~/.aitextaroo/credentials.json` with owner-only permissions.
|
|
76
76
|
`aitextaroo` and `aitextaroo-bridge` find it there, or in `AITEXTAROO_API_KEY`, or in `--api-key`.
|
|
77
|
+
Verifying a phone that already has an account makes a new key: apps connected before are
|
|
78
|
+
signed out and webhook forwarding is turned off.
|
|
77
79
|
|
|
78
80
|
## The `aitextaroo` command
|
|
79
81
|
|
|
80
82
|
Made for agents with a shell. Output is JSON; errors go to stderr with exit code 1.
|
|
81
83
|
`aitextaroo verify-pin` exits 2 when the PIN is wrong, so `aitextaroo verify-pin <<< 4821 && …`
|
|
82
84
|
only continues on a match. `verify-pin` and `set-pin` read the PIN from stdin when it isn't
|
|
83
|
-
an argument, which keeps it out of shell
|
|
85
|
+
an argument. At a terminal they ask for it without echoing, which keeps it out of your shell
|
|
86
|
+
history (a command with the PIN written in it, like the `<<<` form, can still be saved there).
|
|
84
87
|
|
|
85
88
|
```bash
|
|
86
89
|
aitextaroo send "Build finished: 318 tests passing."
|
|
@@ -88,11 +91,13 @@ aitextaroo ask "Deploy to production? Reply YES or NO" --timeout 120 # text, t
|
|
|
88
91
|
aitextaroo wait --timeout 60 # wait for the next text
|
|
89
92
|
aitextaroo recent # texts from the last 5 minutes
|
|
90
93
|
aitextaroo relay --to +12125550100 --name Mom "Happy birthday!" # Pro: text a contact
|
|
91
|
-
aitextaroo verify-pin
|
|
92
|
-
aitextaroo set-pin
|
|
94
|
+
aitextaroo verify-pin # check a PIN (hidden prompt)
|
|
95
|
+
aitextaroo set-pin # set the user's PIN (4-8 digits)
|
|
96
|
+
aitextaroo set-pin --current # change it: current PIN, then the new one
|
|
97
|
+
aitextaroo remove-pin # needs the current PIN
|
|
93
98
|
aitextaroo account
|
|
94
99
|
aitextaroo upgrade # Stripe link for Pro
|
|
95
|
-
aitextaroo webhook set https://example.com/hook --auth-header "Bearer <token>"
|
|
100
|
+
aitextaroo webhook set https://example.com/hook --auth-header "Bearer <token>" # add --pin if a PIN is set
|
|
96
101
|
aitextaroo connect [claude-code|codex|gemini|cursor|muse-code|claude|chatgpt|grok]
|
|
97
102
|
```
|
|
98
103
|
|
|
@@ -162,8 +167,8 @@ asyncio.run(main())
|
|
|
162
167
|
| `listen()` | Async iterator over inbound texts via SSE, with automatic reconnect and replay. |
|
|
163
168
|
| `text_contact(to, text, recipient_name=None)` | Pro: text someone else; their replies go to the user's phone. |
|
|
164
169
|
| `verify_pin(pin)` | Check a PIN the user texted: `{"verified": bool, "attempts_remaining": int \| None}`. |
|
|
165
|
-
| `set_pin(pin)` / `remove_pin()` | Set
|
|
166
|
-
| `set_webhook(url, auth_header=None)` / `remove_webhook()` | Forward inbound texts to a URL (signed with `X-Signature`). |
|
|
170
|
+
| `set_pin(pin, current_pin=None)` / `remove_pin(current_pin)` | Set the security PIN the user chose (4–8 digits), change it (needs `current_pin`), or remove it (needs the current PIN). The user is texted before any change. |
|
|
171
|
+
| `set_webhook(url, auth_header=None, pin=None)` / `remove_webhook()` | Forward inbound texts to a URL (signed with `X-Signature`). `pin` is required if the user has one; the user is texted where their texts go and can reply `UNHOOK` to stop it. |
|
|
167
172
|
| `account()` | Plan, assigned number, usage and daily limit. |
|
|
168
173
|
| `upgrade_link()` | Stripe checkout link for Pro. |
|
|
169
174
|
| `TextarooClient.request_verification(phone)` / `confirm_verification(phone, code)` | Sign-up, no key needed. |
|
|
@@ -174,8 +179,10 @@ asyncio.run(main())
|
|
|
174
179
|
## MCP server
|
|
175
180
|
|
|
176
181
|
`https://api.aitextaroo.com/mcp` (streamable HTTP). Tools: `send_text`, `wait_for_reply`,
|
|
177
|
-
`get_recent_messages`, `text_contact`, `verify_pin`, `get_account`, `get_upgrade_link
|
|
178
|
-
|
|
182
|
+
`get_recent_messages`, `text_contact`, `verify_pin`, `get_account`, `get_upgrade_link`,
|
|
183
|
+
`set_webhook`, `remove_webhook`, and `get_help` (how it works and how it's secured, for
|
|
184
|
+
answering the user's questions). Clients sign in with OAuth, or send
|
|
185
|
+
`Authorization: Bearer <API key>`:
|
|
179
186
|
|
|
180
187
|
```bash
|
|
181
188
|
claude mcp add --transport http --scope user aitextaroo https://api.aitextaroo.com/mcp \
|
|
@@ -217,8 +224,9 @@ Details: <https://aitextaroo.com/security>.
|
|
|
217
224
|
|
|
218
225
|
## Releasing
|
|
219
226
|
|
|
220
|
-
Bump
|
|
221
|
-
`
|
|
227
|
+
Bump the version in `pyproject.toml`, `aitextaroo/__init__.py`, `plugin.json`,
|
|
228
|
+
`.claude-plugin/plugin.json` and `gemini-extension.json`, merge to `main`, then push a
|
|
229
|
+
matching tag: `git tag v0.2.2 && git push origin v0.2.2`. The
|
|
222
230
|
[Release workflow](.github/workflows/release.yml) tests, builds and publishes to PyPI with
|
|
223
231
|
Trusted Publishing.
|
|
224
232
|
|
|
@@ -145,7 +145,19 @@ class TestCommands:
|
|
|
145
145
|
capsys,
|
|
146
146
|
)
|
|
147
147
|
assert out["has_auth_header"] is True
|
|
148
|
-
client.set_webhook.assert_awaited_once_with(
|
|
148
|
+
client.set_webhook.assert_awaited_once_with(
|
|
149
|
+
"https://hook.example", auth_header="Bearer t", pin=None
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def test_webhook_set_reads_the_pin_from_stdin(self, capsys, monkeypatch) -> None:
|
|
153
|
+
monkeypatch.setattr("sys.stdin", io.StringIO("4821\n"))
|
|
154
|
+
client = self._client()
|
|
155
|
+
client.set_webhook.return_value = {"status": "registered", "owner_notified": True}
|
|
156
|
+
with patch.object(agent_cli, "TextarooClient", return_value=client):
|
|
157
|
+
_run_cli(["--api-key", "k", "webhook", "set", "https://hook.example", "--pin"], capsys)
|
|
158
|
+
client.set_webhook.assert_awaited_once_with(
|
|
159
|
+
"https://hook.example", auth_header=None, pin="4821"
|
|
160
|
+
)
|
|
149
161
|
|
|
150
162
|
def test_api_error_exits_nonzero(self, capsys) -> None:
|
|
151
163
|
from aitextaroo.client import TextarooError
|
|
@@ -197,7 +209,25 @@ class TestCommands:
|
|
|
197
209
|
with patch.object(agent_cli, "TextarooClient", return_value=client):
|
|
198
210
|
out = _run_cli(["--api-key", "k", "set-pin"], capsys)
|
|
199
211
|
assert out == {"status": "pin_set"}
|
|
200
|
-
client.set_pin.assert_awaited_once_with("482193")
|
|
212
|
+
client.set_pin.assert_awaited_once_with("482193", current_pin=None)
|
|
213
|
+
|
|
214
|
+
def test_changing_the_pin_reads_the_current_one_first(self, capsys, monkeypatch) -> None:
|
|
215
|
+
monkeypatch.setattr("sys.stdin", io.StringIO("1111\n4821\n"))
|
|
216
|
+
client = self._client()
|
|
217
|
+
client.set_pin.return_value = {"status": "pin_changed"}
|
|
218
|
+
with patch.object(agent_cli, "TextarooClient", return_value=client):
|
|
219
|
+
out = _run_cli(["--api-key", "k", "set-pin", "--current"], capsys)
|
|
220
|
+
assert out == {"status": "pin_changed"}
|
|
221
|
+
client.set_pin.assert_awaited_once_with("4821", current_pin="1111")
|
|
222
|
+
|
|
223
|
+
def test_remove_pin_reads_the_current_pin(self, capsys, monkeypatch) -> None:
|
|
224
|
+
monkeypatch.setattr("sys.stdin", io.StringIO("1111\n"))
|
|
225
|
+
client = self._client()
|
|
226
|
+
client.remove_pin.return_value = {"status": "pin_removed"}
|
|
227
|
+
with patch.object(agent_cli, "TextarooClient", return_value=client):
|
|
228
|
+
out = _run_cli(["--api-key", "k", "remove-pin"], capsys)
|
|
229
|
+
assert out == {"status": "pin_removed"}
|
|
230
|
+
client.remove_pin.assert_awaited_once_with("1111")
|
|
201
231
|
|
|
202
232
|
def test_pin_prompt_is_hidden_at_a_terminal(self, capsys, monkeypatch) -> None:
|
|
203
233
|
tty = io.StringIO()
|
|
@@ -138,11 +138,19 @@ class TestVerifyPin:
|
|
|
138
138
|
assert json.loads(seen[0].content) == {"pin": "4821"}
|
|
139
139
|
await client.close()
|
|
140
140
|
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_change_pin_sends_the_current_one(self) -> None:
|
|
143
|
+
client, seen = _recording_client(200, '{"status": "pin_changed"}')
|
|
144
|
+
assert await client.set_pin("4821", current_pin="1111") == {"status": "pin_changed"}
|
|
145
|
+
assert json.loads(seen[0].content) == {"pin": "4821", "current_pin": "1111"}
|
|
146
|
+
await client.close()
|
|
147
|
+
|
|
141
148
|
@pytest.mark.asyncio
|
|
142
149
|
async def test_remove_pin(self) -> None:
|
|
143
150
|
client, seen = _recording_client(200, '{"status": "pin_removed"}')
|
|
144
|
-
assert await client.remove_pin() == {"status": "pin_removed"}
|
|
151
|
+
assert await client.remove_pin("1111") == {"status": "pin_removed"}
|
|
145
152
|
assert (seen[0].method, seen[0].url.path) == ("DELETE", "/v1/account/pin")
|
|
153
|
+
assert json.loads(seen[0].content) == {"current_pin": "1111"}
|
|
146
154
|
await client.close()
|
|
147
155
|
|
|
148
156
|
|
|
@@ -162,6 +170,23 @@ class TestWebhook:
|
|
|
162
170
|
}
|
|
163
171
|
await client.close()
|
|
164
172
|
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_set_with_pin(self) -> None:
|
|
175
|
+
client, seen = _recording_client(200, '{"status": "registered", "owner_notified": true}')
|
|
176
|
+
await client.set_webhook("https://hook.example", pin="4821")
|
|
177
|
+
assert json.loads(seen[0].content) == {"url": "https://hook.example", "pin": "4821"}
|
|
178
|
+
await client.close()
|
|
179
|
+
|
|
180
|
+
@pytest.mark.asyncio
|
|
181
|
+
async def test_pin_required_is_an_error(self) -> None:
|
|
182
|
+
client, _ = _recording_client(
|
|
183
|
+
403, '{"error": {"code": "pin_required", "message": "This account has a security PIN"}}'
|
|
184
|
+
)
|
|
185
|
+
with pytest.raises(TextarooError) as exc_info:
|
|
186
|
+
await client.set_webhook("https://hook.example")
|
|
187
|
+
assert exc_info.value.code == "pin_required"
|
|
188
|
+
await client.close()
|
|
189
|
+
|
|
165
190
|
@pytest.mark.asyncio
|
|
166
191
|
async def test_remove(self) -> None:
|
|
167
192
|
client, seen = _recording_client(200, '{"status": "removed"}')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|