codegpt-ai 1.26.0 → 1.28.1
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.
- package/chat.py +517 -2
- package/package.json +1 -1
package/chat.py
CHANGED
|
@@ -422,6 +422,8 @@ COMMANDS = {
|
|
|
422
422
|
"/vote": "All agents vote on a question",
|
|
423
423
|
"/swarm": "Agents collaborate on a task step by step",
|
|
424
424
|
"/team": "Start a team chat with 2 AIs (/team coder reviewer)",
|
|
425
|
+
"/room": "AI chat room — multiple AIs talk (/room coder reviewer architect)",
|
|
426
|
+
"/spectate": "Watch AIs chat without you (/spectate claude codex topic)",
|
|
425
427
|
"/github": "GitHub tools (/github repos, issues, prs)",
|
|
426
428
|
"/weather": "Get weather (/weather London)",
|
|
427
429
|
"/open": "Open URL in browser (/open google.com)",
|
|
@@ -436,6 +438,12 @@ COMMANDS = {
|
|
|
436
438
|
"/audit": "View security audit log",
|
|
437
439
|
"/security": "Security status dashboard",
|
|
438
440
|
"/permissions": "View/reset action permissions",
|
|
441
|
+
"/skill": "Create a custom command (/skill name prompt)",
|
|
442
|
+
"/skills": "List custom skills",
|
|
443
|
+
"/browse": "Browse a URL and summarize (/browse url)",
|
|
444
|
+
"/cron": "Schedule a recurring task (/cron 5m /weather)",
|
|
445
|
+
"/crons": "List scheduled tasks",
|
|
446
|
+
"/auto": "AI creates a skill from your description",
|
|
439
447
|
"/connect": "Connect to remote Ollama (/connect 192.168.1.237)",
|
|
440
448
|
"/disconnect": "Switch back to local Ollama",
|
|
441
449
|
"/server": "Show current Ollama server",
|
|
@@ -2406,10 +2414,35 @@ def get_weather(city):
|
|
|
2406
2414
|
|
|
2407
2415
|
|
|
2408
2416
|
def open_url(url):
|
|
2409
|
-
"""Open a URL in the default browser."""
|
|
2417
|
+
"""Open a URL or search query in the default browser."""
|
|
2410
2418
|
import webbrowser
|
|
2411
|
-
|
|
2419
|
+
|
|
2420
|
+
# Shortcuts
|
|
2421
|
+
shortcuts = {
|
|
2422
|
+
"google": "https://google.com",
|
|
2423
|
+
"youtube": "https://youtube.com",
|
|
2424
|
+
"github": "https://github.com",
|
|
2425
|
+
"reddit": "https://reddit.com",
|
|
2426
|
+
"twitter": "https://x.com",
|
|
2427
|
+
"x": "https://x.com",
|
|
2428
|
+
"stackoverflow": "https://stackoverflow.com",
|
|
2429
|
+
"npm": "https://npmjs.com",
|
|
2430
|
+
"pypi": "https://pypi.org",
|
|
2431
|
+
"ollama": "https://ollama.com",
|
|
2432
|
+
"claude": "https://claude.ai",
|
|
2433
|
+
"chatgpt": "https://chat.openai.com",
|
|
2434
|
+
"gemini": "https://gemini.google.com",
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
if url.lower() in shortcuts:
|
|
2438
|
+
url = shortcuts[url.lower()]
|
|
2439
|
+
elif "." not in url and ":" not in url:
|
|
2440
|
+
# No dots = search query, not a URL
|
|
2441
|
+
query = url.replace(" ", "+")
|
|
2442
|
+
url = f"https://google.com/search?q={query}"
|
|
2443
|
+
elif not url.startswith("http"):
|
|
2412
2444
|
url = "https://" + url
|
|
2445
|
+
|
|
2413
2446
|
webbrowser.open(url)
|
|
2414
2447
|
print_sys(f"Opened: {url}")
|
|
2415
2448
|
audit_log("OPEN_URL", url)
|
|
@@ -3878,6 +3911,149 @@ def team_chat(name1, name2, default_model, system):
|
|
|
3878
3911
|
return history
|
|
3879
3912
|
|
|
3880
3913
|
|
|
3914
|
+
# --- Chat Room ---
|
|
3915
|
+
|
|
3916
|
+
def chat_room(member_names, default_model, system, user_joins=True):
|
|
3917
|
+
"""Multi-AI chat room. User can join or spectate."""
|
|
3918
|
+
members = [resolve_team_member(n) for n in member_names]
|
|
3919
|
+
|
|
3920
|
+
names_display = ", ".join(f"[{m['color']}]{m['name']}[/]" for m in members)
|
|
3921
|
+
mode = "Join" if user_joins else "Spectate"
|
|
3922
|
+
|
|
3923
|
+
console.print(Rule(style="bright_green", characters="─"))
|
|
3924
|
+
console.print(Text.from_markup(
|
|
3925
|
+
f" [bold]Chat Room[/] — {mode} mode\n"
|
|
3926
|
+
f" Members: {names_display}\n"
|
|
3927
|
+
))
|
|
3928
|
+
if user_joins:
|
|
3929
|
+
console.print(Text(" Type to talk. @name to address one AI. 'exit' to leave.", style="dim"))
|
|
3930
|
+
else:
|
|
3931
|
+
console.print(Text(" Watching AIs chat. Ctrl+C to stop.", style="dim"))
|
|
3932
|
+
console.print(Rule(style="bright_green", characters="─"))
|
|
3933
|
+
console.print()
|
|
3934
|
+
|
|
3935
|
+
history = []
|
|
3936
|
+
|
|
3937
|
+
if user_joins:
|
|
3938
|
+
# Interactive room — user + multiple AIs
|
|
3939
|
+
while True:
|
|
3940
|
+
try:
|
|
3941
|
+
user_input = prompt(
|
|
3942
|
+
[("class:prompt", " You ❯ ")],
|
|
3943
|
+
style=input_style,
|
|
3944
|
+
history=input_history,
|
|
3945
|
+
).strip()
|
|
3946
|
+
except (KeyboardInterrupt, EOFError):
|
|
3947
|
+
break
|
|
3948
|
+
|
|
3949
|
+
if not user_input or user_input.lower() in ("exit", "/exit", "quit"):
|
|
3950
|
+
break
|
|
3951
|
+
|
|
3952
|
+
console.print(Text(f" {user_input}", style="bold white"))
|
|
3953
|
+
console.print()
|
|
3954
|
+
history.append({"speaker": "user", "content": user_input})
|
|
3955
|
+
|
|
3956
|
+
# Check for @mentions
|
|
3957
|
+
mentioned = []
|
|
3958
|
+
for m in members:
|
|
3959
|
+
if f"@{m['name']}" in user_input.lower():
|
|
3960
|
+
mentioned.append(m)
|
|
3961
|
+
|
|
3962
|
+
# If no mentions, all respond
|
|
3963
|
+
responders = mentioned if mentioned else members
|
|
3964
|
+
|
|
3965
|
+
for member in responders:
|
|
3966
|
+
others = [m['name'] for m in members if m != member] + ["user"]
|
|
3967
|
+
conv = "\n".join(
|
|
3968
|
+
f"{h['speaker']}: {h['content'][:200]}"
|
|
3969
|
+
for h in history[-10:]
|
|
3970
|
+
)
|
|
3971
|
+
|
|
3972
|
+
room_prompt = (
|
|
3973
|
+
f"You are {member['name']} in a group chat with {', '.join(others)}.\n"
|
|
3974
|
+
f"Chat so far:\n{conv}\n\n"
|
|
3975
|
+
f"Respond as {member['name']}. Keep it short (2-4 sentences). "
|
|
3976
|
+
f"React to what was said. Agree, disagree, or add something new. "
|
|
3977
|
+
f"Don't repeat what others said."
|
|
3978
|
+
)
|
|
3979
|
+
|
|
3980
|
+
try:
|
|
3981
|
+
resp = requests.post(OLLAMA_URL, json={
|
|
3982
|
+
"model": member["model"] or default_model,
|
|
3983
|
+
"messages": [
|
|
3984
|
+
{"role": "system", "content": member["system"]},
|
|
3985
|
+
{"role": "user", "content": room_prompt},
|
|
3986
|
+
],
|
|
3987
|
+
"stream": False,
|
|
3988
|
+
}, timeout=60)
|
|
3989
|
+
response = resp.json().get("message", {}).get("content", "")
|
|
3990
|
+
except Exception as e:
|
|
3991
|
+
response = f"(offline)"
|
|
3992
|
+
|
|
3993
|
+
console.print(Text.from_markup(f" [{member['color']}]{member['name']}[/] {response}"))
|
|
3994
|
+
console.print()
|
|
3995
|
+
history.append({"speaker": member["name"], "content": response})
|
|
3996
|
+
bus_send(member["name"], "codegpt", response[:200], "response")
|
|
3997
|
+
|
|
3998
|
+
else:
|
|
3999
|
+
# Spectate mode — AIs chat with each other, user watches
|
|
4000
|
+
try:
|
|
4001
|
+
# Get initial topic from last arg or default
|
|
4002
|
+
topic = "Introduce yourselves and start a technical discussion."
|
|
4003
|
+
if history:
|
|
4004
|
+
topic = history[-1]["content"]
|
|
4005
|
+
|
|
4006
|
+
current_input = topic
|
|
4007
|
+
rounds = 0
|
|
4008
|
+
max_rounds = 12
|
|
4009
|
+
|
|
4010
|
+
while rounds < max_rounds:
|
|
4011
|
+
for member in members:
|
|
4012
|
+
rounds += 1
|
|
4013
|
+
if rounds > max_rounds:
|
|
4014
|
+
break
|
|
4015
|
+
|
|
4016
|
+
others = [m['name'] for m in members if m != member]
|
|
4017
|
+
conv = "\n".join(
|
|
4018
|
+
f"{h['speaker']}: {h['content'][:200]}"
|
|
4019
|
+
for h in history[-8:]
|
|
4020
|
+
)
|
|
4021
|
+
|
|
4022
|
+
room_prompt = (
|
|
4023
|
+
f"You are {member['name']} in a group chat with {', '.join(others)}.\n"
|
|
4024
|
+
f"{'Topic: ' + current_input if not history else 'Chat so far:'}\n"
|
|
4025
|
+
f"{conv}\n\n"
|
|
4026
|
+
f"Respond as {member['name']}. Keep it short (2-3 sentences). "
|
|
4027
|
+
f"Build on the conversation. Be opinionated."
|
|
4028
|
+
)
|
|
4029
|
+
|
|
4030
|
+
try:
|
|
4031
|
+
resp = requests.post(OLLAMA_URL, json={
|
|
4032
|
+
"model": member["model"] or default_model,
|
|
4033
|
+
"messages": [
|
|
4034
|
+
{"role": "system", "content": member["system"]},
|
|
4035
|
+
{"role": "user", "content": room_prompt},
|
|
4036
|
+
],
|
|
4037
|
+
"stream": False,
|
|
4038
|
+
}, timeout=60)
|
|
4039
|
+
response = resp.json().get("message", {}).get("content", "")
|
|
4040
|
+
except Exception as e:
|
|
4041
|
+
response = "(offline)"
|
|
4042
|
+
|
|
4043
|
+
console.print(Text.from_markup(f" [{member['color']}]{member['name']}[/] {response}"))
|
|
4044
|
+
console.print()
|
|
4045
|
+
history.append({"speaker": member["name"], "content": response})
|
|
4046
|
+
time.sleep(0.5)
|
|
4047
|
+
|
|
4048
|
+
except KeyboardInterrupt:
|
|
4049
|
+
pass
|
|
4050
|
+
|
|
4051
|
+
console.print(Rule(style="dim", characters="─"))
|
|
4052
|
+
console.print(Text(f" Room closed. {len(history)} messages.", style="dim"))
|
|
4053
|
+
console.print()
|
|
4054
|
+
return history
|
|
4055
|
+
|
|
4056
|
+
|
|
3881
4057
|
# --- Split Screen ---
|
|
3882
4058
|
|
|
3883
4059
|
def get_tool_cmd(name):
|
|
@@ -4037,6 +4213,210 @@ def grid_tools(tools):
|
|
|
4037
4213
|
audit_log("GRID_LAUNCH", tool_list)
|
|
4038
4214
|
|
|
4039
4215
|
|
|
4216
|
+
# --- Custom Skills (OpenClaw-style self-extending) ---
|
|
4217
|
+
|
|
4218
|
+
SKILLS_DIR = Path.home() / ".codegpt" / "skills"
|
|
4219
|
+
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
4220
|
+
|
|
4221
|
+
|
|
4222
|
+
def load_skills():
|
|
4223
|
+
"""Load all custom skills."""
|
|
4224
|
+
skills = {}
|
|
4225
|
+
for f in SKILLS_DIR.glob("*.json"):
|
|
4226
|
+
try:
|
|
4227
|
+
data = json.loads(f.read_text())
|
|
4228
|
+
skills[data["name"]] = data
|
|
4229
|
+
except Exception:
|
|
4230
|
+
pass
|
|
4231
|
+
return skills
|
|
4232
|
+
|
|
4233
|
+
|
|
4234
|
+
def save_skill(name, prompt_text, desc=""):
|
|
4235
|
+
"""Save a custom skill."""
|
|
4236
|
+
skill = {
|
|
4237
|
+
"name": name,
|
|
4238
|
+
"prompt": prompt_text,
|
|
4239
|
+
"desc": desc or f"Custom skill: {name}",
|
|
4240
|
+
"created": datetime.now().isoformat(),
|
|
4241
|
+
}
|
|
4242
|
+
(SKILLS_DIR / f"{name}.json").write_text(json.dumps(skill, indent=2))
|
|
4243
|
+
return skill
|
|
4244
|
+
|
|
4245
|
+
|
|
4246
|
+
def delete_skill(name):
|
|
4247
|
+
f = SKILLS_DIR / f"{name}.json"
|
|
4248
|
+
if f.exists():
|
|
4249
|
+
f.unlink()
|
|
4250
|
+
return True
|
|
4251
|
+
return False
|
|
4252
|
+
|
|
4253
|
+
|
|
4254
|
+
# --- Browser ---
|
|
4255
|
+
|
|
4256
|
+
def browse_url(url):
|
|
4257
|
+
"""Fetch a URL, extract text, and summarize it."""
|
|
4258
|
+
if not url.startswith("http"):
|
|
4259
|
+
url = "https://" + url
|
|
4260
|
+
|
|
4261
|
+
print_sys(f"Fetching {url}...")
|
|
4262
|
+
|
|
4263
|
+
try:
|
|
4264
|
+
resp = requests.get(url, timeout=15, headers={"User-Agent": "CodeGPT/2.0"})
|
|
4265
|
+
resp.raise_for_status()
|
|
4266
|
+
html = resp.text
|
|
4267
|
+
|
|
4268
|
+
# Simple HTML to text — strip tags
|
|
4269
|
+
import re as _re
|
|
4270
|
+
text = _re.sub(r'<script[^>]*>.*?</script>', '', html, flags=_re.DOTALL)
|
|
4271
|
+
text = _re.sub(r'<style[^>]*>.*?</style>', '', text, flags=_re.DOTALL)
|
|
4272
|
+
text = _re.sub(r'<[^>]+>', ' ', text)
|
|
4273
|
+
text = _re.sub(r'\s+', ' ', text).strip()
|
|
4274
|
+
|
|
4275
|
+
# Truncate
|
|
4276
|
+
text = text[:5000]
|
|
4277
|
+
|
|
4278
|
+
console.print(Rule(style="bright_cyan", characters="─"))
|
|
4279
|
+
console.print(Text(f" {url}", style="dim"))
|
|
4280
|
+
console.print()
|
|
4281
|
+
|
|
4282
|
+
# Ask AI to summarize
|
|
4283
|
+
try:
|
|
4284
|
+
ai_resp = requests.post(OLLAMA_URL, json={
|
|
4285
|
+
"model": MODEL,
|
|
4286
|
+
"messages": [
|
|
4287
|
+
{"role": "system", "content": "Summarize this web page content in 3-5 bullet points. Be concise."},
|
|
4288
|
+
{"role": "user", "content": f"URL: {url}\n\nContent:\n{text}"},
|
|
4289
|
+
],
|
|
4290
|
+
"stream": False,
|
|
4291
|
+
}, timeout=60)
|
|
4292
|
+
summary = ai_resp.json().get("message", {}).get("content", text[:500])
|
|
4293
|
+
console.print(Markdown(summary))
|
|
4294
|
+
except Exception:
|
|
4295
|
+
# Fallback: show raw text
|
|
4296
|
+
console.print(Text(text[:500], style="white"))
|
|
4297
|
+
|
|
4298
|
+
console.print()
|
|
4299
|
+
return text
|
|
4300
|
+
|
|
4301
|
+
except Exception as e:
|
|
4302
|
+
print_err(f"Cannot fetch {url}: {e}")
|
|
4303
|
+
return None
|
|
4304
|
+
|
|
4305
|
+
|
|
4306
|
+
# --- Cron / Scheduled Tasks ---
|
|
4307
|
+
|
|
4308
|
+
active_crons = []
|
|
4309
|
+
|
|
4310
|
+
|
|
4311
|
+
def add_cron(interval_str, command):
|
|
4312
|
+
"""Schedule a recurring command."""
|
|
4313
|
+
# Parse interval: 5m, 1h, 30s
|
|
4314
|
+
match = re.match(r'^(\d+)\s*(s|sec|m|min|h|hr|hour)s?$', interval_str, re.IGNORECASE)
|
|
4315
|
+
if not match:
|
|
4316
|
+
print_err("Bad interval. Examples: 30s, 5m, 1h")
|
|
4317
|
+
return
|
|
4318
|
+
|
|
4319
|
+
value = int(match.group(1))
|
|
4320
|
+
unit = match.group(2).lower()
|
|
4321
|
+
if unit in ('m', 'min'):
|
|
4322
|
+
seconds = value * 60
|
|
4323
|
+
elif unit in ('h', 'hr', 'hour'):
|
|
4324
|
+
seconds = value * 3600
|
|
4325
|
+
else:
|
|
4326
|
+
seconds = value
|
|
4327
|
+
|
|
4328
|
+
def run_cron():
|
|
4329
|
+
while True:
|
|
4330
|
+
time.sleep(seconds)
|
|
4331
|
+
# Check if still active
|
|
4332
|
+
if cron_entry not in active_crons:
|
|
4333
|
+
break
|
|
4334
|
+
print_sys(f"[cron] Running: {command}")
|
|
4335
|
+
# Execute as if user typed it
|
|
4336
|
+
cron_entry["last_run"] = datetime.now().isoformat()
|
|
4337
|
+
cron_entry["runs"] += 1
|
|
4338
|
+
|
|
4339
|
+
cron_entry = {
|
|
4340
|
+
"command": command,
|
|
4341
|
+
"interval": interval_str,
|
|
4342
|
+
"seconds": seconds,
|
|
4343
|
+
"runs": 0,
|
|
4344
|
+
"created": datetime.now().isoformat(),
|
|
4345
|
+
"last_run": None,
|
|
4346
|
+
}
|
|
4347
|
+
active_crons.append(cron_entry)
|
|
4348
|
+
|
|
4349
|
+
t = threading.Thread(target=run_cron, daemon=True)
|
|
4350
|
+
t.start()
|
|
4351
|
+
cron_entry["thread"] = t
|
|
4352
|
+
|
|
4353
|
+
print_sys(f"Scheduled: {command} every {interval_str}")
|
|
4354
|
+
|
|
4355
|
+
|
|
4356
|
+
def list_crons():
|
|
4357
|
+
if not active_crons:
|
|
4358
|
+
print_sys("No scheduled tasks. Use: /cron 5m /weather")
|
|
4359
|
+
return
|
|
4360
|
+
|
|
4361
|
+
table = Table(title="Scheduled Tasks", border_style="bright_cyan",
|
|
4362
|
+
title_style="bold cyan", show_header=True, header_style="bold")
|
|
4363
|
+
table.add_column("#", style="cyan", width=3)
|
|
4364
|
+
table.add_column("Command", style="bright_cyan")
|
|
4365
|
+
table.add_column("Interval", style="dim")
|
|
4366
|
+
table.add_column("Runs", style="dim", width=5)
|
|
4367
|
+
for i, c in enumerate(active_crons, 1):
|
|
4368
|
+
table.add_row(str(i), c["command"], c["interval"], str(c["runs"]))
|
|
4369
|
+
console.print(table)
|
|
4370
|
+
console.print()
|
|
4371
|
+
|
|
4372
|
+
|
|
4373
|
+
# --- Auto-Skill (AI creates commands) ---
|
|
4374
|
+
|
|
4375
|
+
def auto_create_skill(description, model):
|
|
4376
|
+
"""AI creates a custom skill from a description."""
|
|
4377
|
+
print_sys("AI is designing your skill...")
|
|
4378
|
+
|
|
4379
|
+
try:
|
|
4380
|
+
resp = requests.post(OLLAMA_URL, json={
|
|
4381
|
+
"model": model,
|
|
4382
|
+
"messages": [
|
|
4383
|
+
{"role": "system", "content": (
|
|
4384
|
+
"You are a skill designer for CodeGPT CLI. "
|
|
4385
|
+
"Given a description, create a skill with:\n"
|
|
4386
|
+
"1. A short name (lowercase, no spaces)\n"
|
|
4387
|
+
"2. A system prompt that the AI will use\n"
|
|
4388
|
+
"3. A description\n\n"
|
|
4389
|
+
"Respond ONLY in this JSON format:\n"
|
|
4390
|
+
'{"name": "skillname", "prompt": "system prompt here", "desc": "short description"}'
|
|
4391
|
+
)},
|
|
4392
|
+
{"role": "user", "content": description},
|
|
4393
|
+
],
|
|
4394
|
+
"stream": False,
|
|
4395
|
+
}, timeout=60)
|
|
4396
|
+
content = resp.json().get("message", {}).get("content", "")
|
|
4397
|
+
|
|
4398
|
+
# Parse JSON from response
|
|
4399
|
+
json_match = re.search(r'\{[^}]+\}', content, re.DOTALL)
|
|
4400
|
+
if json_match:
|
|
4401
|
+
skill_data = json.loads(json_match.group())
|
|
4402
|
+
name = skill_data.get("name", "").lower().replace(" ", "-")
|
|
4403
|
+
prompt_text = skill_data.get("prompt", "")
|
|
4404
|
+
desc = skill_data.get("desc", "")
|
|
4405
|
+
|
|
4406
|
+
if name and prompt_text:
|
|
4407
|
+
save_skill(name, prompt_text, desc)
|
|
4408
|
+
print_success(f"Skill created: /{name}")
|
|
4409
|
+
print_sys(f" {desc}")
|
|
4410
|
+
print_sys(f" Use it: /{name} <your message>")
|
|
4411
|
+
return name
|
|
4412
|
+
|
|
4413
|
+
print_err("AI couldn't create a valid skill. Try a clearer description.")
|
|
4414
|
+
|
|
4415
|
+
except Exception as e:
|
|
4416
|
+
print_err(f"Failed: {e}")
|
|
4417
|
+
return None
|
|
4418
|
+
|
|
4419
|
+
|
|
4040
4420
|
# --- Voice Input ---
|
|
4041
4421
|
|
|
4042
4422
|
def voice_input():
|
|
@@ -5446,6 +5826,46 @@ def main():
|
|
|
5446
5826
|
print_sys("Terminal too narrow for sidebar. Widen to 80+ chars.")
|
|
5447
5827
|
continue
|
|
5448
5828
|
|
|
5829
|
+
elif cmd == "/room":
|
|
5830
|
+
parts = user_input[len("/room "):].strip().split()
|
|
5831
|
+
if len(parts) >= 2:
|
|
5832
|
+
history = chat_room(parts, model, system, user_joins=True)
|
|
5833
|
+
for h in history:
|
|
5834
|
+
if h["speaker"] == "user":
|
|
5835
|
+
messages.append({"role": "user", "content": h["content"]})
|
|
5836
|
+
else:
|
|
5837
|
+
messages.append({"role": "assistant", "content": f"[{h['speaker']}] {h['content']}"})
|
|
5838
|
+
session_stats["messages"] += len(history)
|
|
5839
|
+
else:
|
|
5840
|
+
print_sys("Usage: /room coder reviewer architect")
|
|
5841
|
+
print_sys(" /room claude codex gemini deepseek")
|
|
5842
|
+
print_sys(f"\nAvailable: {', '.join(list(AI_AGENTS.keys()) + list(TOOL_PERSONAS.keys()))}")
|
|
5843
|
+
continue
|
|
5844
|
+
|
|
5845
|
+
elif cmd == "/spectate":
|
|
5846
|
+
args = user_input[len("/spectate "):].strip().split()
|
|
5847
|
+
if len(args) >= 2:
|
|
5848
|
+
# Last arg could be a topic
|
|
5849
|
+
names = args
|
|
5850
|
+
topic = ""
|
|
5851
|
+
# Check if last args aren't AI names — treat as topic
|
|
5852
|
+
all_names = set(AI_AGENTS.keys()) | set(TOOL_PERSONAS.keys())
|
|
5853
|
+
topic_words = []
|
|
5854
|
+
while names and names[-1] not in all_names:
|
|
5855
|
+
topic_words.insert(0, names.pop())
|
|
5856
|
+
topic = " ".join(topic_words) if topic_words else "Discuss the best programming practices"
|
|
5857
|
+
|
|
5858
|
+
if len(names) >= 2:
|
|
5859
|
+
# Inject topic
|
|
5860
|
+
history_init = [{"speaker": "moderator", "content": topic}]
|
|
5861
|
+
h = chat_room(names, model, system, user_joins=False)
|
|
5862
|
+
else:
|
|
5863
|
+
print_sys("Need at least 2 AIs. Example: /spectate coder reviewer discuss Python")
|
|
5864
|
+
else:
|
|
5865
|
+
print_sys("Usage: /spectate coder reviewer discuss error handling")
|
|
5866
|
+
print_sys(" /spectate claude codex gemini debate which is best")
|
|
5867
|
+
continue
|
|
5868
|
+
|
|
5449
5869
|
elif cmd == "/monitor":
|
|
5450
5870
|
# Live updating dashboard — press Ctrl+C to exit
|
|
5451
5871
|
console.print(Text(" Live monitor — Ctrl+C to stop\n", style="dim"))
|
|
@@ -6449,6 +6869,101 @@ def main():
|
|
|
6449
6869
|
print_sys("Cancelled.")
|
|
6450
6870
|
continue
|
|
6451
6871
|
|
|
6872
|
+
elif cmd == "/skill":
|
|
6873
|
+
args_text = user_input[len("/skill "):].strip()
|
|
6874
|
+
parts = args_text.split(maxsplit=1)
|
|
6875
|
+
if len(parts) == 2:
|
|
6876
|
+
skill_name = parts[0].lower().replace(" ", "-")
|
|
6877
|
+
skill_prompt = parts[1]
|
|
6878
|
+
save_skill(skill_name, skill_prompt)
|
|
6879
|
+
print_success(f"Skill created: /{skill_name}")
|
|
6880
|
+
print_sys(f" Use it: /{skill_name} <your message>")
|
|
6881
|
+
elif len(parts) == 1 and parts[0] == "delete":
|
|
6882
|
+
print_sys("Usage: /skill delete <name>")
|
|
6883
|
+
elif len(parts) == 1:
|
|
6884
|
+
# Check if it's a delete
|
|
6885
|
+
print_sys("Usage: /skill myskill Your custom system prompt here")
|
|
6886
|
+
else:
|
|
6887
|
+
print_sys("Usage: /skill myskill Your system prompt for this skill")
|
|
6888
|
+
print_sys("Example: /skill poet Write responses as poetry")
|
|
6889
|
+
continue
|
|
6890
|
+
|
|
6891
|
+
elif cmd == "/skills":
|
|
6892
|
+
skills = load_skills()
|
|
6893
|
+
if skills:
|
|
6894
|
+
console.print(Text(" Custom skills:", style="bold"))
|
|
6895
|
+
for name, data in skills.items():
|
|
6896
|
+
console.print(Text.from_markup(
|
|
6897
|
+
f" [bright_cyan]/{name}[/] — [dim]{data.get('desc', data.get('prompt', '')[:40])}[/]"
|
|
6898
|
+
))
|
|
6899
|
+
console.print()
|
|
6900
|
+
else:
|
|
6901
|
+
print_sys("No custom skills. Create one:")
|
|
6902
|
+
print_sys(" /skill myskill Your system prompt")
|
|
6903
|
+
print_sys(" /auto describe what you want the skill to do")
|
|
6904
|
+
continue
|
|
6905
|
+
|
|
6906
|
+
elif cmd == "/browse":
|
|
6907
|
+
url = user_input[len("/browse "):].strip()
|
|
6908
|
+
if url and ask_permission("open_url", f"Fetch {url}"):
|
|
6909
|
+
content = browse_url(url)
|
|
6910
|
+
if content:
|
|
6911
|
+
messages.append({"role": "user", "content": f"[browsed: {url}]"})
|
|
6912
|
+
messages.append({"role": "assistant", "content": content[:500]})
|
|
6913
|
+
session_stats["messages"] += 2
|
|
6914
|
+
else:
|
|
6915
|
+
print_sys("Usage: /browse google.com")
|
|
6916
|
+
continue
|
|
6917
|
+
|
|
6918
|
+
elif cmd == "/cron":
|
|
6919
|
+
args_text = user_input[len("/cron "):].strip()
|
|
6920
|
+
parts = args_text.split(maxsplit=1)
|
|
6921
|
+
if len(parts) == 2:
|
|
6922
|
+
add_cron(parts[0], parts[1])
|
|
6923
|
+
elif args_text == "stop":
|
|
6924
|
+
active_crons.clear()
|
|
6925
|
+
print_sys("All crons stopped.")
|
|
6926
|
+
else:
|
|
6927
|
+
print_sys("Usage: /cron 5m /weather")
|
|
6928
|
+
print_sys(" /cron 1h /status")
|
|
6929
|
+
print_sys(" /cron stop")
|
|
6930
|
+
continue
|
|
6931
|
+
|
|
6932
|
+
elif cmd == "/crons":
|
|
6933
|
+
list_crons()
|
|
6934
|
+
continue
|
|
6935
|
+
|
|
6936
|
+
elif cmd == "/auto":
|
|
6937
|
+
desc = user_input[len("/auto "):].strip()
|
|
6938
|
+
if desc:
|
|
6939
|
+
auto_create_skill(desc, model)
|
|
6940
|
+
else:
|
|
6941
|
+
print_sys("Usage: /auto a skill that writes haiku poetry")
|
|
6942
|
+
print_sys(" /auto a code reviewer that checks for security bugs")
|
|
6943
|
+
continue
|
|
6944
|
+
|
|
6945
|
+
# Check custom skills
|
|
6946
|
+
elif cmd[1:] in load_skills():
|
|
6947
|
+
skill = load_skills()[cmd[1:]]
|
|
6948
|
+
skill_input = user_input[len(cmd):].strip()
|
|
6949
|
+
if skill_input:
|
|
6950
|
+
messages.append({"role": "user", "content": skill_input})
|
|
6951
|
+
session_stats["messages"] += 1
|
|
6952
|
+
# Use skill's prompt as system
|
|
6953
|
+
old_system = system
|
|
6954
|
+
system = skill["prompt"]
|
|
6955
|
+
response = stream_response(messages, system, model)
|
|
6956
|
+
system = old_system
|
|
6957
|
+
if response:
|
|
6958
|
+
messages.append({"role": "assistant", "content": response})
|
|
6959
|
+
session_stats["messages"] += 1
|
|
6960
|
+
else:
|
|
6961
|
+
messages.pop()
|
|
6962
|
+
else:
|
|
6963
|
+
print_sys(f"Usage: /{cmd[1:]} <your message>")
|
|
6964
|
+
print_sys(f" Prompt: {skill['prompt'][:60]}...")
|
|
6965
|
+
continue
|
|
6966
|
+
|
|
6452
6967
|
elif cmd == "/permissions":
|
|
6453
6968
|
sub = user_input[len("/permissions "):].strip().lower()
|
|
6454
6969
|
if sub == "reset":
|