ai-cli-toolkit 0.2.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.
Files changed (55) hide show
  1. ai_cli_toolkit-0.2.0/LICENSE +21 -0
  2. ai_cli_toolkit-0.2.0/PKG-INFO +17 -0
  3. ai_cli_toolkit-0.2.0/README.md +318 -0
  4. ai_cli_toolkit-0.2.0/ai_cli/__init__.py +3 -0
  5. ai_cli_toolkit-0.2.0/ai_cli/__main__.py +6 -0
  6. ai_cli_toolkit-0.2.0/ai_cli/bin/ai-mux-linux-x86_64 +0 -0
  7. ai_cli_toolkit-0.2.0/ai_cli/bin/remote-tty-wrapper +153 -0
  8. ai_cli_toolkit-0.2.0/ai_cli/ca.py +175 -0
  9. ai_cli_toolkit-0.2.0/ai_cli/completion_gen.py +680 -0
  10. ai_cli_toolkit-0.2.0/ai_cli/config.py +185 -0
  11. ai_cli_toolkit-0.2.0/ai_cli/credentials.py +341 -0
  12. ai_cli_toolkit-0.2.0/ai_cli/detached_cleanup.py +135 -0
  13. ai_cli_toolkit-0.2.0/ai_cli/housekeeping.py +50 -0
  14. ai_cli_toolkit-0.2.0/ai_cli/instructions.py +308 -0
  15. ai_cli_toolkit-0.2.0/ai_cli/log.py +53 -0
  16. ai_cli_toolkit-0.2.0/ai_cli/main.py +1516 -0
  17. ai_cli_toolkit-0.2.0/ai_cli/main_helpers.py +553 -0
  18. ai_cli_toolkit-0.2.0/ai_cli/prompt_editor_launcher.py +324 -0
  19. ai_cli_toolkit-0.2.0/ai_cli/proxy.py +627 -0
  20. ai_cli_toolkit-0.2.0/ai_cli/remote.py +669 -0
  21. ai_cli_toolkit-0.2.0/ai_cli/remote_package.py +1111 -0
  22. ai_cli_toolkit-0.2.0/ai_cli/session.py +1344 -0
  23. ai_cli_toolkit-0.2.0/ai_cli/session_store.py +236 -0
  24. ai_cli_toolkit-0.2.0/ai_cli/traffic.py +1510 -0
  25. ai_cli_toolkit-0.2.0/ai_cli/traffic_db.py +118 -0
  26. ai_cli_toolkit-0.2.0/ai_cli/tui.py +525 -0
  27. ai_cli_toolkit-0.2.0/ai_cli/update.py +200 -0
  28. ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/PKG-INFO +17 -0
  29. ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/SOURCES.txt +53 -0
  30. ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/dependency_links.txt +1 -0
  31. ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/entry_points.txt +2 -0
  32. ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/requires.txt +10 -0
  33. ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/top_level.txt +1 -0
  34. ai_cli_toolkit-0.2.0/pyproject.toml +75 -0
  35. ai_cli_toolkit-0.2.0/setup.cfg +4 -0
  36. ai_cli_toolkit-0.2.0/setup.py +51 -0
  37. ai_cli_toolkit-0.2.0/tests/test_cleanup.py +40 -0
  38. ai_cli_toolkit-0.2.0/tests/test_codex_addon_websocket.py +284 -0
  39. ai_cli_toolkit-0.2.0/tests/test_codex_instruction_modes.py +108 -0
  40. ai_cli_toolkit-0.2.0/tests/test_completion_gen.py +47 -0
  41. ai_cli_toolkit-0.2.0/tests/test_config.py +100 -0
  42. ai_cli_toolkit-0.2.0/tests/test_gemini_addon.py +27 -0
  43. ai_cli_toolkit-0.2.0/tests/test_instructions.py +159 -0
  44. ai_cli_toolkit-0.2.0/tests/test_main_helpers_mux.py +52 -0
  45. ai_cli_toolkit-0.2.0/tests/test_main_prompt_overrides.py +75 -0
  46. ai_cli_toolkit-0.2.0/tests/test_main_proxy_failure.py +70 -0
  47. ai_cli_toolkit-0.2.0/tests/test_main_remote_sync.py +250 -0
  48. ai_cli_toolkit-0.2.0/tests/test_prompt_editor_launcher.py +143 -0
  49. ai_cli_toolkit-0.2.0/tests/test_proxy.py +197 -0
  50. ai_cli_toolkit-0.2.0/tests/test_remote.py +230 -0
  51. ai_cli_toolkit-0.2.0/tests/test_remote_package.py +318 -0
  52. ai_cli_toolkit-0.2.0/tests/test_session_gemini_parser.py +19 -0
  53. ai_cli_toolkit-0.2.0/tests/test_session_remote_context.py +56 -0
  54. ai_cli_toolkit-0.2.0/tests/test_tools.py +86 -0
  55. ai_cli_toolkit-0.2.0/tests/test_traffic_log_addon.py +34 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 example-git
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,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-cli-toolkit
3
+ Version: 0.2.0
4
+ Summary: Unified AI CLI wrapper for Claude, Codex, Copilot, and Gemini
5
+ Author-email: example-git <admin@xo.vg>
6
+ Requires-Python: >=3.12
7
+ License-File: LICENSE
8
+ Requires-Dist: mitmproxy>=12.1.2
9
+ Requires-Dist: shtab>=1.7
10
+ Requires-Dist: bcrypt>=4.1
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.11; extra == "dev"
13
+ Requires-Dist: pre-commit>=3.8; extra == "dev"
14
+ Requires-Dist: pytest>=8.3; extra == "dev"
15
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.6.9; extra == "dev"
17
+ Dynamic: license-file
@@ -0,0 +1,318 @@
1
+ <p align="center">
2
+ <img src="docs/assets/ai-cli-banner.png" alt="AI Cli Toolkit banner" width="980" />
3
+ </p>
4
+
5
+ # AI Cli Toolkit
6
+
7
+ AI Cli Toolkit is a unified wrapper around multiple AI coding CLIs:
8
+
9
+ - Claude Code
10
+ - OpenAI Codex CLI
11
+ - GitHub Copilot CLI
12
+ - Gemini CLI
13
+
14
+ It runs each tool through a managed mitmproxy layer to inject instructions consistently, handle per-tool request formats, and keep session tooling in one place.
15
+
16
+ ## Early Version Notice
17
+
18
+ ---
19
+
20
+ This is an early `0.2.0` release. Features are still evolving, behavior may change, and some workflows may be incomplete or unstable.
21
+
22
+ Use this project at your own risk. You are responsible for how you use it, including compliance with platform policies, terms of service, and applicable laws. The maintainers are not liable for misuse, data loss, account issues, service interruptions, or other consequences resulting from use of this tool.
23
+
24
+ ---
25
+
26
+ ## Key Features
27
+
28
+ - Single entrypoint: `ai-cli <tool> [DIR] [args...]`
29
+ - Optional aliasing so `claude`, `codex`, `copilot`, `gemini` route through the wrapper
30
+ - Layered instruction composition:
31
+ - Canary
32
+ - Base
33
+ - Per-tool
34
+ - Per-project
35
+ - User custom
36
+ - Agent-agnostic session inspection:
37
+ - `ai-cli session --list`
38
+ - `ai-cli session --agent claude --tail 20`
39
+ - `ai-cli session --all --grep "keyword"`
40
+ - Startup continuity context:
41
+ - At tool launch, recent cwd-matching context is built from session logs
42
+ - The context block is included in startup prompt text and printed at init
43
+ - Per-tool updater:
44
+ - `ai-cli update --list`
45
+ - `ai-cli update codex`
46
+ - `ai-cli update --all`
47
+ - Remote session support:
48
+ - Launch tools on remote hosts: `ai-cli codex user@host:/path/to/project`
49
+ - Packages and deploys ai-mux, prompt layers, and editor launcher to the remote
50
+ - Syncs edited prompt files back on session exit
51
+ - In-session prompt editor (F5–F8):
52
+ - F5: global instructions (`system_instructions.txt`)
53
+ - F6: base instructions (`base_instructions.txt`)
54
+ - F7: per-tool instructions (`instructions/<tool>.txt`)
55
+ - F8: per-project instructions (`.ai-cli/project_instructions.txt`)
56
+ - Status bar shows shortcut hints; prefix with `C-]` to trigger
57
+ - ai-mux tmux orchestrator:
58
+ - Per-tool tmux sockets (`--socket-name`)
59
+ - Auto-detach stale clients on reconnect (scoped per-session)
60
+ - Cross-platform: arm64 macOS + x86_64 Linux binaries
61
+ ## Requirements
62
+
63
+ - Python `>=3.12`
64
+ - `mitmproxy` (auto-installed on first run if missing)
65
+ - Wrapped tool binaries installed (`claude`, `codex`, `copilot`, `gemini`)
66
+
67
+ ## Install
68
+
69
+ ### Preferred
70
+
71
+ ```bash
72
+ bash install.sh
73
+ ```
74
+
75
+ Useful flags:
76
+
77
+ - `--reinstall`
78
+ - `--alias-all`
79
+ - `--alias <tool>` (repeatable)
80
+ - `--no-alias`
81
+ - `--auto-install-deps` (allow installer to install required system deps like `tmux`)
82
+ - `--yes` (assume yes for interactive prompts)
83
+ - `--non-interactive`
84
+
85
+ ### Manual dev install
86
+
87
+ ```bash
88
+ python3 -m pip install --user -e .
89
+ ```
90
+
91
+ ## CLI Usage
92
+
93
+ ```bash
94
+ ai-cli <tool> [DIR] [args...]
95
+ ai-cli <tool> user@host:/remote/dir [args...] # remote session
96
+ ai-cli menu
97
+ ai-cli status
98
+ ai-cli system [tool]
99
+ ai-cli system prompt [model]
100
+ ai-cli prompt-edit <global|tool> [tool]
101
+ ai-cli history [options]
102
+ ai-cli session [options] # alias for history
103
+ ai-cli traffic [options]
104
+ ai-cli cleanup [options]
105
+ ai-cli update [tool|--all]
106
+ ai-cli completions generate [--shell bash|zsh|all]
107
+ ```
108
+
109
+ Directory launch behavior:
110
+
111
+ - If the first argument after `<tool>` is an existing directory, ai-cli launches the wrapped tool in that directory.
112
+ - If the argument is `user@host:/path`, ai-cli packages and deploys a remote session via SSH/rsync.
113
+ - This applies to both:
114
+ - `ai-cli claude /path/to/project`
115
+ - `claude /path/to/project` (when `claude` is aliased to ai-cli wrapper)
116
+ - `ai-cli codex user@server:/home/user/project`
117
+
118
+ `ai-cli menu` uses curses in an interactive TTY and falls back to a non-interactive status output otherwise.
119
+
120
+ Tools:
121
+
122
+ - `claude`
123
+ - `codex`
124
+ - `copilot`
125
+ - `gemini`
126
+
127
+ Note for Codex users:
128
+
129
+ - In wrapped `ai-cli codex` sessions, `Ctrl+G` external-editor behavior is enabled
130
+ by default.
131
+ - To disable Codex external-editor behavior, set:
132
+ `AI_CLI_CODEX_DISABLE_EXTERNAL_EDITOR=1`
133
+
134
+ ## Configuration
135
+
136
+ Config file:
137
+
138
+ - `~/.ai-cli/config.json`
139
+
140
+ Key fields:
141
+
142
+ - Global `instructions_file` and `canary_rule`
143
+ - Proxy host and CA path
144
+ - Retention policy:
145
+ - `retention.logs_days`
146
+ - `retention.traffic_days`
147
+ - Privacy policy:
148
+ - `privacy.redact_traffic_bodies`
149
+ - Per-tool overrides for:
150
+ - `enabled`
151
+ - `binary`
152
+ - `instructions_file`
153
+ - `canary_rule`
154
+ - `passthrough`
155
+ - `debug_requests`
156
+ - `developer_instructions_mode` (Codex: `overwrite`, `append`, `prepend`; default `overwrite`)
157
+ - Alias state tracking
158
+
159
+ Codex prompt handling (`developer_instructions_mode=overwrite`) builds a sectioned developer message:
160
+ - `<GLOBAL GUIDELINES>` from global user instructions file (`instructions_file`)
161
+ - `<DEVELOPER PROMPT>` from codex-specific instructions
162
+ - recurring runtime blocks (permissions/apps/collaboration mode) preserved in tagged recurring sections
163
+
164
+ In `ai-mux` sessions:
165
+ - `F5` opens the global prompt file in your editor (`VISUAL`/`EDITOR`, fallback `nano`/`vi`/`vim`)
166
+ - `F6` opens the base instructions file
167
+ - `F7` opens the active tool's prompt file
168
+ - `F8` opens the project-level prompt file
169
+ - All F-key bindings require `C-]` prefix (shown in status bar)
170
+ - Codex injections read these files per request, so file edits apply to subsequent turns in the same conversation
171
+
172
+ ## Instruction Files
173
+
174
+ Instruction sources:
175
+
176
+ 1. Canary rule
177
+ 2. Base template (`templates/base_instructions.txt` or `~/.ai-cli/base_instructions.txt`)
178
+ 3. Per-tool (`~/.ai-cli/instructions/<tool>.txt`)
179
+ 4. Project (`./.ai-cli/project_instructions.txt`)
180
+ 5. User (`~/.ai-cli/system_instructions.txt` or configured file)
181
+
182
+ Runtime behavior:
183
+
184
+ - `compose_instructions()` builds the 5-layer text for wrapper logging/hash visibility.
185
+ - Addons inject using the global instructions file + canary rule.
186
+ - For Codex, `~/.ai-cli/instructions/codex.txt` is also used as the `<DEVELOPER PROMPT>` section when `developer_instructions_mode=overwrite`.
187
+ - Startup recent-context is appended to the canary rule unless disabled.
188
+
189
+ Edit quickly:
190
+
191
+ ```bash
192
+ ai-cli system
193
+ ai-cli system codex
194
+ ai-cli prompt-edit global
195
+ ai-cli prompt-edit tool codex
196
+ ```
197
+
198
+ ## Retention And Privacy
199
+
200
+ - Wrapper startup runs best-effort housekeeping:
201
+ - Prunes old wrapper logs from `~/.ai-cli/logs` using `retention.logs_days`
202
+ - Prunes old rows from `~/.ai-cli/traffic.db` using `retention.traffic_days`
203
+ - Traffic body capture is redacted by default (`privacy.redact_traffic_bodies = true`) for common secret/token patterns.
204
+
205
+ ## Session Tooling
206
+
207
+ List all discovered sessions:
208
+
209
+ ```bash
210
+ ai-cli session --list
211
+ ```
212
+
213
+ Show merged timeline:
214
+
215
+ ```bash
216
+ ai-cli session --all --tail 50
217
+ ```
218
+
219
+ Filter by agent:
220
+
221
+ ```bash
222
+ ai-cli session --agent codex --tail 30
223
+ ```
224
+
225
+ Filter by text:
226
+
227
+ ```bash
228
+ ai-cli session --all --grep "statusline"
229
+ ```
230
+
231
+ ## Shell Completions
232
+
233
+ `install.sh` copies completion source files from this repo into shell completion directories:
234
+
235
+ - Zsh source: `completions/_ai-cli` -> `~/.oh-my-zsh/custom/completions/_ai-cli` (or `~/.zsh/completions/_ai-cli`)
236
+ - Bash source: `completions/ai-cli.bash` -> `~/.local/share/bash-completion/completions/ai-cli`
237
+
238
+ You can also generate scripts directly:
239
+
240
+ ```bash
241
+ ai-cli completions generate --shell all
242
+ ```
243
+
244
+ ## Statusline
245
+
246
+ A multi-tool aware statusline command is installed to:
247
+
248
+ - `~/.claude/statusline-command.sh`
249
+
250
+ Installer updates `~/.claude/settings.json` to point `statusLine` at the script.
251
+
252
+ ## Development
253
+
254
+ Basic checks:
255
+
256
+ ```bash
257
+ python3 -m compileall ai_cli
258
+ python3 -m pip install -e '.[dev]'
259
+ pytest
260
+ pre-commit run --all-files
261
+ python3 -m ai_cli --help
262
+ python3 -m ai_cli session --help
263
+ python3 -m ai_cli update --help
264
+ bash -n install.sh
265
+ ```
266
+
267
+ CI is configured in `.github/workflows/ci.yml` and currently runs `pre-commit` and `pytest`.
268
+
269
+ ## Docs
270
+
271
+ Additional user docs live under `docs/`:
272
+
273
+ - `docs/index.md`
274
+ - `docs/getting-started.md`
275
+ - `docs/cli-reference.md`
276
+ - `docs/config-reference.md`
277
+ - `docs/operations-runbook.md`
278
+ - `docs/privacy-data-handling.md`
279
+
280
+ ## Troubleshooting
281
+
282
+ - Proxy launches but tool cannot reach APIs:
283
+ - Confirm CA exists at `~/.mitmproxy/mitmproxy-ca-cert.pem`
284
+ - Check `~/.ai-cli/logs/*.mitmdump.log` for TLS/connection errors
285
+ - Codex injection/traffic capture stopped:
286
+ - Ensure `~/.codex/config.toml` has `[network] allow_upstream_proxy = true` and `mitm = false`
287
+ - No traffic rows appear:
288
+ - Verify tool was launched through `ai-cli`
289
+ - Check retention config is not too aggressive (`retention.traffic_days`)
290
+ - Installer fails on missing `tmux`:
291
+ - Re-run with `--auto-install-deps` (or install `tmux` manually first)
292
+
293
+ ## Project Layout
294
+
295
+ - `ai_cli/` core package
296
+ - `ai_cli/tools/` tool specs and registry
297
+ - `ai_cli/addons/` mitmproxy injection addons
298
+ - `ai_cli/bin/` compiled ai-mux binaries (arm64 macOS, x86_64 Linux)
299
+ - `ai_cli/remote.py` remote session spec and SSH runner
300
+ - `ai_cli/remote_package.py` remote package builder, tmux conf, prompt sync
301
+ - `ai_cli/prompt_editor_launcher.py` F5–F8 prompt editor (deployed to remote)
302
+ - `mux/` Rust source for ai-mux tmux orchestrator
303
+ - `templates/` base instruction template
304
+ - `completions/` shell completion scripts
305
+ - `statusline/` statusline command script
306
+ - `reference/` preserved source/reference material
307
+
308
+ ## README Maintenance
309
+
310
+ This README is intended to be a living operational guide.
311
+
312
+ When behavior changes, update this file in the same change set for:
313
+
314
+ - CLI commands or flags
315
+ - Installer behavior
316
+ - Config schema/defaults
317
+ - Session discovery/parsing behavior
318
+ - Prompt/instruction composition behavior
@@ -0,0 +1,3 @@
1
+ """ai-cli: Unified AI CLI wrapper for Claude, Codex, Copilot, and Gemini."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m ai_cli."""
2
+
3
+ from ai_cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env zsh
2
+ set -euo pipefail
3
+
4
+ die() { print -u2 -- "Error: $*"; exit 1; }
5
+
6
+ usage() {
7
+ cat <<'USAGE'
8
+ remote-tty-wrapper -H user@host [-s session] [--ssh-opt "..."] <mode> [mode args]
9
+
10
+ Global:
11
+ -H, --host user@host required
12
+ -s, --session NAME tmux session name (default: sshwrap)
13
+ --ssh-opt "..." extra ssh option (repeatable)
14
+
15
+ Modes:
16
+ start [--init 'CMD'] ensure tmux session exists; optionally run CMD inside it
17
+ shell [--init 'CMD'] attach to tmux session; optionally run CMD inside it first
18
+ send CMD... send one command line into the tmux session
19
+ send -- 'CMD1' -- 'CMD2' send multiple command lines (separator is literal --)
20
+ close kill the tmux session
21
+
22
+ Examples:
23
+ remote-tty-wrapper -H example@192.168.1.117 start
24
+ remote-tty-wrapper -H example@192.168.1.117 shell
25
+ remote-tty-wrapper -H example@192.168.1.117 shell --init 'source ~/miniconda3/bin/activate; conda activate bot-refactor/conda'
26
+ remote-tty-wrapper -H example@192.168.1.117 send 'cd bot-refactor'
27
+ remote-tty-wrapper -H example@192.168.1.117 send -- 'cd bot-refactor' -- 'pwd' -- 'python -V'
28
+ USAGE
29
+ }
30
+
31
+ REMOTE_USER_HOST="${REMOTE_USER_HOST:-}"
32
+ SESSION="sshwrap"
33
+ MODE=""
34
+ INIT_CMD=""
35
+
36
+ typeset -a SSH_OPTS
37
+ SSH_OPTS=(-o PermitLocalCommand=no -o ServerAliveInterval=30 -o ServerAliveCountMax=3)
38
+
39
+ # ---- parse globals ----
40
+ while (( $# )); do
41
+ case "$1" in
42
+ -H|--host) shift; (( $# )) || die "Missing --host"; REMOTE_USER_HOST="$1"; shift;;
43
+ -s|--session) shift; (( $# )) || die "Missing --session"; SESSION="$1"; shift;;
44
+ --ssh-opt) shift; (( $# )) || die "Missing --ssh-opt"; SSH_OPTS+=("$1"); shift;;
45
+ -h|--help) usage; exit 0;;
46
+ start|shell|send|close) MODE="$1"; shift; break;;
47
+ *) die "Unknown option or missing mode: $1 (use --help)";;
48
+ esac
49
+ done
50
+
51
+ [[ -n "$REMOTE_USER_HOST" ]] || die "No host set (-H user@host)"
52
+ [[ -n "$MODE" ]] || die "No mode (use --help)"
53
+
54
+ # ---- helper: run a shell snippet on remote with argv preserved ----
55
+ ssh_sh() {
56
+ ssh "${SSH_OPTS[@]}" "$REMOTE_USER_HOST" sh -s -- "$@"
57
+ }
58
+
59
+ ensure_tmux_session() {
60
+ ssh_sh "$SESSION" <<'SH'
61
+ sess="$1"
62
+ command -v tmux >/dev/null 2>&1 || { echo "tmux not found on remote" >&2; exit 127; }
63
+ tmux has-session -t "$sess" 2>/dev/null || tmux new-session -d -s "$sess"
64
+ SH
65
+ }
66
+
67
+ tmux_send_line() {
68
+ ssh_sh "$SESSION" "$1" <<'SH'
69
+ sess="$1"
70
+ line="$2"
71
+ tmux has-session -t "$sess" 2>/dev/null || tmux new-session -d -s "$sess"
72
+ tmux send-keys -t "$sess" -l -- "$line"
73
+ tmux send-keys -t "$sess" Enter
74
+ SH
75
+ }
76
+ typeset -a REMAINING_ARGS
77
+ parse_init() {
78
+ INIT_CMD=""
79
+ REMAINING_ARGS=()
80
+ if (( $# >= 2 )) && [[ "$1" == "--init" ]]; then
81
+ INIT_CMD="$2"
82
+ shift 2
83
+ fi
84
+ REMAINING_ARGS=("$@")
85
+ }
86
+
87
+ do_start() {
88
+ parse_init "$@"
89
+ (( ${#REMAINING_ARGS[@]} == 0 )) || die "start takes no extra args (use --init '...')"
90
+
91
+ ensure_tmux_session
92
+ [[ -n "$INIT_CMD" ]] && tmux_send_line "$INIT_CMD"
93
+ }
94
+
95
+ do_shell() {
96
+ parse_init "$@"
97
+ (( ${#REMAINING_ARGS[@]} == 0 )) || die "shell takes no extra args (use --init '...')"
98
+
99
+ [[ -r /dev/tty && -w /dev/tty ]] || die "shell requires a real terminal (/dev/tty unavailable). Use start/send."
100
+
101
+ local remote_cmd
102
+ remote_cmd="tmux has-session -t $(printf %q "$SESSION") 2>/dev/null || tmux new-session -d -s $(printf %q "$SESSION")"
103
+ if [[ -n "$INIT_CMD" ]]; then
104
+ remote_cmd="$remote_cmd; tmux send-keys -t $(printf %q "$SESSION") -l -- $(printf %q "$INIT_CMD"); tmux send-keys -t $(printf %q "$SESSION") Enter"
105
+ fi
106
+ remote_cmd="$remote_cmd; tmux attach -t $(printf %q "$SESSION")"
107
+
108
+ exec </dev/tty >/dev/tty 2>&1 \
109
+ ssh "${SSH_OPTS[@]}" -o RequestTTY=force "$REMOTE_USER_HOST" "$remote_cmd"
110
+ }
111
+
112
+ do_send() {
113
+ ensure_tmux_session
114
+ (( $# > 0 )) || die "send requires a command (or: send -- 'CMD1' -- 'CMD2' ...)"
115
+
116
+ if [[ "$1" == "--" ]]; then
117
+ shift
118
+ local cur=""
119
+ while (( $# )); do
120
+ if [[ "$1" == "--" ]]; then
121
+ [[ -n "$cur" ]] && tmux_send_line "$cur"
122
+ cur=""
123
+ shift
124
+ continue
125
+ fi
126
+ if [[ -z "$cur" ]]; then
127
+ cur="$1"
128
+ else
129
+ cur="$cur $1"
130
+ fi
131
+ shift
132
+ done
133
+ [[ -n "$cur" ]] && tmux_send_line "$cur"
134
+ else
135
+ tmux_send_line "$*"
136
+ fi
137
+ }
138
+
139
+ do_close() {
140
+ (( $# == 0 )) || die "close takes no arguments"
141
+ ssh_sh "$SESSION" <<'SH'
142
+ sess="$1"
143
+ command -v tmux >/dev/null 2>&1 || exit 0
144
+ tmux kill-session -t "$sess" 2>/dev/null || true
145
+ SH
146
+ }
147
+
148
+ case "$MODE" in
149
+ start) do_start "$@";;
150
+ shell) do_shell "$@";;
151
+ send) do_send "$@";;
152
+ close) do_close "$@";;
153
+ esac
@@ -0,0 +1,175 @@
1
+ """CA certificate bootstrap and optional trust-store installation.
2
+
3
+ Handles generating mitmproxy CA certificates on first run, and optionally
4
+ installing them into the macOS or Linux system trust store.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ import shutil
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from ai_cli.log import append_log, fmt_cmd
17
+
18
+ DEFAULT_CA_PATH = "~/.mitmproxy/mitmproxy-ca-cert.pem"
19
+
20
+
21
+ def _stop_process(proc: subprocess.Popen[Any]) -> None:
22
+ """Terminate a subprocess, escalating to kill after timeout."""
23
+ if proc.poll() is not None:
24
+ return
25
+ proc.terminate()
26
+ try:
27
+ proc.wait(timeout=3)
28
+ except subprocess.TimeoutExpired:
29
+ proc.kill()
30
+
31
+
32
+ def bootstrap_ca_cert(
33
+ ca_path: Path,
34
+ mitmdump_bin: str,
35
+ log_path: Path,
36
+ ) -> bool:
37
+ """Ensure mitmproxy CA cert exists at *ca_path*.
38
+
39
+ If missing, runs a short-lived mitmdump process to generate CA material.
40
+ Returns True if cert is available after bootstrap.
41
+ """
42
+ if ca_path.is_file():
43
+ return True
44
+
45
+ confdir = ca_path.parent
46
+ generated_path = confdir / "mitmproxy-ca-cert.pem"
47
+ try:
48
+ confdir.mkdir(parents=True, exist_ok=True)
49
+ except OSError as exc:
50
+ append_log(log_path, f"Failed to create CA directory {confdir}: {exc}")
51
+ return False
52
+
53
+ append_log(log_path, f"CA cert missing at {ca_path}. Bootstrapping with mitmdump.")
54
+
55
+ for _ in range(3):
56
+ port = random.randint(39000, 49000)
57
+ bootstrap_cmd = [
58
+ mitmdump_bin,
59
+ "--quiet",
60
+ "--set",
61
+ f"confdir={confdir}",
62
+ "--listen-host",
63
+ "127.0.0.1",
64
+ "-p",
65
+ str(port),
66
+ ]
67
+ append_log(log_path, f"CA bootstrap command: {fmt_cmd(bootstrap_cmd)}")
68
+
69
+ try:
70
+ bootstrap_proc = subprocess.Popen(
71
+ bootstrap_cmd,
72
+ stdin=subprocess.DEVNULL,
73
+ stdout=subprocess.DEVNULL,
74
+ stderr=subprocess.DEVNULL,
75
+ start_new_session=True,
76
+ )
77
+ except OSError as exc:
78
+ append_log(log_path, f"Failed to start bootstrap mitmdump: {exc}")
79
+ continue
80
+
81
+ time.sleep(0.6)
82
+ _stop_process(bootstrap_proc)
83
+ if generated_path.is_file() or ca_path.is_file():
84
+ break
85
+
86
+ if generated_path.is_file() and generated_path != ca_path:
87
+ try:
88
+ shutil.copy2(generated_path, ca_path)
89
+ except OSError as exc:
90
+ append_log(
91
+ log_path, f"Failed to copy generated CA cert to {ca_path}: {exc}"
92
+ )
93
+
94
+ if ca_path.is_file():
95
+ append_log(log_path, f"CA cert available at {ca_path}.")
96
+ return True
97
+
98
+ append_log(log_path, f"CA bootstrap failed. Expected cert at {ca_path}.")
99
+ return False
100
+
101
+
102
+ def install_ca_macos(ca_path: Path, log_path: Path) -> bool:
103
+ """Install CA cert into the macOS system keychain (requires sudo)."""
104
+ if not ca_path.is_file():
105
+ append_log(log_path, f"CA cert not found at {ca_path}")
106
+ return False
107
+
108
+ cmd = [
109
+ "sudo",
110
+ "security",
111
+ "add-trusted-cert",
112
+ "-d",
113
+ "-r",
114
+ "trustRoot",
115
+ "-k",
116
+ "/Library/Keychains/System.keychain",
117
+ str(ca_path),
118
+ ]
119
+ append_log(log_path, f"Installing CA to macOS keychain: {fmt_cmd(cmd)}")
120
+ try:
121
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True)
122
+ if result.returncode == 0:
123
+ append_log(log_path, "CA cert installed to macOS system keychain.")
124
+ return True
125
+ append_log(
126
+ log_path,
127
+ f"CA install failed (exit={result.returncode}): {result.stderr.strip()}",
128
+ )
129
+ except OSError as exc:
130
+ append_log(log_path, f"CA install failed: {exc}")
131
+ return False
132
+
133
+
134
+ def install_ca_linux(ca_path: Path, log_path: Path) -> bool:
135
+ """Install CA cert into the Linux system trust store."""
136
+ if not ca_path.is_file():
137
+ append_log(log_path, f"CA cert not found at {ca_path}")
138
+ return False
139
+
140
+ # Try Debian/Ubuntu style
141
+ dest_dir = Path("/usr/local/share/ca-certificates")
142
+ update_cmd = "update-ca-certificates"
143
+ if not dest_dir.exists():
144
+ # Try RHEL/Fedora style
145
+ dest_dir = Path("/etc/pki/ca-trust/source/anchors")
146
+ update_cmd = "update-ca-trust"
147
+
148
+ if not dest_dir.exists():
149
+ append_log(log_path, "No known CA trust directory found on this system.")
150
+ return False
151
+
152
+ dest = dest_dir / "mitmproxy-ca-cert.crt"
153
+ try:
154
+ result = subprocess.run(
155
+ ["sudo", "cp", str(ca_path), str(dest)],
156
+ check=False,
157
+ capture_output=True,
158
+ text=True,
159
+ )
160
+ if result.returncode != 0:
161
+ append_log(log_path, f"Failed to copy CA cert: {result.stderr.strip()}")
162
+ return False
163
+ result = subprocess.run(
164
+ ["sudo", update_cmd],
165
+ check=False,
166
+ capture_output=True,
167
+ text=True,
168
+ )
169
+ if result.returncode == 0:
170
+ append_log(log_path, f"CA cert installed via {update_cmd}.")
171
+ return True
172
+ append_log(log_path, f"{update_cmd} failed: {result.stderr.strip()}")
173
+ except OSError as exc:
174
+ append_log(log_path, f"CA install failed: {exc}")
175
+ return False