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.
- package/chat.py +339 -0
- 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":
|