agex-cli 0.11.0__py3-none-any.whl
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.
- agent_experience/__init__.py +3 -0
- agent_experience/__main__.py +4 -0
- agent_experience/backends/__init__.py +0 -0
- agent_experience/backends/acp/__init__.py +0 -0
- agent_experience/backends/acp/probe.py +9 -0
- agent_experience/backends/capabilities/acp.yaml +7 -0
- agent_experience/backends/capabilities/claude-code.yaml +4 -0
- agent_experience/backends/capabilities/codex.yaml +7 -0
- agent_experience/backends/capabilities/copilot.yaml +7 -0
- agent_experience/backends/claude_code/__init__.py +0 -0
- agent_experience/backends/claude_code/probe.py +97 -0
- agent_experience/backends/codex/__init__.py +0 -0
- agent_experience/backends/codex/probe.py +16 -0
- agent_experience/backends/copilot/__init__.py +0 -0
- agent_experience/backends/copilot/probe.py +9 -0
- agent_experience/cli.py +170 -0
- agent_experience/commands/__init__.py +0 -0
- agent_experience/commands/explain/SKILL.md +26 -0
- agent_experience/commands/explain/__init__.py +0 -0
- agent_experience/commands/explain/assets/topics/agex.md +35 -0
- agent_experience/commands/explain/references/.gitkeep +0 -0
- agent_experience/commands/explain/scripts/__init__.py +0 -0
- agent_experience/commands/explain/scripts/explain.py +63 -0
- agent_experience/commands/gamify/SKILL.md +31 -0
- agent_experience/commands/gamify/__init__.py +0 -0
- agent_experience/commands/gamify/assets/hooks/claude-code.json +28 -0
- agent_experience/commands/gamify/references/.gitkeep +0 -0
- agent_experience/commands/gamify/scripts/__init__.py +0 -0
- agent_experience/commands/gamify/scripts/install.py +196 -0
- agent_experience/commands/hook/SKILL.md +31 -0
- agent_experience/commands/hook/__init__.py +0 -0
- agent_experience/commands/hook/assets/table.md.j2 +17 -0
- agent_experience/commands/hook/references/.gitkeep +0 -0
- agent_experience/commands/hook/scripts/__init__.py +0 -0
- agent_experience/commands/hook/scripts/read.py +38 -0
- agent_experience/commands/hook/scripts/write.py +24 -0
- agent_experience/commands/learn/SKILL.md +21 -0
- agent_experience/commands/learn/__init__.py +0 -0
- agent_experience/commands/learn/assets/menu.md.j2 +7 -0
- agent_experience/commands/learn/assets/topics/gamify/SKILL.md +35 -0
- agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/introspect/SKILL.md +41 -0
- agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/levelup/SKILL.md +31 -0
- agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/visualize/SKILL.md +27 -0
- agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +19 -0
- agent_experience/commands/learn/references/.gitkeep +0 -0
- agent_experience/commands/learn/scripts/__init__.py +0 -0
- agent_experience/commands/learn/scripts/learn.py +72 -0
- agent_experience/commands/overview/SKILL.md +31 -0
- agent_experience/commands/overview/__init__.py +0 -0
- agent_experience/commands/overview/assets/backends/acp.yaml +7 -0
- agent_experience/commands/overview/assets/backends/claude-code.yaml +7 -0
- agent_experience/commands/overview/assets/backends/codex.yaml +7 -0
- agent_experience/commands/overview/assets/backends/copilot.yaml +7 -0
- agent_experience/commands/overview/assets/sections.md.j2 +52 -0
- agent_experience/commands/overview/references/.gitkeep +0 -0
- agent_experience/commands/overview/scripts/__init__.py +0 -0
- agent_experience/commands/overview/scripts/overview.py +40 -0
- agent_experience/core/__init__.py +0 -0
- agent_experience/core/backend.py +16 -0
- agent_experience/core/capabilities.py +44 -0
- agent_experience/core/config.py +42 -0
- agent_experience/core/hook_io.py +95 -0
- agent_experience/core/paths.py +26 -0
- agent_experience/core/render.py +27 -0
- agent_experience/core/skill_loader.py +36 -0
- agex_cli-0.11.0.dist-info/METADATA +56 -0
- agex_cli-0.11.0.dist-info/RECORD +73 -0
- agex_cli-0.11.0.dist-info/WHEEL +4 -0
- agex_cli-0.11.0.dist-info/entry_points.txt +2 -0
- agex_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from agent_experience.backends.claude_code.probe import ProbeResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def probe(project_dir: Path) -> ProbeResult:
|
|
7
|
+
"""Stub ACP probe — v0.1 returns empty. Full discovery tracked as open issue."""
|
|
8
|
+
del project_dir # accepted for signature parity with other probes; real discovery deferred
|
|
9
|
+
return ProbeResult()
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from agent_experience.core.skill_loader import load_skill
|
|
9
|
+
|
|
10
|
+
_CLAUDE_DIR = ".claude"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ProbeResult:
|
|
15
|
+
skills: list[dict[str, Any]] = field(default_factory=list)
|
|
16
|
+
hooks: list[dict[str, Any]] = field(default_factory=list)
|
|
17
|
+
agents: list[dict[str, Any]] = field(default_factory=list)
|
|
18
|
+
mcp_servers: list[dict[str, Any]] = field(default_factory=list)
|
|
19
|
+
claude_md: Path | None = None
|
|
20
|
+
settings: dict[str, Any] | None = None
|
|
21
|
+
warnings: list[str] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_skill(path: Path) -> tuple[dict[str, Any] | None, str | None]:
|
|
25
|
+
try:
|
|
26
|
+
skill = load_skill(path)
|
|
27
|
+
except (ValueError, OSError, yaml.YAMLError) as e:
|
|
28
|
+
return None, str(e)
|
|
29
|
+
return (
|
|
30
|
+
{
|
|
31
|
+
"name": skill.name,
|
|
32
|
+
"description": skill.description,
|
|
33
|
+
"path": str(path),
|
|
34
|
+
},
|
|
35
|
+
None,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _probe_settings(claude_dir: Path, result: ProbeResult) -> None:
|
|
40
|
+
settings = claude_dir / "settings.json"
|
|
41
|
+
if not settings.exists():
|
|
42
|
+
return
|
|
43
|
+
try:
|
|
44
|
+
result.settings = json.loads(settings.read_text(encoding="utf-8"))
|
|
45
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
46
|
+
result.warnings.append(f"could not parse {settings}: {e}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _probe_skills(claude_dir: Path, result: ProbeResult) -> None:
|
|
50
|
+
skills_dir = claude_dir / "skills"
|
|
51
|
+
if not skills_dir.is_dir():
|
|
52
|
+
return
|
|
53
|
+
# Sort for deterministic snapshot ordering across platforms / filesystems.
|
|
54
|
+
for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
|
|
55
|
+
parsed, err = _read_skill(skill_md)
|
|
56
|
+
if parsed is not None:
|
|
57
|
+
result.skills.append(parsed)
|
|
58
|
+
else:
|
|
59
|
+
result.warnings.append(f"could not parse {skill_md}: {err}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _probe_hooks(claude_dir: Path, result: ProbeResult) -> None:
|
|
63
|
+
hooks_file = claude_dir / "hooks.json"
|
|
64
|
+
if not hooks_file.exists():
|
|
65
|
+
return
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(hooks_file.read_text(encoding="utf-8"))
|
|
68
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
69
|
+
result.warnings.append(f"could not parse {hooks_file}: {e}")
|
|
70
|
+
return
|
|
71
|
+
if not isinstance(data, dict):
|
|
72
|
+
result.warnings.append(f"could not parse {hooks_file}: expected a JSON object")
|
|
73
|
+
return
|
|
74
|
+
for event, entries in data.items():
|
|
75
|
+
if not isinstance(entries, list):
|
|
76
|
+
result.warnings.append(
|
|
77
|
+
f"could not parse {hooks_file}: expected list for event '{event}'"
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
result.hooks.append({"event": event, "entries": entries})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def probe(project_dir: Path) -> ProbeResult:
|
|
84
|
+
result = ProbeResult()
|
|
85
|
+
if not project_dir.exists():
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
claude_md = project_dir / "CLAUDE.md"
|
|
89
|
+
if claude_md.exists():
|
|
90
|
+
result.claude_md = claude_md
|
|
91
|
+
|
|
92
|
+
claude_dir = project_dir / _CLAUDE_DIR
|
|
93
|
+
_probe_settings(claude_dir, result)
|
|
94
|
+
_probe_skills(claude_dir, result)
|
|
95
|
+
_probe_hooks(claude_dir, result)
|
|
96
|
+
|
|
97
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from agent_experience.backends.claude_code.probe import ProbeResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def probe(project_dir: Path) -> ProbeResult:
|
|
7
|
+
"""Minimal Codex probe — reads AGENTS.md if present. Other discovery deferred."""
|
|
8
|
+
result = ProbeResult()
|
|
9
|
+
if not project_dir.exists():
|
|
10
|
+
return result
|
|
11
|
+
agents_md = project_dir / "AGENTS.md"
|
|
12
|
+
if agents_md.exists():
|
|
13
|
+
result.claude_md = (
|
|
14
|
+
agents_md # reusing field — rename to `project_memory` in a future cleanup
|
|
15
|
+
)
|
|
16
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from agent_experience.backends.claude_code.probe import ProbeResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def probe(project_dir: Path) -> ProbeResult:
|
|
7
|
+
"""Stub Copilot probe — v0.1 returns empty. Full discovery tracked as open issue."""
|
|
8
|
+
del project_dir # accepted for signature parity with other probes; real discovery deferred
|
|
9
|
+
return ProbeResult()
|
agent_experience/cli.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from agent_experience import __version__
|
|
7
|
+
from agent_experience.commands.explain.scripts import explain as explain_script
|
|
8
|
+
from agent_experience.commands.gamify.scripts import install as gamify_script
|
|
9
|
+
from agent_experience.commands.hook.scripts import read as hook_read_script
|
|
10
|
+
from agent_experience.commands.hook.scripts import write as hook_write_script
|
|
11
|
+
from agent_experience.commands.learn.scripts import learn as learn_script
|
|
12
|
+
from agent_experience.commands.overview.scripts import overview as overview_script
|
|
13
|
+
from agent_experience.core.backend import parse_backend
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="agex",
|
|
17
|
+
help="Agent-operated developer-experience CLI.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _version_callback(value: bool) -> None:
|
|
23
|
+
if value:
|
|
24
|
+
typer.echo(__version__)
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def main(
|
|
30
|
+
version: Optional[bool] = typer.Option(
|
|
31
|
+
None, "--version", callback=_version_callback, is_eager=True
|
|
32
|
+
),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Root callback — exists only to hold the --version option.
|
|
35
|
+
|
|
36
|
+
Typer invokes the eager _version_callback before any subcommand
|
|
37
|
+
dispatch; there is nothing else to do at the app level.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("explain")
|
|
42
|
+
def explain(topic: str = typer.Argument(..., help="Topic to explain.")) -> None:
|
|
43
|
+
stdout, exit_code, stderr = explain_script.run(topic)
|
|
44
|
+
if stdout:
|
|
45
|
+
typer.echo(stdout, nl=False)
|
|
46
|
+
if stderr:
|
|
47
|
+
typer.echo(stderr, err=True)
|
|
48
|
+
if exit_code != 0:
|
|
49
|
+
raise typer.Exit(code=exit_code)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _agent_option() -> Any:
|
|
53
|
+
return typer.Option(..., "--agent", help="Backend: claude-code, codex, copilot, or acp.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
hook_app = typer.Typer(help="Write and read agex tracking events.", no_args_is_help=True)
|
|
57
|
+
app.add_typer(hook_app, name="hook")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@hook_app.command("write")
|
|
61
|
+
def hook_write(
|
|
62
|
+
event: str = typer.Argument(..., help="Event name (e.g., post-tool-use)."),
|
|
63
|
+
args: list[str] = typer.Argument(None, help="Additional key=value pairs."),
|
|
64
|
+
) -> None:
|
|
65
|
+
args = args or []
|
|
66
|
+
_, exit_code, stderr = hook_write_script.run(event, args)
|
|
67
|
+
if stderr:
|
|
68
|
+
typer.echo(stderr, err=True)
|
|
69
|
+
if exit_code != 0:
|
|
70
|
+
raise typer.Exit(code=exit_code)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@hook_app.command("read")
|
|
74
|
+
def hook_read(agent: str = _agent_option()) -> None:
|
|
75
|
+
try:
|
|
76
|
+
backend = parse_backend(agent)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
typer.echo(f"agex: error: {e}", err=True)
|
|
79
|
+
raise typer.Exit(code=2)
|
|
80
|
+
stdout, exit_code, stderr = hook_read_script.run(backend)
|
|
81
|
+
if stdout:
|
|
82
|
+
typer.echo(stdout, nl=False)
|
|
83
|
+
if stderr:
|
|
84
|
+
typer.echo(stderr, err=True)
|
|
85
|
+
if exit_code != 0:
|
|
86
|
+
raise typer.Exit(code=exit_code)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("learn")
|
|
90
|
+
def learn(
|
|
91
|
+
topic: Optional[str] = typer.Argument(None, help="Lesson topic (omit for menu)."),
|
|
92
|
+
agent: str = _agent_option(),
|
|
93
|
+
) -> None:
|
|
94
|
+
try:
|
|
95
|
+
backend = parse_backend(agent)
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
typer.echo(f"agex: error: {e}", err=True)
|
|
98
|
+
raise typer.Exit(code=2)
|
|
99
|
+
if topic is None:
|
|
100
|
+
stdout, exit_code, stderr = learn_script.run_menu(backend)
|
|
101
|
+
else:
|
|
102
|
+
stdout, exit_code, stderr = learn_script.run_topic(topic, backend)
|
|
103
|
+
if stdout:
|
|
104
|
+
typer.echo(stdout, nl=False)
|
|
105
|
+
if stderr:
|
|
106
|
+
typer.echo(stderr, err=True)
|
|
107
|
+
if exit_code != 0:
|
|
108
|
+
raise typer.Exit(code=exit_code)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("gamify")
|
|
112
|
+
def gamify(
|
|
113
|
+
agent: str = _agent_option(),
|
|
114
|
+
uninstall: bool = typer.Option(False, "--uninstall", help="Reverse gamify."),
|
|
115
|
+
) -> None:
|
|
116
|
+
try:
|
|
117
|
+
backend = parse_backend(agent)
|
|
118
|
+
except ValueError as e:
|
|
119
|
+
typer.echo(f"agex: error: {e}", err=True)
|
|
120
|
+
raise typer.Exit(code=2)
|
|
121
|
+
if uninstall:
|
|
122
|
+
stdout, exit_code, stderr = gamify_script.uninstall(backend)
|
|
123
|
+
else:
|
|
124
|
+
stdout, exit_code, stderr = gamify_script.install(backend)
|
|
125
|
+
if stdout:
|
|
126
|
+
typer.echo(stdout, nl=False)
|
|
127
|
+
if stderr:
|
|
128
|
+
typer.echo(stderr, err=True)
|
|
129
|
+
if exit_code != 0:
|
|
130
|
+
raise typer.Exit(code=exit_code)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("overview")
|
|
134
|
+
def overview(agent: str = _agent_option()) -> None:
|
|
135
|
+
try:
|
|
136
|
+
backend = parse_backend(agent)
|
|
137
|
+
except ValueError as e:
|
|
138
|
+
typer.echo(f"agex: error: {e}", err=True)
|
|
139
|
+
raise typer.Exit(code=2)
|
|
140
|
+
stdout, exit_code, stderr = overview_script.run(backend)
|
|
141
|
+
if stdout:
|
|
142
|
+
typer.echo(stdout, nl=False)
|
|
143
|
+
if stderr:
|
|
144
|
+
typer.echo(stderr, err=True)
|
|
145
|
+
if exit_code != 0:
|
|
146
|
+
raise typer.Exit(code=exit_code)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Keep in sync with the @app.command / app.add_typer registrations above.
|
|
150
|
+
# If a new top-level command is added, extend this set so _main_entrypoint
|
|
151
|
+
# stops routing it to the unknown-command fallback page.
|
|
152
|
+
_KNOWN_COMMANDS = {"explain", "overview", "learn", "gamify", "hook"}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _main_entrypoint() -> None:
|
|
156
|
+
"""CLI entry point that routes unknown subcommands to ``agex explain agex``.
|
|
157
|
+
|
|
158
|
+
When the first positional argument is not a known command (and is not a
|
|
159
|
+
flag), this function prints the ``agex explain agex`` page to stdout and
|
|
160
|
+
the canonical error message to stderr, then exits with code 2. All other
|
|
161
|
+
invocations — known commands, ``--version``, ``--help``, zero-arg help —
|
|
162
|
+
fall through to the normal Typer ``app()`` dispatch unchanged.
|
|
163
|
+
"""
|
|
164
|
+
argv = sys.argv[1:]
|
|
165
|
+
if argv and not argv[0].startswith("-") and argv[0] not in _KNOWN_COMMANDS:
|
|
166
|
+
typer.echo(f"agex: error: unknown command '{argv[0]}'", err=True)
|
|
167
|
+
stdout, _, _ = explain_script.run("agex")
|
|
168
|
+
typer.echo(stdout, nl=False)
|
|
169
|
+
sys.exit(2)
|
|
170
|
+
app()
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explain
|
|
3
|
+
description: Emit markdown documentation for any agex command, lesson, or concept.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex explain <topic>`
|
|
8
|
+
|
|
9
|
+
Use this to get authoritative, deterministic documentation on an agex command, lesson, or concept without invoking a lesson or running a probe.
|
|
10
|
+
|
|
11
|
+
## How it resolves
|
|
12
|
+
|
|
13
|
+
1. `commands/<topic>/SKILL.md` (command-level, wins if present)
|
|
14
|
+
2. `commands/learn/assets/topics/<topic>/SKILL.md` (lesson-level)
|
|
15
|
+
3. `commands/explain/assets/topics/<topic>.md` (concept-level override)
|
|
16
|
+
|
|
17
|
+
First match wins.
|
|
18
|
+
|
|
19
|
+
## From your shell tool
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
agex explain overview
|
|
23
|
+
agex explain gamify
|
|
24
|
+
agex explain levelup
|
|
25
|
+
agex explain agex # self-describing page
|
|
26
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# `agex` — agent-operated developer-experience CLI
|
|
2
|
+
|
|
3
|
+
`agex` is a non-agentic Python CLI that emits deterministic per-backend markdown for autonomous agents. You (the agent) invoke it from your shell tool to learn about and configure your own runtime.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `agex overview --agent X` | Snapshot of the project's current setup for backend X. |
|
|
10
|
+
| `agex learn --agent X` | Menu of lesson topics available for backend X. |
|
|
11
|
+
| `agex learn <topic> --agent X` | Teach a lesson (e.g., introspect, visualize, gamify, levelup). |
|
|
12
|
+
| `agex gamify --agent X` | Install usage-tracking hooks (or unsupported notice). |
|
|
13
|
+
| `agex gamify --uninstall --agent X` | Reverse `gamify`. |
|
|
14
|
+
| `agex hook write <event> [...]` | Append a tracking event. Called by installed hooks. |
|
|
15
|
+
| `agex hook read --agent X` | Show tracked events as markdown + source path. |
|
|
16
|
+
| `agex explain <topic>` | You're reading this. |
|
|
17
|
+
|
|
18
|
+
## First steps
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
agex explain agex # this page
|
|
22
|
+
agex learn --agent claude-code # what can I learn for my backend?
|
|
23
|
+
agex overview --agent claude-code # what's in this project?
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Design invariants
|
|
27
|
+
|
|
28
|
+
- **Non-agentic.** Zero LLM calls inside agex. All output is deterministic.
|
|
29
|
+
- **Markdown is the universal format.** No `--json` flag.
|
|
30
|
+
- **`--agent` is required** on backend-sensitive commands.
|
|
31
|
+
- **Unsupported is success.** If your backend lacks a feature, you get a markdown notice + link to file an issue — exit code 0.
|
|
32
|
+
|
|
33
|
+
## Repo
|
|
34
|
+
|
|
35
|
+
<https://github.com/OriNachum/agex>
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
from importlib.resources.abc import Traversable
|
|
4
|
+
|
|
5
|
+
from agent_experience.core.skill_loader import Skill, load_skill
|
|
6
|
+
|
|
7
|
+
_TOPIC_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _commands_root() -> Traversable:
|
|
11
|
+
return files("agent_experience.commands")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolve_topic(topic: str) -> tuple[str, Traversable] | None:
|
|
15
|
+
"""Resolve topic per spec precedence. Returns (kind, traversable) or None.
|
|
16
|
+
|
|
17
|
+
Rejects any topic that isn't a simple slug to prevent path traversal.
|
|
18
|
+
"""
|
|
19
|
+
if not _TOPIC_RE.match(topic):
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
cmds = _commands_root()
|
|
23
|
+
|
|
24
|
+
cmd_skill = cmds.joinpath(topic, "SKILL.md")
|
|
25
|
+
if cmd_skill.is_file():
|
|
26
|
+
return ("command", cmd_skill)
|
|
27
|
+
|
|
28
|
+
lesson_skill = cmds.joinpath("learn", "assets", "topics", topic, "SKILL.md")
|
|
29
|
+
if lesson_skill.is_file():
|
|
30
|
+
return ("lesson", lesson_skill)
|
|
31
|
+
|
|
32
|
+
concept = cmds.joinpath("explain", "assets", "topics", f"{topic}.md")
|
|
33
|
+
if concept.is_file():
|
|
34
|
+
return ("concept", concept)
|
|
35
|
+
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_skill_from_traversable(trav: Traversable) -> Skill:
|
|
40
|
+
# load_skill expects a pathlib.Path; resolve via as_file when needed. Since
|
|
41
|
+
# our package resources are on a real filesystem (hatch force-include), the
|
|
42
|
+
# Traversable is a MultiplexedPath / PosixPath wrapper whose .read_text()
|
|
43
|
+
# works directly. We rebuild a Skill by parsing the body in-line to avoid
|
|
44
|
+
# Path coupling.
|
|
45
|
+
from importlib.resources import as_file
|
|
46
|
+
|
|
47
|
+
with as_file(trav) as path:
|
|
48
|
+
return load_skill(path)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(topic: str) -> tuple[str, int, str]:
|
|
52
|
+
"""Return (stdout, exit_code, stderr)."""
|
|
53
|
+
resolved = resolve_topic(topic)
|
|
54
|
+
if resolved is None:
|
|
55
|
+
agex_page = _commands_root().joinpath("explain", "assets", "topics", "agex.md")
|
|
56
|
+
body = agex_page.read_text(encoding="utf-8") if agex_page.is_file() else ""
|
|
57
|
+
return (body, 2, f"agex: error: unknown topic '{topic}'")
|
|
58
|
+
|
|
59
|
+
kind, trav = resolved
|
|
60
|
+
if kind == "concept":
|
|
61
|
+
return (trav.read_text(encoding="utf-8"), 0, "")
|
|
62
|
+
skill = _load_skill_from_traversable(trav)
|
|
63
|
+
return (skill.body, 0, "")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamify
|
|
3
|
+
description: Install or uninstall backend-native hooks that track usage via agex hook write.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex gamify --agent <backend>` / `agex gamify --uninstall --agent <backend>`
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Writes backend-native hook fragments (each tagged with a stable `agex:*` ID) that call `agex hook write <event>` on PostToolUse, UserPromptSubmit, and Stop events. Agent-authored skills (e.g., `levelup`) read the accumulated data via `agex hook read`.
|
|
12
|
+
|
|
13
|
+
## Why it's safe
|
|
14
|
+
|
|
15
|
+
- Idempotent: re-running is a no-op.
|
|
16
|
+
- Reversible: `--uninstall` removes exactly the `agex:*` fragments; user-authored hooks are untouched.
|
|
17
|
+
- Calling `agex gamify` explicitly is the confirmation — no separate prompt.
|
|
18
|
+
|
|
19
|
+
## Unsupported backends
|
|
20
|
+
|
|
21
|
+
If your backend doesn't support hooks, you get a markdown notice + issue link instead.
|
|
22
|
+
|
|
23
|
+
## From your shell tool
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agex gamify --agent claude-code
|
|
27
|
+
# ... use your runtime for a while ...
|
|
28
|
+
agex hook read --agent claude-code
|
|
29
|
+
# ... later, to undo:
|
|
30
|
+
agex gamify --uninstall --agent claude-code
|
|
31
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fragments": [
|
|
3
|
+
{
|
|
4
|
+
"id": "agex:post-tool-use",
|
|
5
|
+
"event": "PostToolUse",
|
|
6
|
+
"hook": {
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "agex hook write post-tool-use tool=\"$CLAUDE_TOOL_NAME\""
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "agex:user-prompt",
|
|
13
|
+
"event": "UserPromptSubmit",
|
|
14
|
+
"hook": {
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "agex hook write user-prompt"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "agex:stop",
|
|
21
|
+
"event": "Stop",
|
|
22
|
+
"hook": {
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "agex hook write stop"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
File without changes
|
|
File without changes
|