roundtable-sh 0.1.1__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.
- roundtable_cli/__init__.py +52 -0
- roundtable_cli/__main__.py +6 -0
- roundtable_cli/_engine/bin/roundtable +115 -0
- roundtable_cli/_engine/lib/config.sh +494 -0
- roundtable_cli/_engine/lib/core.selftest.sh +85 -0
- roundtable_cli/_engine/lib/core.sh +359 -0
- roundtable_cli/_engine/lib/install.sh +256 -0
- roundtable_cli/_engine/lib/mcp.py +350 -0
- roundtable_cli/_engine/skill/SKILL.md +119 -0
- roundtable_sh-0.1.1.dist-info/METADATA +251 -0
- roundtable_sh-0.1.1.dist-info/RECORD +14 -0
- roundtable_sh-0.1.1.dist-info/WHEEL +4 -0
- roundtable_sh-0.1.1.dist-info/entry_points.txt +2 -0
- roundtable_sh-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Thin console-script launcher for Roundtable.
|
|
2
|
+
|
|
3
|
+
`uv tool install roundtable-sh` / `pipx install roundtable-sh` expose a `roundtable` command that simply
|
|
4
|
+
execs the bundled bash engine (bin/roundtable), which sources lib/*. The engine
|
|
5
|
+
and its libs are bundled as package data under roundtable_cli/_engine/ (see
|
|
6
|
+
pyproject.toml [tool.hatch.build.targets.wheel.force-include]); the bin/ and
|
|
7
|
+
lib/ sibling layout is preserved so the dispatcher's `../lib` resolution works.
|
|
8
|
+
|
|
9
|
+
Keep this tiny: all real behavior lives in the bash engine.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.1"
|
|
18
|
+
|
|
19
|
+
_ENGINE = Path(__file__).resolve().parent / "_engine" / "bin" / "roundtable"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def engine_path() -> Path:
|
|
23
|
+
"""Absolute path to the bundled dispatcher."""
|
|
24
|
+
return _ENGINE
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> "int":
|
|
28
|
+
engine = engine_path()
|
|
29
|
+
if not engine.exists():
|
|
30
|
+
sys.stderr.write(
|
|
31
|
+
"roundtable: bundled engine not found at %s\n"
|
|
32
|
+
"The package may be damaged. Reinstall it:\n"
|
|
33
|
+
" uv tool install --reinstall roundtable-sh\n"
|
|
34
|
+
% engine
|
|
35
|
+
)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
# Exec via bash for portability: wheels do not reliably preserve the
|
|
39
|
+
# executable bit on data files, so we never rely on it. This replaces the
|
|
40
|
+
# current process, so the engine owns stdin/stdout/stderr and the exit code.
|
|
41
|
+
bash = shutil.which("bash") or "/bin/bash"
|
|
42
|
+
argv = [bash, str(engine), *sys.argv[1:]]
|
|
43
|
+
try:
|
|
44
|
+
os.execv(bash, argv)
|
|
45
|
+
except OSError as exc: # pragma: no cover - exec almost never returns
|
|
46
|
+
sys.stderr.write("roundtable: failed to launch engine: %s\n" % exc)
|
|
47
|
+
return 1
|
|
48
|
+
return 0 # unreachable on success
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# roundtable — a council of frontier models (a Frontier Infra project)
|
|
3
|
+
#
|
|
4
|
+
# Portable subcommand dispatcher. Targets macOS bash 3.2 + Linux.
|
|
5
|
+
# Resolves its own real directory without relying on GNU `readlink -f`.
|
|
6
|
+
#
|
|
7
|
+
# roundtable "question" ask the council (default)
|
|
8
|
+
# roundtable ask "question" ... same, explicit
|
|
9
|
+
# roundtable auth|models|heads|doctor|config|version|update
|
|
10
|
+
# roundtable mcp ... MCP server (lib/mcp.py)
|
|
11
|
+
# roundtable install ... harness auto-wire (lib/install.sh)
|
|
12
|
+
|
|
13
|
+
# ── resolve the real script dir (follow symlinks, portable) ────────────────
|
|
14
|
+
SOURCE="${BASH_SOURCE[0]:-$0}"
|
|
15
|
+
while [ -h "$SOURCE" ]; do
|
|
16
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)"
|
|
17
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
18
|
+
case "$SOURCE" in /*) ;; *) SOURCE="$DIR/$SOURCE" ;; esac
|
|
19
|
+
done
|
|
20
|
+
BINDIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)"
|
|
21
|
+
LIBDIR="$BINDIR/../lib"
|
|
22
|
+
[ -d "$LIBDIR" ] && LIBDIR="$(cd -P "$LIBDIR" >/dev/null 2>&1 && pwd)"
|
|
23
|
+
|
|
24
|
+
RT_CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/roundtable/config.env"
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
cat <<EOF
|
|
28
|
+
Roundtable — a council of frontier models (a Frontier Infra project)
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
roundtable "question" Ask the council (advisory, all heads)
|
|
32
|
+
roundtable ask "question" [flags] Same, explicit
|
|
33
|
+
roundtable <command> [args]
|
|
34
|
+
|
|
35
|
+
Commands:
|
|
36
|
+
ask Run the council (default when no command matches)
|
|
37
|
+
auth Add / view provider API keys (masked paste)
|
|
38
|
+
models Set the model id used by each head
|
|
39
|
+
heads Choose which heads are enabled by default
|
|
40
|
+
doctor Show per-head config + reachability
|
|
41
|
+
config Show (masked) or edit the config file ('config edit')
|
|
42
|
+
mcp Run the MCP server (e.g. 'roundtable mcp serve')
|
|
43
|
+
install Auto-wire Roundtable into detected harnesses
|
|
44
|
+
version Print the version
|
|
45
|
+
update Re-run the installer to upgrade
|
|
46
|
+
help Show this help
|
|
47
|
+
|
|
48
|
+
Engine flags (passed through to 'ask'):
|
|
49
|
+
-q, --question TEXT The question (also accepted as a positional)
|
|
50
|
+
-c, --context FILE Prepend a shared context file
|
|
51
|
+
--heads CSV Subset: claude,openai,grok,glm,minimax,gemini,codex
|
|
52
|
+
--rounds N N>=2 -> deliberation to consensus (default 1 = advisory)
|
|
53
|
+
--timeout S Per-head timeout, seconds
|
|
54
|
+
--research Allow web/tool use where heads support it
|
|
55
|
+
--out FILE Also save the transcript to FILE
|
|
56
|
+
|
|
57
|
+
Config: $RT_CONFIG_FILE
|
|
58
|
+
Docs: https://roundtable.sh
|
|
59
|
+
EOF
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Value of KEY from config.env only (last match wins).
|
|
63
|
+
rt_cfg_value() {
|
|
64
|
+
local name="$1"
|
|
65
|
+
[ -f "$RT_CONFIG_FILE" ] || return 0
|
|
66
|
+
grep -E "^[[:space:]]*${name}=" "$RT_CONFIG_FILE" 2>/dev/null | tail -n1 | sed -E "s/^[[:space:]]*${name}=//"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Hand off to the engine, injecting default --heads when configured and the
|
|
70
|
+
# user didn't pass their own --heads.
|
|
71
|
+
exec_engine() {
|
|
72
|
+
if [ ! -f "$LIBDIR/core.sh" ]; then
|
|
73
|
+
echo "roundtable: engine not found ($LIBDIR/core.sh). Reinstall: curl -fsSL https://roundtable.sh/install.sh | bash" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
local has_heads=0 a
|
|
77
|
+
for a in "$@"; do [ "$a" = "--heads" ] && has_heads=1; done
|
|
78
|
+
local heads_default="${ROUNDTABLE_HEADS:-}"
|
|
79
|
+
[ -z "$heads_default" ] && heads_default="$(rt_cfg_value ROUNDTABLE_HEADS)"
|
|
80
|
+
if [ "$has_heads" -eq 0 ] && [ -n "$heads_default" ]; then
|
|
81
|
+
exec bash "$LIBDIR/core.sh" --heads "$heads_default" "$@"
|
|
82
|
+
fi
|
|
83
|
+
exec bash "$LIBDIR/core.sh" "$@"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cmd="${1:-}"
|
|
87
|
+
case "$cmd" in
|
|
88
|
+
""|-h|--help|help)
|
|
89
|
+
usage; exit 0 ;;
|
|
90
|
+
|
|
91
|
+
-V|--version)
|
|
92
|
+
exec bash "$LIBDIR/config.sh" version ;;
|
|
93
|
+
|
|
94
|
+
auth|models|heads|doctor|config|version|update)
|
|
95
|
+
if [ ! -f "$LIBDIR/config.sh" ]; then
|
|
96
|
+
echo "roundtable: $LIBDIR/config.sh is missing" >&2; exit 1
|
|
97
|
+
fi
|
|
98
|
+
exec bash "$LIBDIR/config.sh" "$@" ;;
|
|
99
|
+
|
|
100
|
+
mcp)
|
|
101
|
+
shift
|
|
102
|
+
if [ -f "$LIBDIR/mcp.py" ]; then exec python3 "$LIBDIR/mcp.py" "$@"; fi
|
|
103
|
+
echo "roundtable: MCP server not available ($LIBDIR/mcp.py missing)" >&2; exit 1 ;;
|
|
104
|
+
|
|
105
|
+
install)
|
|
106
|
+
shift
|
|
107
|
+
if [ -f "$LIBDIR/install.sh" ]; then exec bash "$LIBDIR/install.sh" "$@"; fi
|
|
108
|
+
echo "roundtable: installer not available ($LIBDIR/install.sh missing)" >&2; exit 1 ;;
|
|
109
|
+
|
|
110
|
+
ask)
|
|
111
|
+
shift; exec_engine "$@" ;;
|
|
112
|
+
|
|
113
|
+
*)
|
|
114
|
+
exec_engine "$@" ;;
|
|
115
|
+
esac
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# roundtable/lib/config.sh — auth / models / heads / doctor / config / version / update
|
|
3
|
+
#
|
|
4
|
+
# Sourced or executed by bin/roundtable. Portable: macOS bash 3.2 + Linux.
|
|
5
|
+
# No associative arrays, no config parser/framework, no new deps.
|
|
6
|
+
#
|
|
7
|
+
# Config home: ${XDG_CONFIG_HOME:-$HOME/.config}/roundtable/config.env (dotenv, chmod 600)
|
|
8
|
+
# The engine (lib/core.sh) sources the same file plus reads ROUNDTABLE_* env overrides.
|
|
9
|
+
#
|
|
10
|
+
# Keyed heads + env keys:
|
|
11
|
+
# claude=ANTHROPIC_API_KEY openai=OPENAI_API_KEY grok=XAI_API_KEY
|
|
12
|
+
# glm=ZAI_API_KEY minimax=MINIMAX_API_KEY gemini=GEMINI_API_KEY (or GOOGLE_API_KEY)
|
|
13
|
+
# codex=local `codex` CLI login (no key stored)
|
|
14
|
+
|
|
15
|
+
# ── self-location (resolve symlinks without GNU `readlink -f`) ──────────────
|
|
16
|
+
_rt_cfg_src="${BASH_SOURCE[0]:-$0}"
|
|
17
|
+
while [ -h "$_rt_cfg_src" ]; do
|
|
18
|
+
_rt_cfg_dir="$(cd -P "$(dirname "$_rt_cfg_src")" >/dev/null 2>&1 && pwd)"
|
|
19
|
+
_rt_cfg_src="$(readlink "$_rt_cfg_src")"
|
|
20
|
+
case "$_rt_cfg_src" in /*) ;; *) _rt_cfg_src="$_rt_cfg_dir/$_rt_cfg_src" ;; esac
|
|
21
|
+
done
|
|
22
|
+
RT_LIBDIR="$(cd -P "$(dirname "$_rt_cfg_src")" >/dev/null 2>&1 && pwd)"
|
|
23
|
+
RT_REPODIR="$(cd -P "$RT_LIBDIR/.." >/dev/null 2>&1 && pwd)"
|
|
24
|
+
|
|
25
|
+
RT_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/roundtable"
|
|
26
|
+
RT_CONFIG_FILE="$RT_CONFIG_DIR/config.env"
|
|
27
|
+
RT_INSTALL_URL="https://roundtable.sh/install.sh"
|
|
28
|
+
|
|
29
|
+
# All heads (display/iteration order) and the subset that take an API key.
|
|
30
|
+
ALL_HEADS="claude openai grok glm minimax gemini codex"
|
|
31
|
+
KEYED_HEADS="claude openai grok glm minimax gemini"
|
|
32
|
+
|
|
33
|
+
# ── env-key name for a keyed head ──────────────────────────────────────────
|
|
34
|
+
key_for_head() {
|
|
35
|
+
case "$1" in
|
|
36
|
+
claude) echo "ANTHROPIC_API_KEY" ;;
|
|
37
|
+
openai) echo "OPENAI_API_KEY" ;;
|
|
38
|
+
grok) echo "XAI_API_KEY" ;;
|
|
39
|
+
glm) echo "ZAI_API_KEY" ;;
|
|
40
|
+
minimax) echo "MINIMAX_API_KEY" ;;
|
|
41
|
+
gemini) echo "GEMINI_API_KEY" ;;
|
|
42
|
+
*) echo "" ;;
|
|
43
|
+
esac
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
47
|
+
# Embedded python3 helpers (passed via `python3 -c` so stdin stays the caller's
|
|
48
|
+
# real stdin — required so a piped key is read, not the heredoc). No single
|
|
49
|
+
# quotes inside, so the whole program can live in a single-quoted bash string.
|
|
50
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
RT_SECRET_PY='
|
|
52
|
+
import sys
|
|
53
|
+
def from_stdin():
|
|
54
|
+
return sys.stdin.readline().rstrip("\r\n")
|
|
55
|
+
def masked(prompt):
|
|
56
|
+
try:
|
|
57
|
+
ti = open("/dev/tty", "r"); to = open("/dev/tty", "w")
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
try:
|
|
61
|
+
import termios, tty
|
|
62
|
+
fd = ti.fileno()
|
|
63
|
+
old = termios.tcgetattr(fd)
|
|
64
|
+
buf = []
|
|
65
|
+
to.write(prompt); to.flush()
|
|
66
|
+
try:
|
|
67
|
+
tty.setraw(fd)
|
|
68
|
+
while True:
|
|
69
|
+
ch = ti.read(1)
|
|
70
|
+
if ch == "" or ch in ("\r", "\n"):
|
|
71
|
+
break
|
|
72
|
+
if ch == "\x03":
|
|
73
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
74
|
+
to.write("\n"); to.flush()
|
|
75
|
+
sys.exit(130)
|
|
76
|
+
if ch in ("\x7f", "\b"):
|
|
77
|
+
if buf:
|
|
78
|
+
buf.pop(); to.write("\b \b"); to.flush()
|
|
79
|
+
continue
|
|
80
|
+
if ch == "\x1b":
|
|
81
|
+
continue
|
|
82
|
+
buf.append(ch); to.write("*"); to.flush()
|
|
83
|
+
finally:
|
|
84
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
85
|
+
to.write("\n"); to.flush()
|
|
86
|
+
return "".join(buf)
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
prompt = sys.argv[1] if len(sys.argv) > 1 else "Paste key: "
|
|
90
|
+
if not sys.stdin.isatty():
|
|
91
|
+
sys.stdout.write(from_stdin())
|
|
92
|
+
else:
|
|
93
|
+
v = masked(prompt)
|
|
94
|
+
if v is None:
|
|
95
|
+
import getpass
|
|
96
|
+
try:
|
|
97
|
+
v = getpass.getpass(prompt)
|
|
98
|
+
except Exception:
|
|
99
|
+
v = from_stdin()
|
|
100
|
+
sys.stdout.write(v)
|
|
101
|
+
'
|
|
102
|
+
|
|
103
|
+
RT_UPSERT_PY='
|
|
104
|
+
import sys, os
|
|
105
|
+
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
106
|
+
lines, found = [], False
|
|
107
|
+
if os.path.exists(path):
|
|
108
|
+
with open(path) as f:
|
|
109
|
+
for line in f:
|
|
110
|
+
s = line.lstrip()
|
|
111
|
+
head = s.split("=", 1)[0].strip() if "=" in s else None
|
|
112
|
+
if head == key:
|
|
113
|
+
if not found:
|
|
114
|
+
lines.append(key + "=" + val + "\n"); found = True
|
|
115
|
+
continue
|
|
116
|
+
if not line.endswith("\n"):
|
|
117
|
+
line += "\n"
|
|
118
|
+
lines.append(line)
|
|
119
|
+
if not found:
|
|
120
|
+
lines.append(key + "=" + val + "\n")
|
|
121
|
+
with open(path, "w") as f:
|
|
122
|
+
f.writelines(lines)
|
|
123
|
+
try:
|
|
124
|
+
os.chmod(path, 0o600)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
'
|
|
128
|
+
|
|
129
|
+
# ── config.env primitives ──────────────────────────────────────────────────
|
|
130
|
+
rt_config_ensure_dir() {
|
|
131
|
+
[ -d "$RT_CONFIG_DIR" ] || mkdir -p "$RT_CONFIG_DIR" 2>/dev/null
|
|
132
|
+
chmod 700 "$RT_CONFIG_DIR" 2>/dev/null || true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Value of KEY from config.env only (last match wins). Empty if absent.
|
|
136
|
+
rt_cfg_value() {
|
|
137
|
+
local name="$1"
|
|
138
|
+
[ -f "$RT_CONFIG_FILE" ] || return 0
|
|
139
|
+
grep -E "^[[:space:]]*${name}=" "$RT_CONFIG_FILE" 2>/dev/null | tail -n1 | sed -E "s/^[[:space:]]*${name}=//"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Value from the live environment if set, else from config.env.
|
|
143
|
+
rt_env_or_config() {
|
|
144
|
+
local name="$1" v=""
|
|
145
|
+
eval "v=\${$name:-}"
|
|
146
|
+
if [ -n "$v" ]; then printf '%s' "$v"; return 0; fi
|
|
147
|
+
rt_cfg_value "$name"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Idempotent KEY=VALUE upsert; keeps file chmod 600.
|
|
151
|
+
rt_upsert() {
|
|
152
|
+
local key="$1" val="$2"
|
|
153
|
+
rt_config_ensure_dir
|
|
154
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
155
|
+
python3 -c "$RT_UPSERT_PY" "$RT_CONFIG_FILE" "$key" "$val"
|
|
156
|
+
else
|
|
157
|
+
local tmp; tmp="$(mktemp)"
|
|
158
|
+
[ -f "$RT_CONFIG_FILE" ] && grep -v -E "^[[:space:]]*${key}=" "$RT_CONFIG_FILE" > "$tmp" 2>/dev/null
|
|
159
|
+
printf '%s=%s\n' "$key" "$val" >> "$tmp"
|
|
160
|
+
mv "$tmp" "$RT_CONFIG_FILE"
|
|
161
|
+
fi
|
|
162
|
+
chmod 600 "$RT_CONFIG_FILE" 2>/dev/null || true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
rt_remove_key() {
|
|
166
|
+
local key="$1" tmp
|
|
167
|
+
[ -f "$RT_CONFIG_FILE" ] || return 0
|
|
168
|
+
tmp="$(mktemp)"
|
|
169
|
+
grep -v -E "^[[:space:]]*${key}=" "$RT_CONFIG_FILE" > "$tmp" 2>/dev/null || true
|
|
170
|
+
mv "$tmp" "$RT_CONFIG_FILE"
|
|
171
|
+
chmod 600 "$RT_CONFIG_FILE" 2>/dev/null || true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Mask a secret value for display: keep at most first 2 + last 2 chars.
|
|
175
|
+
mask_value() {
|
|
176
|
+
local v="$1"
|
|
177
|
+
local n=${#v}
|
|
178
|
+
if [ "$n" -le 6 ]; then printf '%s' '******'
|
|
179
|
+
else printf '%s********%s' "${v:0:2}" "${v:$((n-2))}"; fi
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
rt_read_secret() {
|
|
183
|
+
local prompt="$1" v
|
|
184
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
185
|
+
python3 -c "$RT_SECRET_PY" "$prompt"
|
|
186
|
+
return $?
|
|
187
|
+
fi
|
|
188
|
+
printf '%s' "$prompt" >&2
|
|
189
|
+
read -rs v 2>/dev/null
|
|
190
|
+
printf '\n' >&2
|
|
191
|
+
printf '%s' "$v"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# ── presence checks ────────────────────────────────────────────────────────
|
|
195
|
+
head_key_present() { # keyed heads only
|
|
196
|
+
local h="$1" v
|
|
197
|
+
v="$(rt_env_or_config "$(key_for_head "$h")")"
|
|
198
|
+
if [ -z "$v" ] && [ "$h" = gemini ]; then v="$(rt_env_or_config GOOGLE_API_KEY)"; fi
|
|
199
|
+
[ -n "$v" ]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
rt_codex_logged_in() {
|
|
203
|
+
command -v codex >/dev/null 2>&1 || return 1
|
|
204
|
+
local h="${CODEX_HOME:-$HOME/.codex}"
|
|
205
|
+
[ -f "$h/auth.json" ] && return 0
|
|
206
|
+
return 1
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
210
|
+
# auth
|
|
211
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
212
|
+
rt_print_provider_status() {
|
|
213
|
+
local h mark src
|
|
214
|
+
echo "Providers (✓ = configured, ✗ = missing):"
|
|
215
|
+
echo
|
|
216
|
+
for h in $KEYED_HEADS; do
|
|
217
|
+
src="$(key_for_head "$h")"
|
|
218
|
+
if head_key_present "$h"; then mark="✓"; else mark="✗"; fi
|
|
219
|
+
printf ' %s %-8s %s\n' "$mark" "$h" "$src"
|
|
220
|
+
done
|
|
221
|
+
if rt_codex_logged_in; then
|
|
222
|
+
printf ' %s %-8s %s\n' "✓" "codex" "local codex CLI (logged in)"
|
|
223
|
+
elif command -v codex >/dev/null 2>&1; then
|
|
224
|
+
printf ' %s %-8s %s\n' "✗" "codex" "local codex CLI found — run 'codex login'"
|
|
225
|
+
else
|
|
226
|
+
printf ' %s %-8s %s\n' "✗" "codex" "local codex CLI not installed"
|
|
227
|
+
fi
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
rt_cmd_auth() {
|
|
231
|
+
local provider="${1:-}"
|
|
232
|
+
rt_print_provider_status
|
|
233
|
+
if [ -z "$provider" ]; then
|
|
234
|
+
echo
|
|
235
|
+
printf 'Which provider to configure? [%s]: ' "$(echo "$KEYED_HEADS" | tr ' ' '/')"
|
|
236
|
+
read -r provider 2>/dev/null || provider=""
|
|
237
|
+
fi
|
|
238
|
+
provider="$(printf '%s' "$provider" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
|
239
|
+
[ -z "$provider" ] && { echo "No provider selected — nothing changed." >&2; return 1; }
|
|
240
|
+
|
|
241
|
+
if [ "$provider" = codex ]; then
|
|
242
|
+
if rt_codex_logged_in; then
|
|
243
|
+
echo "codex is logged in via the local codex CLI — nothing to store here."
|
|
244
|
+
elif command -v codex >/dev/null 2>&1; then
|
|
245
|
+
echo "codex CLI is installed but not logged in. Run: codex login"
|
|
246
|
+
else
|
|
247
|
+
echo "codex CLI not installed. Install it and run 'codex login' to enable the codex head." >&2
|
|
248
|
+
fi
|
|
249
|
+
return 0
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
local keyname; keyname="$(key_for_head "$provider")"
|
|
253
|
+
if [ -z "$keyname" ]; then
|
|
254
|
+
echo "Unknown provider: $provider (expected one of: $KEYED_HEADS codex)" >&2
|
|
255
|
+
return 1
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
rt_config_ensure_dir
|
|
259
|
+
local secret; secret="$(rt_read_secret "Paste $provider key ($keyname): ")"
|
|
260
|
+
secret="$(printf '%s' "$secret" | tr -d '\r\n')"
|
|
261
|
+
[ -z "$secret" ] && { echo "No key entered — nothing changed." >&2; return 1; }
|
|
262
|
+
rt_upsert "$keyname" "$secret"
|
|
263
|
+
echo "✓ Saved $keyname → $RT_CONFIG_FILE (chmod 600)."
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
267
|
+
# models
|
|
268
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
269
|
+
rt_cmd_models() {
|
|
270
|
+
rt_config_ensure_dir
|
|
271
|
+
echo "Per-head model id (blank = keep, '-' = reset to engine default):"
|
|
272
|
+
echo
|
|
273
|
+
local h H var cur new
|
|
274
|
+
for h in $ALL_HEADS; do
|
|
275
|
+
[ "$h" = codex ] && continue # codex uses the local codex CLI's own default model
|
|
276
|
+
H="$(printf '%s' "$h" | tr '[:lower:]' '[:upper:]')"
|
|
277
|
+
var="ROUNDTABLE_${H}_MODEL"
|
|
278
|
+
cur="$(rt_env_or_config "$var")"
|
|
279
|
+
[ -z "$cur" ] && cur="(engine default)"
|
|
280
|
+
printf ' %-8s [%s]: ' "$h" "$cur"
|
|
281
|
+
read -r new 2>/dev/null || new=""
|
|
282
|
+
[ -z "$new" ] && continue
|
|
283
|
+
if [ "$new" = "-" ]; then
|
|
284
|
+
rt_remove_key "$var"; echo " → $var reset to engine default"
|
|
285
|
+
else
|
|
286
|
+
rt_upsert "$var" "$new"; echo " → $var=$new"
|
|
287
|
+
fi
|
|
288
|
+
done
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
292
|
+
# heads (default-enabled set)
|
|
293
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
294
|
+
rt_cmd_heads() {
|
|
295
|
+
rt_config_ensure_dir
|
|
296
|
+
local cur; cur="$(rt_env_or_config ROUNDTABLE_HEADS)"
|
|
297
|
+
[ -z "$cur" ] && cur="(engine default: all)"
|
|
298
|
+
echo "Available heads: $ALL_HEADS"
|
|
299
|
+
echo "Current default: $cur"
|
|
300
|
+
echo
|
|
301
|
+
printf 'Comma-separated heads to enable by default (blank = keep, "-" = clear): '
|
|
302
|
+
local new; read -r new 2>/dev/null || new=""
|
|
303
|
+
[ -z "$new" ] && { echo "Unchanged."; return 0; }
|
|
304
|
+
if [ "$new" = "-" ]; then rt_remove_key ROUNDTABLE_HEADS; echo "Cleared default heads."; return 0; fi
|
|
305
|
+
new="$(printf '%s' "$new" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
|
|
306
|
+
local h k ok bad="" list
|
|
307
|
+
list="$(printf '%s' "$new" | tr ',' ' ')"
|
|
308
|
+
for h in $list; do
|
|
309
|
+
ok=0
|
|
310
|
+
for k in $ALL_HEADS; do [ "$h" = "$k" ] && ok=1; done
|
|
311
|
+
[ "$ok" -eq 0 ] && bad="$bad $h"
|
|
312
|
+
done
|
|
313
|
+
[ -n "$bad" ] && echo "WARN: unknown head(s):$bad (kept; the engine skips unknowns)" >&2
|
|
314
|
+
rt_upsert ROUNDTABLE_HEADS "$new"
|
|
315
|
+
echo "Set ROUNDTABLE_HEADS=$new"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
319
|
+
# doctor — per-head configured? + fast (<=5s) reachability. Read-only.
|
|
320
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
321
|
+
rt_classify_http() {
|
|
322
|
+
case "$1" in
|
|
323
|
+
2*) printf 'ok (%s)' "$1" ;;
|
|
324
|
+
401|403) printf 'key rejected (%s)' "$1" ;;
|
|
325
|
+
000|"") printf 'unreachable' ;;
|
|
326
|
+
*) printf 'reachable (%s)' "$1" ;;
|
|
327
|
+
esac
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
head_ping() {
|
|
331
|
+
local h="$1" key="$2" code
|
|
332
|
+
command -v curl >/dev/null 2>&1 || { printf 'curl missing'; return; }
|
|
333
|
+
case "$h" in
|
|
334
|
+
claude)
|
|
335
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
|
336
|
+
-H "x-api-key: $key" -H "anthropic-version: 2023-06-01" \
|
|
337
|
+
https://api.anthropic.com/v1/models 2>/dev/null)" ;;
|
|
338
|
+
openai)
|
|
339
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
|
340
|
+
-H "Authorization: Bearer $key" https://api.openai.com/v1/models 2>/dev/null)" ;;
|
|
341
|
+
grok)
|
|
342
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
|
343
|
+
-H "Authorization: Bearer $key" https://api.x.ai/v1/models 2>/dev/null)" ;;
|
|
344
|
+
glm)
|
|
345
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
|
346
|
+
-H "Authorization: Bearer $key" https://api.z.ai/api/paas/v4/models 2>/dev/null)" ;;
|
|
347
|
+
minimax)
|
|
348
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
|
349
|
+
-H "Authorization: Bearer $key" https://api.minimax.io/v1/models 2>/dev/null)" ;;
|
|
350
|
+
gemini)
|
|
351
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
|
|
352
|
+
"https://generativelanguage.googleapis.com/v1beta/models?key=$key" 2>/dev/null)" ;;
|
|
353
|
+
*) printf 'n/a'; return ;;
|
|
354
|
+
esac
|
|
355
|
+
rt_classify_http "$code"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
rt_doctor_row() {
|
|
359
|
+
local h="$1" src conf reach keyval
|
|
360
|
+
if [ "$h" = codex ]; then
|
|
361
|
+
src="codex CLI login"
|
|
362
|
+
if rt_codex_logged_in; then conf="✓"; reach="logged in"
|
|
363
|
+
elif command -v codex >/dev/null 2>&1; then conf="✗"; reach="CLI found, not logged in"
|
|
364
|
+
else conf="✗"; reach="CLI not found"; fi
|
|
365
|
+
printf ' %-9s %-22s %-11s %s\n' "$h" "$src" "$conf" "$reach"
|
|
366
|
+
return
|
|
367
|
+
fi
|
|
368
|
+
src="$(key_for_head "$h")"
|
|
369
|
+
keyval="$(rt_env_or_config "$src")"
|
|
370
|
+
if [ -z "$keyval" ] && [ "$h" = gemini ]; then
|
|
371
|
+
keyval="$(rt_env_or_config GOOGLE_API_KEY)"; [ -n "$keyval" ] && src="GOOGLE_API_KEY"
|
|
372
|
+
fi
|
|
373
|
+
if [ -n "$keyval" ]; then
|
|
374
|
+
conf="✓"; reach="$(head_ping "$h" "$keyval")"
|
|
375
|
+
else
|
|
376
|
+
conf="✗"
|
|
377
|
+
if [ "$h" = grok ] && command -v grok >/dev/null 2>&1; then reach="grok CLI present (no key)"
|
|
378
|
+
else reach="— (no key)"; fi
|
|
379
|
+
fi
|
|
380
|
+
printf ' %-9s %-22s %-11s %s\n' "$h" "$src" "$conf" "$reach"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
rt_cmd_doctor() {
|
|
384
|
+
echo "Roundtable doctor"
|
|
385
|
+
echo "Config: $RT_CONFIG_FILE"
|
|
386
|
+
echo
|
|
387
|
+
printf ' %-9s %-22s %-11s %s\n' "HEAD" "AUTH SOURCE" "CONFIGURED" "REACHABILITY"
|
|
388
|
+
printf ' %-9s %-22s %-11s %s\n' "----" "-----------" "----------" "------------"
|
|
389
|
+
local h
|
|
390
|
+
for h in $ALL_HEADS; do rt_doctor_row "$h"; done
|
|
391
|
+
echo
|
|
392
|
+
echo "(reachability is a ≤5s ping; read-only, nothing is billed)"
|
|
393
|
+
return 0
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
397
|
+
# config (show masked / edit)
|
|
398
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
rt_print_config_line() {
|
|
400
|
+
local line="$1" key val
|
|
401
|
+
case "$line" in
|
|
402
|
+
''|\#*) printf '%s\n' "$line"; return ;;
|
|
403
|
+
esac
|
|
404
|
+
case "$line" in
|
|
405
|
+
*=*) key="${line%%=*}"; val="${line#*=}" ;;
|
|
406
|
+
*) printf '%s\n' "$line"; return ;;
|
|
407
|
+
esac
|
|
408
|
+
key="$(printf '%s' "$key" | tr -d '[:space:]')"
|
|
409
|
+
case "$key" in
|
|
410
|
+
*_API_KEY|*_KEY|*_TOKEN|*_SECRET|*_PASSWORD)
|
|
411
|
+
printf '%s=%s\n' "$key" "$(mask_value "$val")" ;;
|
|
412
|
+
*)
|
|
413
|
+
printf '%s=%s\n' "$key" "$val" ;;
|
|
414
|
+
esac
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
rt_cmd_config() {
|
|
418
|
+
local sub="${1:-}"
|
|
419
|
+
if [ "$sub" = edit ]; then
|
|
420
|
+
rt_config_ensure_dir
|
|
421
|
+
if [ ! -f "$RT_CONFIG_FILE" ]; then : > "$RT_CONFIG_FILE"; chmod 600 "$RT_CONFIG_FILE" 2>/dev/null; fi
|
|
422
|
+
"${EDITOR:-vi}" "$RT_CONFIG_FILE"
|
|
423
|
+
return $?
|
|
424
|
+
fi
|
|
425
|
+
echo "Config: $RT_CONFIG_FILE"
|
|
426
|
+
if [ ! -f "$RT_CONFIG_FILE" ]; then
|
|
427
|
+
echo "(not created yet — run 'roundtable auth' to add a key)"
|
|
428
|
+
return 0
|
|
429
|
+
fi
|
|
430
|
+
echo
|
|
431
|
+
local line
|
|
432
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
433
|
+
rt_print_config_line "$line"
|
|
434
|
+
done < "$RT_CONFIG_FILE"
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
438
|
+
# version / update
|
|
439
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
440
|
+
rt_cmd_version() {
|
|
441
|
+
local v="0.1.1"
|
|
442
|
+
[ -f "$RT_REPODIR/VERSION" ] && v="$(head -n1 "$RT_REPODIR/VERSION" 2>/dev/null | tr -d '[:space:]')"
|
|
443
|
+
[ -z "$v" ] && v="0.1.1"
|
|
444
|
+
echo "roundtable $v"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
rt_cmd_update() {
|
|
448
|
+
# Point each install method at its own upgrade path.
|
|
449
|
+
case "$RT_REPODIR" in
|
|
450
|
+
*/.claude/plugins/*|*/.claude-*/plugins/*)
|
|
451
|
+
echo "Roundtable came from the Claude Code plugin. Update it inside Claude Code:"
|
|
452
|
+
echo
|
|
453
|
+
echo " /plugin marketplace update frontier-infra"
|
|
454
|
+
return 0 ;;
|
|
455
|
+
*/site-packages/*)
|
|
456
|
+
echo "Roundtable came from a Python package. Update it with the tool you used:"
|
|
457
|
+
echo
|
|
458
|
+
echo " uv tool upgrade roundtable-sh"
|
|
459
|
+
echo " pipx upgrade roundtable-sh"
|
|
460
|
+
echo " pip install -U roundtable-sh"
|
|
461
|
+
return 0 ;;
|
|
462
|
+
esac
|
|
463
|
+
local cmd="curl -fsSL $RT_INSTALL_URL | bash"
|
|
464
|
+
echo "Update Roundtable by re-running the installer:"
|
|
465
|
+
echo
|
|
466
|
+
echo " $cmd"
|
|
467
|
+
echo
|
|
468
|
+
printf 'Run it now? [y/N]: '
|
|
469
|
+
local ans; read -r ans 2>/dev/null || ans=""
|
|
470
|
+
case "$ans" in
|
|
471
|
+
y|Y|yes|YES) exec bash -c "$cmd" ;;
|
|
472
|
+
*) echo "Skipped." ;;
|
|
473
|
+
esac
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
# ── dispatch (only when executed directly, not when sourced) ────────────────
|
|
477
|
+
rt_main() {
|
|
478
|
+
local sub="${1:-}"
|
|
479
|
+
[ $# -gt 0 ] && shift
|
|
480
|
+
case "$sub" in
|
|
481
|
+
auth) rt_cmd_auth "$@" ;;
|
|
482
|
+
models) rt_cmd_models "$@" ;;
|
|
483
|
+
heads) rt_cmd_heads "$@" ;;
|
|
484
|
+
doctor) rt_cmd_doctor "$@" ;;
|
|
485
|
+
config) rt_cmd_config "$@" ;;
|
|
486
|
+
version) rt_cmd_version "$@" ;;
|
|
487
|
+
update) rt_cmd_update "$@" ;;
|
|
488
|
+
*) echo "config.sh: unknown subcommand '$sub' (auth|models|heads|doctor|config|version|update)" >&2; return 2 ;;
|
|
489
|
+
esac
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
493
|
+
rt_main "$@"
|
|
494
|
+
fi
|