codegpt-ai 1.25.0 → 1.27.0

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 +339 -0
  2. package/package.json +1 -1
package/chat.py CHANGED
@@ -436,6 +436,12 @@ COMMANDS = {
436
436
  "/audit": "View security audit log",
437
437
  "/security": "Security status dashboard",
438
438
  "/permissions": "View/reset action permissions",
439
+ "/skill": "Create a custom command (/skill name prompt)",
440
+ "/skills": "List custom skills",
441
+ "/browse": "Browse a URL and summarize (/browse url)",
442
+ "/cron": "Schedule a recurring task (/cron 5m /weather)",
443
+ "/crons": "List scheduled tasks",
444
+ "/auto": "AI creates a skill from your description",
439
445
  "/connect": "Connect to remote Ollama (/connect 192.168.1.237)",
440
446
  "/disconnect": "Switch back to local Ollama",
441
447
  "/server": "Show current Ollama server",
@@ -2986,8 +2992,12 @@ def build_codegpt_context(messages=None):
2986
2992
  "pipe_dir": str(MSG_PIPE_DIR),
2987
2993
  "unread_messages": bus_unread("codegpt"),
2988
2994
 
2995
+ # Tool roles
2996
+ "tool_roles": TOOL_ROLES,
2997
+
2989
2998
  "instructions": (
2990
2999
  "You are connected to CodeGPT, a local AI assistant hub. "
3000
+ "Check $CODEGPT_TOOL_ROLE for your specific job. "
2991
3001
  "The user's name is shown above. Their memories contain persistent context. "
2992
3002
  "Recent messages show what they were discussing before launching you. "
2993
3003
  "Project files are Python source in the project_dir. "
@@ -3003,12 +3013,38 @@ def build_codegpt_context(messages=None):
3003
3013
  return context_file
3004
3014
 
3005
3015
 
3016
+ # Tool role descriptions — what each tool's job is when launched from CodeGPT
3017
+ TOOL_ROLES = {
3018
+ "claude": "You are Claude Code, the primary coding assistant. Edit files, run commands, debug code. You have full access to the CodeGPT project.",
3019
+ "openclaw": "You are OpenClaw, a personal AI assistant. Help with tasks, answer questions, manage workflows. You're running inside CodeGPT's sandbox.",
3020
+ "codex": "You are Codex, OpenAI's coding agent. Write and edit code, fix bugs, refactor. You have access to the CodeGPT project files.",
3021
+ "gemini": "You are Gemini CLI, Google's AI. Help with coding, research, and analysis. You have access to the CodeGPT project.",
3022
+ "copilot": "You are GitHub Copilot. Suggest code completions, write functions, help with git workflows.",
3023
+ "cline": "You are Cline, an autonomous coding agent. Plan and implement features, fix bugs, refactor code across multiple files.",
3024
+ "aider": "You are Aider, an AI pair programmer. Edit files based on user instructions. Focus on clean, working code changes.",
3025
+ "interpreter": "You are Open Interpreter. Run code, install packages, manage files. Execute the user's instructions step by step.",
3026
+ "shellgpt": "You are ShellGPT. Generate shell commands from natural language. Be precise and safe.",
3027
+ "opencode": "You are OpenCode, a terminal IDE. Help write, edit, and manage code projects.",
3028
+ "llm": "You are LLM CLI. Chat with various AI models. Help the user with questions and tasks.",
3029
+ "litellm": "You are LiteLLM. Provide a unified interface to 100+ AI models. Help route requests to the best model.",
3030
+ "gorilla": "You are Gorilla CLI. Generate accurate CLI commands from natural language descriptions.",
3031
+ "chatgpt": "You are ChatGPT CLI. Have helpful conversations, answer questions, write content.",
3032
+ "opencommit": "You are OpenCommit. Generate clear, descriptive git commit messages from staged changes.",
3033
+ "aipick": "You are AIPick. Help select and craft the best git commits with AI assistance.",
3034
+ "cursor": "You are Cursor CLI. Help with code editing, navigation, and AI-powered development.",
3035
+ }
3036
+
3037
+
3006
3038
  def build_tool_env(tool_name):
3007
3039
  """Build environment variables for a launched tool."""
3008
3040
  project_dir = str(Path(__file__).parent)
3009
3041
  env = os.environ.copy()
3010
3042
  profile = load_profile()
3011
3043
 
3044
+ # Tool's specific role/job
3045
+ role = TOOL_ROLES.get(tool_name, f"You are {tool_name}, launched from CodeGPT. Help the user with their task.")
3046
+ tool_info = AI_TOOLS.get(tool_name, {})
3047
+
3012
3048
  # Inject CodeGPT context into every tool
3013
3049
  env["CODEGPT_HOME"] = str(Path.home() / ".codegpt")
3014
3050
  env["CODEGPT_PROJECT"] = project_dir
@@ -3018,11 +3054,15 @@ def build_tool_env(tool_name):
3018
3054
  env["CODEGPT_TRAINING"] = str(Path.home() / ".codegpt" / "training")
3019
3055
  env["CODEGPT_CHATS"] = str(Path.home() / ".codegpt" / "chats")
3020
3056
  env["CODEGPT_TOOL"] = tool_name
3057
+ env["CODEGPT_TOOL_ROLE"] = role
3058
+ env["CODEGPT_TOOL_DESC"] = tool_info.get("desc", "")
3021
3059
  env["CODEGPT_USER"] = profile.get("name", "")
3022
3060
  env["CODEGPT_MODEL"] = profile.get("model", MODEL)
3023
3061
  env["CODEGPT_PERSONA"] = profile.get("persona", "default")
3024
3062
  env["CODEGPT_SESSIONS"] = str(profile.get("total_sessions", 0))
3025
3063
  env["CODEGPT_TOTAL_MSGS"] = str(profile.get("total_messages", 0))
3064
+ env["CODEGPT_APP"] = "CodeGPT — Local AI Assistant Hub"
3065
+ env["CODEGPT_VERSION"] = "2.0"
3026
3066
 
3027
3067
  return env
3028
3068
 
@@ -4003,6 +4043,210 @@ def grid_tools(tools):
4003
4043
  audit_log("GRID_LAUNCH", tool_list)
4004
4044
 
4005
4045
 
4046
+ # --- Custom Skills (OpenClaw-style self-extending) ---
4047
+
4048
+ SKILLS_DIR = Path.home() / ".codegpt" / "skills"
4049
+ SKILLS_DIR.mkdir(parents=True, exist_ok=True)
4050
+
4051
+
4052
+ def load_skills():
4053
+ """Load all custom skills."""
4054
+ skills = {}
4055
+ for f in SKILLS_DIR.glob("*.json"):
4056
+ try:
4057
+ data = json.loads(f.read_text())
4058
+ skills[data["name"]] = data
4059
+ except Exception:
4060
+ pass
4061
+ return skills
4062
+
4063
+
4064
+ def save_skill(name, prompt_text, desc=""):
4065
+ """Save a custom skill."""
4066
+ skill = {
4067
+ "name": name,
4068
+ "prompt": prompt_text,
4069
+ "desc": desc or f"Custom skill: {name}",
4070
+ "created": datetime.now().isoformat(),
4071
+ }
4072
+ (SKILLS_DIR / f"{name}.json").write_text(json.dumps(skill, indent=2))
4073
+ return skill
4074
+
4075
+
4076
+ def delete_skill(name):
4077
+ f = SKILLS_DIR / f"{name}.json"
4078
+ if f.exists():
4079
+ f.unlink()
4080
+ return True
4081
+ return False
4082
+
4083
+
4084
+ # --- Browser ---
4085
+
4086
+ def browse_url(url):
4087
+ """Fetch a URL, extract text, and summarize it."""
4088
+ if not url.startswith("http"):
4089
+ url = "https://" + url
4090
+
4091
+ print_sys(f"Fetching {url}...")
4092
+
4093
+ try:
4094
+ resp = requests.get(url, timeout=15, headers={"User-Agent": "CodeGPT/2.0"})
4095
+ resp.raise_for_status()
4096
+ html = resp.text
4097
+
4098
+ # Simple HTML to text — strip tags
4099
+ import re as _re
4100
+ text = _re.sub(r'<script[^>]*>.*?</script>', '', html, flags=_re.DOTALL)
4101
+ text = _re.sub(r'<style[^>]*>.*?</style>', '', text, flags=_re.DOTALL)
4102
+ text = _re.sub(r'<[^>]+>', ' ', text)
4103
+ text = _re.sub(r'\s+', ' ', text).strip()
4104
+
4105
+ # Truncate
4106
+ text = text[:5000]
4107
+
4108
+ console.print(Rule(style="bright_cyan", characters="─"))
4109
+ console.print(Text(f" {url}", style="dim"))
4110
+ console.print()
4111
+
4112
+ # Ask AI to summarize
4113
+ try:
4114
+ ai_resp = requests.post(OLLAMA_URL, json={
4115
+ "model": MODEL,
4116
+ "messages": [
4117
+ {"role": "system", "content": "Summarize this web page content in 3-5 bullet points. Be concise."},
4118
+ {"role": "user", "content": f"URL: {url}\n\nContent:\n{text}"},
4119
+ ],
4120
+ "stream": False,
4121
+ }, timeout=60)
4122
+ summary = ai_resp.json().get("message", {}).get("content", text[:500])
4123
+ console.print(Markdown(summary))
4124
+ except Exception:
4125
+ # Fallback: show raw text
4126
+ console.print(Text(text[:500], style="white"))
4127
+
4128
+ console.print()
4129
+ return text
4130
+
4131
+ except Exception as e:
4132
+ print_err(f"Cannot fetch {url}: {e}")
4133
+ return None
4134
+
4135
+
4136
+ # --- Cron / Scheduled Tasks ---
4137
+
4138
+ active_crons = []
4139
+
4140
+
4141
+ def add_cron(interval_str, command):
4142
+ """Schedule a recurring command."""
4143
+ # Parse interval: 5m, 1h, 30s
4144
+ match = re.match(r'^(\d+)\s*(s|sec|m|min|h|hr|hour)s?$', interval_str, re.IGNORECASE)
4145
+ if not match:
4146
+ print_err("Bad interval. Examples: 30s, 5m, 1h")
4147
+ return
4148
+
4149
+ value = int(match.group(1))
4150
+ unit = match.group(2).lower()
4151
+ if unit in ('m', 'min'):
4152
+ seconds = value * 60
4153
+ elif unit in ('h', 'hr', 'hour'):
4154
+ seconds = value * 3600
4155
+ else:
4156
+ seconds = value
4157
+
4158
+ def run_cron():
4159
+ while True:
4160
+ time.sleep(seconds)
4161
+ # Check if still active
4162
+ if cron_entry not in active_crons:
4163
+ break
4164
+ print_sys(f"[cron] Running: {command}")
4165
+ # Execute as if user typed it
4166
+ cron_entry["last_run"] = datetime.now().isoformat()
4167
+ cron_entry["runs"] += 1
4168
+
4169
+ cron_entry = {
4170
+ "command": command,
4171
+ "interval": interval_str,
4172
+ "seconds": seconds,
4173
+ "runs": 0,
4174
+ "created": datetime.now().isoformat(),
4175
+ "last_run": None,
4176
+ }
4177
+ active_crons.append(cron_entry)
4178
+
4179
+ t = threading.Thread(target=run_cron, daemon=True)
4180
+ t.start()
4181
+ cron_entry["thread"] = t
4182
+
4183
+ print_sys(f"Scheduled: {command} every {interval_str}")
4184
+
4185
+
4186
+ def list_crons():
4187
+ if not active_crons:
4188
+ print_sys("No scheduled tasks. Use: /cron 5m /weather")
4189
+ return
4190
+
4191
+ table = Table(title="Scheduled Tasks", border_style="bright_cyan",
4192
+ title_style="bold cyan", show_header=True, header_style="bold")
4193
+ table.add_column("#", style="cyan", width=3)
4194
+ table.add_column("Command", style="bright_cyan")
4195
+ table.add_column("Interval", style="dim")
4196
+ table.add_column("Runs", style="dim", width=5)
4197
+ for i, c in enumerate(active_crons, 1):
4198
+ table.add_row(str(i), c["command"], c["interval"], str(c["runs"]))
4199
+ console.print(table)
4200
+ console.print()
4201
+
4202
+
4203
+ # --- Auto-Skill (AI creates commands) ---
4204
+
4205
+ def auto_create_skill(description, model):
4206
+ """AI creates a custom skill from a description."""
4207
+ print_sys("AI is designing your skill...")
4208
+
4209
+ try:
4210
+ resp = requests.post(OLLAMA_URL, json={
4211
+ "model": model,
4212
+ "messages": [
4213
+ {"role": "system", "content": (
4214
+ "You are a skill designer for CodeGPT CLI. "
4215
+ "Given a description, create a skill with:\n"
4216
+ "1. A short name (lowercase, no spaces)\n"
4217
+ "2. A system prompt that the AI will use\n"
4218
+ "3. A description\n\n"
4219
+ "Respond ONLY in this JSON format:\n"
4220
+ '{"name": "skillname", "prompt": "system prompt here", "desc": "short description"}'
4221
+ )},
4222
+ {"role": "user", "content": description},
4223
+ ],
4224
+ "stream": False,
4225
+ }, timeout=60)
4226
+ content = resp.json().get("message", {}).get("content", "")
4227
+
4228
+ # Parse JSON from response
4229
+ json_match = re.search(r'\{[^}]+\}', content, re.DOTALL)
4230
+ if json_match:
4231
+ skill_data = json.loads(json_match.group())
4232
+ name = skill_data.get("name", "").lower().replace(" ", "-")
4233
+ prompt_text = skill_data.get("prompt", "")
4234
+ desc = skill_data.get("desc", "")
4235
+
4236
+ if name and prompt_text:
4237
+ save_skill(name, prompt_text, desc)
4238
+ print_success(f"Skill created: /{name}")
4239
+ print_sys(f" {desc}")
4240
+ print_sys(f" Use it: /{name} <your message>")
4241
+ return name
4242
+
4243
+ print_err("AI couldn't create a valid skill. Try a clearer description.")
4244
+
4245
+ except Exception as e:
4246
+ print_err(f"Failed: {e}")
4247
+ return None
4248
+
4249
+
4006
4250
  # --- Voice Input ---
4007
4251
 
4008
4252
  def voice_input():
@@ -6415,6 +6659,101 @@ def main():
6415
6659
  print_sys("Cancelled.")
6416
6660
  continue
6417
6661
 
6662
+ elif cmd == "/skill":
6663
+ args_text = user_input[len("/skill "):].strip()
6664
+ parts = args_text.split(maxsplit=1)
6665
+ if len(parts) == 2:
6666
+ skill_name = parts[0].lower().replace(" ", "-")
6667
+ skill_prompt = parts[1]
6668
+ save_skill(skill_name, skill_prompt)
6669
+ print_success(f"Skill created: /{skill_name}")
6670
+ print_sys(f" Use it: /{skill_name} <your message>")
6671
+ elif len(parts) == 1 and parts[0] == "delete":
6672
+ print_sys("Usage: /skill delete <name>")
6673
+ elif len(parts) == 1:
6674
+ # Check if it's a delete
6675
+ print_sys("Usage: /skill myskill Your custom system prompt here")
6676
+ else:
6677
+ print_sys("Usage: /skill myskill Your system prompt for this skill")
6678
+ print_sys("Example: /skill poet Write responses as poetry")
6679
+ continue
6680
+
6681
+ elif cmd == "/skills":
6682
+ skills = load_skills()
6683
+ if skills:
6684
+ console.print(Text(" Custom skills:", style="bold"))
6685
+ for name, data in skills.items():
6686
+ console.print(Text.from_markup(
6687
+ f" [bright_cyan]/{name}[/] — [dim]{data.get('desc', data.get('prompt', '')[:40])}[/]"
6688
+ ))
6689
+ console.print()
6690
+ else:
6691
+ print_sys("No custom skills. Create one:")
6692
+ print_sys(" /skill myskill Your system prompt")
6693
+ print_sys(" /auto describe what you want the skill to do")
6694
+ continue
6695
+
6696
+ elif cmd == "/browse":
6697
+ url = user_input[len("/browse "):].strip()
6698
+ if url and ask_permission("open_url", f"Fetch {url}"):
6699
+ content = browse_url(url)
6700
+ if content:
6701
+ messages.append({"role": "user", "content": f"[browsed: {url}]"})
6702
+ messages.append({"role": "assistant", "content": content[:500]})
6703
+ session_stats["messages"] += 2
6704
+ else:
6705
+ print_sys("Usage: /browse google.com")
6706
+ continue
6707
+
6708
+ elif cmd == "/cron":
6709
+ args_text = user_input[len("/cron "):].strip()
6710
+ parts = args_text.split(maxsplit=1)
6711
+ if len(parts) == 2:
6712
+ add_cron(parts[0], parts[1])
6713
+ elif args_text == "stop":
6714
+ active_crons.clear()
6715
+ print_sys("All crons stopped.")
6716
+ else:
6717
+ print_sys("Usage: /cron 5m /weather")
6718
+ print_sys(" /cron 1h /status")
6719
+ print_sys(" /cron stop")
6720
+ continue
6721
+
6722
+ elif cmd == "/crons":
6723
+ list_crons()
6724
+ continue
6725
+
6726
+ elif cmd == "/auto":
6727
+ desc = user_input[len("/auto "):].strip()
6728
+ if desc:
6729
+ auto_create_skill(desc, model)
6730
+ else:
6731
+ print_sys("Usage: /auto a skill that writes haiku poetry")
6732
+ print_sys(" /auto a code reviewer that checks for security bugs")
6733
+ continue
6734
+
6735
+ # Check custom skills
6736
+ elif cmd[1:] in load_skills():
6737
+ skill = load_skills()[cmd[1:]]
6738
+ skill_input = user_input[len(cmd):].strip()
6739
+ if skill_input:
6740
+ messages.append({"role": "user", "content": skill_input})
6741
+ session_stats["messages"] += 1
6742
+ # Use skill's prompt as system
6743
+ old_system = system
6744
+ system = skill["prompt"]
6745
+ response = stream_response(messages, system, model)
6746
+ system = old_system
6747
+ if response:
6748
+ messages.append({"role": "assistant", "content": response})
6749
+ session_stats["messages"] += 1
6750
+ else:
6751
+ messages.pop()
6752
+ else:
6753
+ print_sys(f"Usage: /{cmd[1:]} <your message>")
6754
+ print_sys(f" Prompt: {skill['prompt'][:60]}...")
6755
+ continue
6756
+
6418
6757
  elif cmd == "/permissions":
6419
6758
  sub = user_input[len("/permissions "):].strip().lower()
6420
6759
  if sub == "reset":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegpt-ai",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "description": "Local AI Assistant Hub — 80+ commands, 29 tools, 8 agents, training, security",
5
5
  "author": "ArukuX",
6
6
  "license": "MIT",