agnes-the-ai-analyst/cli/lib/commands.py
minasarustamyan d9405a6888
Move marketplace plugin updates from hook to /update-agnes-plugins skill (#237)
* Move marketplace plugin updates from hook to /update-agnes-plugins skill

The SessionStart hook used to run `agnes refresh-marketplace --quiet`,
which performed a full fetch+reset+install cycle on every Claude Code
session start. That work was invisible to the user, slowed session
startup, and was unrecoverable interactively when something failed.

Split the responsibility:

- `agnes refresh-marketplace --check` is a new lightweight detector:
  `git fetch` only, compares local HEAD with remote FETCH_HEAD, emits
  a Claude Code hook JSON message pointing the user at
  `/update-agnes-plugins` when the marketplace has changes. No reset,
  no plugin install/update side effects.
- `/update-agnes-plugins` is a new slash command (installed by
  `agnes init` into `<workspace>/.claude/commands/`) that runs
  `agnes refresh-marketplace` (default chatty path). Output streams
  into the Claude Code transcript so the user sees install/update
  progress and can react to errors interactively.
- The SessionStart hook now runs `--check`. Existing workspaces
  auto-upgrade on next `agnes init` (substring marker matches both
  the old `--quiet` entry and the new `--check` one).

BREAKING: `agnes refresh-marketplace --quiet` is removed. Old hooks
calling it silent-noop after the CLI upgrade (the hook's `|| true`
swallows the unknown-flag error) until re-init rewrites them.

* Point marketplace 'Added to your stack' hint at /update-agnes-plugins

The post-install green panel on plugin and skill/agent detail pages
referenced the SessionStart auto-install path and a shell-prompt
`agnes refresh-marketplace` invocation. With the hook now being
detect-only, that copy was misleading — the actual install path is
the new slash command.

Condensed to a single instruction: "Open a new Claude Code session
and run:" followed by `/update-agnes-plugins` in a copy-chip.
JS clipboard string updated to match.

---------

Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com>
2026-05-09 21:10:39 +02:00

82 lines
3.2 KiB
Python

"""Workspace-scoped Claude Code slash-command installer.
Sibling to `cli/lib/hooks.py`. Where hooks live in
`<workspace>/.claude/settings.json`, slash commands live as one
markdown file per command in `<workspace>/.claude/commands/`. This
module installs the Agnes-managed slash commands into a workspace.
Design notes:
- Workspace-scoped (`<workspace>/.claude/commands/<name>.md`), NOT
user-home. The slash commands appear only when Claude Code opens
this workspace, matching the hook scoping in `hooks.py`.
- Idempotent: always overwrites *our* files (server-managed canonical
content, naturally evolves with the CLI version) but never touches
third-party slash commands the user (or another tool) may have
authored under `.claude/commands/`. Listing files individually
rather than wiping the directory keeps custom commands safe.
- Templates ship inside the wheel under `cli/templates/commands/`.
`pyproject.toml` declares `cli` as a hatch wheel package, so
hatchling includes the markdown bodies during the build the same
way it ships `config/agnes_workspace_template.txt`.
"""
from __future__ import annotations
import sys
from pathlib import Path
# Slash commands managed by `agnes init`. Source (template name on
# disk under `cli/templates/commands/`) → destination filename in
# `<workspace>/.claude/commands/`. Today both names match; the indirection
# keeps the door open for renaming (e.g. internal template name vs the
# `/<command>` slug exposed to Claude Code).
_MANAGED_COMMANDS: tuple[tuple[str, str], ...] = (
("update-agnes-plugins.md", "update-agnes-plugins.md"),
)
# Defensive fallback used when the bundled template is missing on disk
# (broken install, stripped-down test environment). Mirrors the
# `agnes_workspace_template.txt` fallback in `cli/commands/init.py` —
# better to write a usable stub than to crash `agnes init`.
_FALLBACK_BODY = (
"---\n"
"description: Update Agnes marketplace plugins to latest versions\n"
"---\n"
"\n"
"Run `agnes refresh-marketplace` and report the output.\n"
)
def _templates_dir() -> Path:
"""Locate the bundled-template directory.
`cli/lib/commands.py` → `cli/templates/commands/`.
Two `.parent` hops: lib/ → cli/, then descend into templates/commands/.
"""
return Path(__file__).parent.parent / "templates" / "commands"
def install_claude_commands(workspace: Path) -> None:
"""Install Agnes-managed slash commands into `<workspace>/.claude/commands/`.
Always writes (overwrites) the managed command files; never touches
other files the user may have under `.claude/commands/`. Idempotent.
"""
commands_dir = workspace / ".claude" / "commands"
commands_dir.mkdir(parents=True, exist_ok=True)
templates_dir = _templates_dir()
for source_name, dest_name in _MANAGED_COMMANDS:
source_path = templates_dir / source_name
try:
body = source_path.read_text(encoding="utf-8")
except OSError:
print(
f"Warning: bundled slash-command template "
f"{source_path} missing; writing defensive fallback.",
file=sys.stderr,
)
body = _FALLBACK_BODY
(commands_dir / dest_name).write_text(body, encoding="utf-8")