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.
Files changed (2) hide show
  1. package/chat.py +305 -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",
@@ -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":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegpt-ai",
3
- "version": "1.26.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",