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.
Files changed (2) hide show
  1. package/chat.py +517 -2
  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
- if not url.startswith("http"):
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":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegpt-ai",
3
- "version": "1.26.0",
3
+ "version": "1.28.1",
4
4
  "description": "Local AI Assistant Hub — 80+ commands, 29 tools, 8 agents, training, security",
5
5
  "author": "ArukuX",
6
6
  "license": "MIT",