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.
- ai_cli_toolkit-0.2.0/LICENSE +21 -0
- ai_cli_toolkit-0.2.0/PKG-INFO +17 -0
- ai_cli_toolkit-0.2.0/README.md +318 -0
- ai_cli_toolkit-0.2.0/ai_cli/__init__.py +3 -0
- ai_cli_toolkit-0.2.0/ai_cli/__main__.py +6 -0
- ai_cli_toolkit-0.2.0/ai_cli/bin/ai-mux-linux-x86_64 +0 -0
- ai_cli_toolkit-0.2.0/ai_cli/bin/remote-tty-wrapper +153 -0
- ai_cli_toolkit-0.2.0/ai_cli/ca.py +175 -0
- ai_cli_toolkit-0.2.0/ai_cli/completion_gen.py +680 -0
- ai_cli_toolkit-0.2.0/ai_cli/config.py +185 -0
- ai_cli_toolkit-0.2.0/ai_cli/credentials.py +341 -0
- ai_cli_toolkit-0.2.0/ai_cli/detached_cleanup.py +135 -0
- ai_cli_toolkit-0.2.0/ai_cli/housekeeping.py +50 -0
- ai_cli_toolkit-0.2.0/ai_cli/instructions.py +308 -0
- ai_cli_toolkit-0.2.0/ai_cli/log.py +53 -0
- ai_cli_toolkit-0.2.0/ai_cli/main.py +1516 -0
- ai_cli_toolkit-0.2.0/ai_cli/main_helpers.py +553 -0
- ai_cli_toolkit-0.2.0/ai_cli/prompt_editor_launcher.py +324 -0
- ai_cli_toolkit-0.2.0/ai_cli/proxy.py +627 -0
- ai_cli_toolkit-0.2.0/ai_cli/remote.py +669 -0
- ai_cli_toolkit-0.2.0/ai_cli/remote_package.py +1111 -0
- ai_cli_toolkit-0.2.0/ai_cli/session.py +1344 -0
- ai_cli_toolkit-0.2.0/ai_cli/session_store.py +236 -0
- ai_cli_toolkit-0.2.0/ai_cli/traffic.py +1510 -0
- ai_cli_toolkit-0.2.0/ai_cli/traffic_db.py +118 -0
- ai_cli_toolkit-0.2.0/ai_cli/tui.py +525 -0
- ai_cli_toolkit-0.2.0/ai_cli/update.py +200 -0
- ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/PKG-INFO +17 -0
- ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/SOURCES.txt +53 -0
- ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/dependency_links.txt +1 -0
- ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/entry_points.txt +2 -0
- ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/requires.txt +10 -0
- ai_cli_toolkit-0.2.0/ai_cli_toolkit.egg-info/top_level.txt +1 -0
- ai_cli_toolkit-0.2.0/pyproject.toml +75 -0
- ai_cli_toolkit-0.2.0/setup.cfg +4 -0
- ai_cli_toolkit-0.2.0/setup.py +51 -0
- ai_cli_toolkit-0.2.0/tests/test_cleanup.py +40 -0
- ai_cli_toolkit-0.2.0/tests/test_codex_addon_websocket.py +284 -0
- ai_cli_toolkit-0.2.0/tests/test_codex_instruction_modes.py +108 -0
- ai_cli_toolkit-0.2.0/tests/test_completion_gen.py +47 -0
- ai_cli_toolkit-0.2.0/tests/test_config.py +100 -0
- ai_cli_toolkit-0.2.0/tests/test_gemini_addon.py +27 -0
- ai_cli_toolkit-0.2.0/tests/test_instructions.py +159 -0
- ai_cli_toolkit-0.2.0/tests/test_main_helpers_mux.py +52 -0
- ai_cli_toolkit-0.2.0/tests/test_main_prompt_overrides.py +75 -0
- ai_cli_toolkit-0.2.0/tests/test_main_proxy_failure.py +70 -0
- ai_cli_toolkit-0.2.0/tests/test_main_remote_sync.py +250 -0
- ai_cli_toolkit-0.2.0/tests/test_prompt_editor_launcher.py +143 -0
- ai_cli_toolkit-0.2.0/tests/test_proxy.py +197 -0
- ai_cli_toolkit-0.2.0/tests/test_remote.py +230 -0
- ai_cli_toolkit-0.2.0/tests/test_remote_package.py +318 -0
- ai_cli_toolkit-0.2.0/tests/test_session_gemini_parser.py +19 -0
- ai_cli_toolkit-0.2.0/tests/test_session_remote_context.py +56 -0
- ai_cli_toolkit-0.2.0/tests/test_tools.py +86 -0
- 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
|
|
Binary file
|
|
@@ -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
|