ai-cli-toolkit 0.2.0__py3-none-any.whl
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/__init__.py +3 -0
- ai_cli/__main__.py +6 -0
- ai_cli/bin/ai-mux-linux-x86_64 +0 -0
- ai_cli/bin/remote-tty-wrapper +153 -0
- ai_cli/ca.py +175 -0
- ai_cli/completion_gen.py +680 -0
- ai_cli/config.py +185 -0
- ai_cli/credentials.py +341 -0
- ai_cli/detached_cleanup.py +135 -0
- ai_cli/housekeeping.py +50 -0
- ai_cli/instructions.py +308 -0
- ai_cli/log.py +53 -0
- ai_cli/main.py +1516 -0
- ai_cli/main_helpers.py +553 -0
- ai_cli/prompt_editor_launcher.py +324 -0
- ai_cli/proxy.py +627 -0
- ai_cli/remote.py +669 -0
- ai_cli/remote_package.py +1111 -0
- ai_cli/session.py +1344 -0
- ai_cli/session_store.py +236 -0
- ai_cli/traffic.py +1510 -0
- ai_cli/traffic_db.py +118 -0
- ai_cli/tui.py +525 -0
- ai_cli/update.py +200 -0
- ai_cli_toolkit-0.2.0.dist-info/METADATA +17 -0
- ai_cli_toolkit-0.2.0.dist-info/RECORD +30 -0
- ai_cli_toolkit-0.2.0.dist-info/WHEEL +5 -0
- ai_cli_toolkit-0.2.0.dist-info/entry_points.txt +2 -0
- ai_cli_toolkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- ai_cli_toolkit-0.2.0.dist-info/top_level.txt +1 -0
ai_cli/__init__.py
ADDED
ai_cli/__main__.py
ADDED
|
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
|
ai_cli/ca.py
ADDED
|
@@ -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
|