mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat(cli): imporve cow cli and skill hub integration
This commit is contained in:
1
cli/VERSION
Normal file
1
cli/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
2.0.4
|
||||
@@ -1,3 +1,13 @@
|
||||
"""CowAgent CLI - Manage your CowAgent from the command line."""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
import os as _os
|
||||
|
||||
def _read_version():
|
||||
version_file = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "VERSION")
|
||||
try:
|
||||
with open(version_file, "r") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return "0.0.0"
|
||||
|
||||
__version__ = _read_version()
|
||||
|
||||
53
cli/cli.py
53
cli/cli.py
@@ -7,11 +7,56 @@ from cli.commands.process import start, stop, restart, status, logs
|
||||
from cli.commands.context import context
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(__version__, '--version', '-v', prog_name='cow')
|
||||
def main():
|
||||
HELP_TEXT = """Usage: cow COMMAND [ARGS]...
|
||||
|
||||
CowAgent CLI - Manage your CowAgent instance.
|
||||
|
||||
Commands:
|
||||
help Show this message.
|
||||
version Show the version.
|
||||
start Start CowAgent.
|
||||
stop Stop CowAgent.
|
||||
restart Restart CowAgent.
|
||||
status Show CowAgent running status.
|
||||
logs View CowAgent logs.
|
||||
context View or manage conversation context.
|
||||
skill Manage CowAgent skills.
|
||||
|
||||
Tip: You can also send /help, /skill list, etc. in chat."""
|
||||
|
||||
|
||||
class CowCLI(click.Group):
|
||||
|
||||
def format_help(self, ctx, formatter):
|
||||
formatter.write(HELP_TEXT.strip())
|
||||
formatter.write("\n")
|
||||
|
||||
def parse_args(self, ctx, args):
|
||||
if args and args[0] == 'help':
|
||||
click.echo(HELP_TEXT.strip())
|
||||
ctx.exit(0)
|
||||
return super().parse_args(ctx, args)
|
||||
|
||||
|
||||
@click.group(cls=CowCLI, invoke_without_command=True, context_settings=dict(help_option_names=[]))
|
||||
@click.pass_context
|
||||
def main(ctx):
|
||||
"""CowAgent CLI - Manage your CowAgent instance."""
|
||||
pass
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo(HELP_TEXT.strip())
|
||||
|
||||
|
||||
@main.command()
|
||||
def version():
|
||||
"""Show the version."""
|
||||
click.echo(f"cow {__version__}")
|
||||
|
||||
|
||||
@main.command(name='help')
|
||||
@click.pass_context
|
||||
def help_cmd(ctx):
|
||||
"""Show this message."""
|
||||
click.echo(HELP_TEXT.strip())
|
||||
|
||||
|
||||
main.add_command(skill)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""cow context - Context management commands."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob as glob_mod
|
||||
|
||||
import click
|
||||
|
||||
from cli.utils import get_workspace_dir
|
||||
|
||||
CHAT_HINT = (
|
||||
"Context commands operate on the running agent's memory.\n"
|
||||
"Please send the command in a chat conversation instead:\n\n"
|
||||
" /context - View current context info\n"
|
||||
" /context clear - Clear conversation context"
|
||||
)
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True)
|
||||
@@ -15,67 +16,14 @@ from cli.utils import get_workspace_dir
|
||||
def context(ctx):
|
||||
"""View or manage conversation context.
|
||||
|
||||
Without a subcommand, shows context info for the current workspace.
|
||||
Context commands need access to the running agent's memory.
|
||||
Use them in chat conversations: /context or /context clear
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
_show_context_info()
|
||||
click.echo(f"\n {CHAT_HINT}\n")
|
||||
|
||||
|
||||
@context.command()
|
||||
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
||||
def clear(yes):
|
||||
def clear():
|
||||
"""Clear conversation context (messages history)."""
|
||||
workspace = get_workspace_dir()
|
||||
sessions_dir = os.path.join(workspace, "sessions")
|
||||
|
||||
if not os.path.isdir(sessions_dir):
|
||||
click.echo("No conversation data found.")
|
||||
return
|
||||
|
||||
db_files = glob_mod.glob(os.path.join(sessions_dir, "*.db"))
|
||||
if not db_files:
|
||||
click.echo("No conversation data found.")
|
||||
return
|
||||
|
||||
if not yes:
|
||||
click.confirm("Clear all conversation context? This cannot be undone.", abort=True)
|
||||
|
||||
removed = 0
|
||||
for db_file in db_files:
|
||||
try:
|
||||
os.remove(db_file)
|
||||
removed += 1
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Failed to remove {db_file}: {e}", err=True)
|
||||
|
||||
click.echo(click.style(f"✓ Cleared {removed} conversation database(s).", fg="green"))
|
||||
|
||||
|
||||
def _show_context_info():
|
||||
"""Display conversation context status."""
|
||||
workspace = get_workspace_dir()
|
||||
sessions_dir = os.path.join(workspace, "sessions")
|
||||
|
||||
click.echo(f"\n Context info")
|
||||
click.echo(f" Workspace: {workspace}")
|
||||
|
||||
if not os.path.isdir(sessions_dir):
|
||||
click.echo(" Sessions: none\n")
|
||||
return
|
||||
|
||||
db_files = glob_mod.glob(os.path.join(sessions_dir, "*.db"))
|
||||
total_size = sum(os.path.getsize(f) for f in db_files if os.path.exists(f))
|
||||
|
||||
click.echo(f" Sessions dir: {sessions_dir}")
|
||||
click.echo(f" Database files: {len(db_files)}")
|
||||
click.echo(f" Total size: {_format_size(total_size)}")
|
||||
click.echo(f"\n Use 'cow context clear' to reset.\n")
|
||||
|
||||
|
||||
def _format_size(size_bytes):
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
click.echo(f"\n {CHAT_HINT}\n")
|
||||
|
||||
@@ -47,7 +47,8 @@ def _remove_pid():
|
||||
|
||||
@click.command()
|
||||
@click.option("--foreground", "-f", is_flag=True, help="Run in foreground (don't daemonize)")
|
||||
def start(foreground):
|
||||
@click.option("--no-logs", is_flag=True, help="Don't tail logs after starting")
|
||||
def start(foreground, no_logs):
|
||||
"""Start CowAgent."""
|
||||
pid = _read_pid()
|
||||
if pid:
|
||||
@@ -81,6 +82,10 @@ def start(foreground):
|
||||
click.echo(click.style(f"✓ CowAgent started (PID: {proc.pid})", fg="green"))
|
||||
click.echo(f" Logs: {log_file}")
|
||||
|
||||
if not no_logs:
|
||||
click.echo(" Press Ctrl+C to stop tailing logs.\n")
|
||||
_tail_log(log_file)
|
||||
|
||||
|
||||
@click.command()
|
||||
def stop():
|
||||
@@ -109,23 +114,39 @@ def stop():
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--no-logs", is_flag=True, help="Don't tail logs after restarting")
|
||||
@click.pass_context
|
||||
def restart(ctx):
|
||||
def restart(ctx, no_logs):
|
||||
"""Restart CowAgent."""
|
||||
ctx.invoke(stop)
|
||||
time.sleep(1)
|
||||
ctx.invoke(start)
|
||||
ctx.invoke(start, no_logs=no_logs)
|
||||
|
||||
|
||||
@click.command()
|
||||
def status():
|
||||
"""Show CowAgent running status."""
|
||||
from cli import __version__
|
||||
from cli.utils import load_config_json
|
||||
|
||||
pid = _read_pid()
|
||||
if pid:
|
||||
click.echo(click.style(f"● CowAgent is running (PID: {pid})", fg="green"))
|
||||
else:
|
||||
click.echo(click.style("● CowAgent is not running", fg="red"))
|
||||
|
||||
click.echo(f" 版本: v{__version__}")
|
||||
|
||||
cfg = load_config_json()
|
||||
if cfg:
|
||||
channel = cfg.get("channel_type", "unknown")
|
||||
if isinstance(channel, list):
|
||||
channel = ", ".join(channel)
|
||||
click.echo(f" 通道: {channel}")
|
||||
click.echo(f" 模型: {cfg.get('model', 'unknown')}")
|
||||
mode = "Agent" if cfg.get("agent") else "Chat"
|
||||
click.echo(f" 模式: {mode}")
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--follow", "-f", is_flag=True, help="Follow log output")
|
||||
@@ -138,18 +159,23 @@ def logs(follow, lines):
|
||||
return
|
||||
|
||||
if follow:
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
["tail", "-f", "-n", str(lines), log_file],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
)
|
||||
proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
_tail_log(log_file, lines)
|
||||
else:
|
||||
proc = subprocess.run(
|
||||
subprocess.run(
|
||||
["tail", "-n", str(lines), log_file],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def _tail_log(log_file: str, lines: int = 50):
|
||||
"""Follow log file output. Blocks until Ctrl+C."""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
["tail", "-f", "-n", str(lines), log_file],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
)
|
||||
proc.wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
@@ -29,11 +29,12 @@ def skill():
|
||||
# cow skill list
|
||||
# ------------------------------------------------------------------
|
||||
@skill.command("list")
|
||||
@click.option("--remote", is_flag=True, help="List skills available on Skill Hub")
|
||||
def skill_list(remote):
|
||||
"""List installed skills or browse remote Skill Hub."""
|
||||
@click.option("--remote", is_flag=True, help="Browse skills on Skill Hub")
|
||||
@click.option("--page", default=1, type=int, help="Page number for remote listing")
|
||||
def skill_list(remote, page):
|
||||
"""List installed skills or browse Skill Hub."""
|
||||
if remote:
|
||||
_list_remote()
|
||||
_list_remote(page=page)
|
||||
else:
|
||||
_list_local()
|
||||
|
||||
@@ -95,10 +96,17 @@ def _print_skill_table(entries):
|
||||
click.echo()
|
||||
|
||||
|
||||
def _list_remote():
|
||||
"""List skills from remote Skill Hub."""
|
||||
_REMOTE_PAGE_SIZE = 10
|
||||
|
||||
|
||||
def _list_remote(page: int = 1):
|
||||
"""List skills from remote Skill Hub with server-side pagination."""
|
||||
try:
|
||||
resp = requests.get(f"{SKILL_HUB_API}/skills", timeout=10)
|
||||
resp = requests.get(
|
||||
f"{SKILL_HUB_API}/skills",
|
||||
params={"page": page, "limit": _REMOTE_PAGE_SIZE},
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
@@ -106,26 +114,40 @@ def _list_remote():
|
||||
sys.exit(1)
|
||||
|
||||
skills = data.get("skills", [])
|
||||
if not skills:
|
||||
total = data.get("total", len(skills))
|
||||
|
||||
if not skills and page == 1:
|
||||
click.echo("No skills available on Skill Hub.")
|
||||
return
|
||||
|
||||
name_w = max(len(s.get("name", "")) for s in skills)
|
||||
total_pages = max(1, (total + _REMOTE_PAGE_SIZE - 1) // _REMOTE_PAGE_SIZE)
|
||||
page = min(page, total_pages)
|
||||
installed = set(load_skills_config().keys())
|
||||
|
||||
name_w = max((len(s.get("name", "")) for s in skills), default=4)
|
||||
name_w = max(name_w, 4) + 2
|
||||
|
||||
click.echo(f"\n Skill Hub ({len(skills)} available)\n")
|
||||
click.echo(f" {'Name':<{name_w}} {'Downloads':<12} {'Description'}")
|
||||
click.echo(f"\n Skill Hub ({total} available) — page {page}/{total_pages}\n")
|
||||
click.echo(f" {'Name':<{name_w}} {'Status':<12} {'Description'}")
|
||||
click.echo(f" {'─' * (name_w + 12 + 50)}")
|
||||
|
||||
for s in skills:
|
||||
name = s.get("name", "")
|
||||
downloads = s.get("downloads", 0)
|
||||
desc = s.get("description", "") or s.get("display_name", "")
|
||||
if len(desc) > 50:
|
||||
desc = desc[:47] + "..."
|
||||
click.echo(f" {name:<{name_w}} {downloads:<12} {desc}")
|
||||
status = click.style("installed", fg="green") if name in installed else "—"
|
||||
click.echo(f" {name:<{name_w}} {status:<12} {desc}")
|
||||
|
||||
click.echo(f"\n Install with: cow skill install <name>\n")
|
||||
click.echo()
|
||||
nav_parts = []
|
||||
if page > 1:
|
||||
nav_parts.append(f"cow skill list --remote --page {page - 1}")
|
||||
if page < total_pages:
|
||||
nav_parts.append(f"cow skill list --remote --page {page + 1}")
|
||||
if nav_parts:
|
||||
click.echo(f" Navigate: {' | '.join(nav_parts)}")
|
||||
click.echo(f" Install: cow skill install <name>\n")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -148,20 +170,21 @@ def search(query):
|
||||
click.echo(f'No skills found for "{query}".')
|
||||
return
|
||||
|
||||
installed = set(load_skills_config().keys())
|
||||
name_w = max(len(s.get("name", "")) for s in skills)
|
||||
name_w = max(name_w, 4) + 2
|
||||
|
||||
click.echo(f'\n Search results for "{query}" ({len(skills)} found)\n')
|
||||
click.echo(f" {'Name':<{name_w}} {'Downloads':<12} {'Description'}")
|
||||
click.echo(f" {'Name':<{name_w}} {'Status':<12} {'Description'}")
|
||||
click.echo(f" {'─' * (name_w + 12 + 50)}")
|
||||
|
||||
for s in skills:
|
||||
name = s.get("name", "")
|
||||
downloads = s.get("downloads", 0)
|
||||
desc = s.get("description", "") or s.get("display_name", "")
|
||||
if len(desc) > 50:
|
||||
desc = desc[:47] + "..."
|
||||
click.echo(f" {name:<{name_w}} {downloads:<12} {desc}")
|
||||
status = click.style("installed", fg="green") if name in installed else "—"
|
||||
click.echo(f" {name:<{name_w}} {status:<12} {desc}")
|
||||
|
||||
click.echo(f"\n Install with: cow skill install <name>\n")
|
||||
|
||||
|
||||
@@ -59,4 +59,4 @@ def ensure_sys_path():
|
||||
sys.path.insert(0, root)
|
||||
|
||||
|
||||
SKILL_HUB_API = "https://cow-skill-hub.pages.dev/api"
|
||||
SKILL_HUB_API = "https://skills.cowagent.ai/api"
|
||||
|
||||
Reference in New Issue
Block a user