codegpt-ai 1.26.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 +305 -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",
|
|
@@ -4037,6 +4043,210 @@ def grid_tools(tools):
|
|
|
4037
4043
|
audit_log("GRID_LAUNCH", tool_list)
|
|
4038
4044
|
|
|
4039
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
|
+
|
|
4040
4250
|
# --- Voice Input ---
|
|
4041
4251
|
|
|
4042
4252
|
def voice_input():
|
|
@@ -6449,6 +6659,101 @@ def main():
|
|
|
6449
6659
|
print_sys("Cancelled.")
|
|
6450
6660
|
continue
|
|
6451
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
|
+
|
|
6452
6757
|
elif cmd == "/permissions":
|
|
6453
6758
|
sub = user_input[len("/permissions "):].strip().lower()
|
|
6454
6759
|
if sub == "reset":
|