agent-notes 2.0.4__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_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Build agent configuration files from source."""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..config import (
|
|
8
|
+
AGENTS_YAML,
|
|
9
|
+
DIST_DIR,
|
|
10
|
+
info
|
|
11
|
+
)
|
|
12
|
+
from ..services.rendering import generate_agent_files, render_globals, load_agents_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Re-export for backward compatibility
|
|
16
|
+
def _load_frontmatter_template(template_name):
|
|
17
|
+
"""DEPRECATED: Use services.rendering._load_frontmatter_template instead."""
|
|
18
|
+
from ..services.rendering import _load_frontmatter_template
|
|
19
|
+
return _load_frontmatter_template(template_name)
|
|
20
|
+
def copy_global_files() -> list[Path]:
|
|
21
|
+
"""Copy global files and rules to destination."""
|
|
22
|
+
from ..config import RULES_DIR, DIST_RULES_DIR
|
|
23
|
+
|
|
24
|
+
copied_files = []
|
|
25
|
+
|
|
26
|
+
# Use the rendering service for global files
|
|
27
|
+
copied_files.extend(render_globals())
|
|
28
|
+
|
|
29
|
+
# Copy all rules files
|
|
30
|
+
if RULES_DIR.exists():
|
|
31
|
+
# Build rule files
|
|
32
|
+
DIST_RULES_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
for rule_file in RULES_DIR.glob('*.md'):
|
|
34
|
+
dest_file = DIST_RULES_DIR / rule_file.name
|
|
35
|
+
shutil.copy2(rule_file, dest_file)
|
|
36
|
+
copied_files.append(dest_file)
|
|
37
|
+
|
|
38
|
+
return copied_files
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def copy_skills() -> list[Path]:
|
|
42
|
+
"""Copy skill directories to dist/skills/."""
|
|
43
|
+
from ..config import find_skill_dirs
|
|
44
|
+
|
|
45
|
+
dist_skills = DIST_DIR / "skills"
|
|
46
|
+
# Clean and recreate
|
|
47
|
+
if dist_skills.exists():
|
|
48
|
+
shutil.rmtree(dist_skills)
|
|
49
|
+
dist_skills.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
copied = []
|
|
52
|
+
for skill_dir in find_skill_dirs():
|
|
53
|
+
dest = dist_skills / skill_dir.name
|
|
54
|
+
shutil.copytree(skill_dir, dest)
|
|
55
|
+
copied.append(dest)
|
|
56
|
+
return copied
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def copy_scripts() -> list[Path]:
|
|
60
|
+
"""Copy script files to dist/scripts/, embedding pricing data from pricing.yaml."""
|
|
61
|
+
import json
|
|
62
|
+
from ..config import SCRIPTS_DIR, DIST_SCRIPTS_DIR, DATA_DIR
|
|
63
|
+
|
|
64
|
+
if not SCRIPTS_DIR.exists():
|
|
65
|
+
return []
|
|
66
|
+
if DIST_SCRIPTS_DIR.exists():
|
|
67
|
+
shutil.rmtree(DIST_SCRIPTS_DIR)
|
|
68
|
+
DIST_SCRIPTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
pricing_path = DATA_DIR / "pricing.yaml"
|
|
71
|
+
pricing_json = "null"
|
|
72
|
+
if pricing_path.exists():
|
|
73
|
+
pricing_json = json.dumps(yaml.safe_load(pricing_path.read_text()), indent=2)
|
|
74
|
+
|
|
75
|
+
copied = []
|
|
76
|
+
for script in SCRIPTS_DIR.iterdir():
|
|
77
|
+
if script.is_file():
|
|
78
|
+
content = script.read_text()
|
|
79
|
+
if "{{PRICING}}" in content:
|
|
80
|
+
content = content.replace("{{PRICING}}", pricing_json)
|
|
81
|
+
dest = DIST_SCRIPTS_DIR / script.name
|
|
82
|
+
dest.write_text(content)
|
|
83
|
+
dest.chmod(0o755)
|
|
84
|
+
copied.append(dest)
|
|
85
|
+
return copied
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def copy_commands() -> list[Path]:
|
|
89
|
+
"""Copy command files from data/commands/ to dist/claude/commands/."""
|
|
90
|
+
from ..config import DATA_DIR, DIST_DIR
|
|
91
|
+
src = DATA_DIR / "commands"
|
|
92
|
+
if not src.exists():
|
|
93
|
+
return []
|
|
94
|
+
dest = DIST_DIR / "claude" / "commands"
|
|
95
|
+
if dest.exists():
|
|
96
|
+
shutil.rmtree(dest)
|
|
97
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
copied = []
|
|
99
|
+
for f in src.glob("*.md"):
|
|
100
|
+
out = dest / f.name
|
|
101
|
+
shutil.copy2(f, out)
|
|
102
|
+
copied.append(out)
|
|
103
|
+
return copied
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def count_lines(file_path: Path) -> int:
|
|
107
|
+
"""Count lines in a file or all files within a directory."""
|
|
108
|
+
try:
|
|
109
|
+
if file_path.is_dir():
|
|
110
|
+
return sum(
|
|
111
|
+
len(f.read_text().splitlines())
|
|
112
|
+
for f in file_path.rglob("*")
|
|
113
|
+
if f.is_file()
|
|
114
|
+
)
|
|
115
|
+
return len(file_path.read_text().splitlines())
|
|
116
|
+
except Exception:
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build() -> None:
|
|
121
|
+
"""Build agent configuration files from source."""
|
|
122
|
+
from .. import state as state_module
|
|
123
|
+
from ..config import ROOT
|
|
124
|
+
|
|
125
|
+
# Read configuration
|
|
126
|
+
try:
|
|
127
|
+
agents_config, tiers = load_agents_config()
|
|
128
|
+
except FileNotFoundError as e:
|
|
129
|
+
print(f"Error: {e}")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Load state if present (no error if missing)
|
|
133
|
+
state = state_module.load()
|
|
134
|
+
|
|
135
|
+
# Generate agent files (state=None is backward compatible)
|
|
136
|
+
print("Generating agent files...")
|
|
137
|
+
agent_files = generate_agent_files(agents_config, tiers, state=state)
|
|
138
|
+
|
|
139
|
+
# Copy global files
|
|
140
|
+
print("Copying global files...")
|
|
141
|
+
global_files = copy_global_files()
|
|
142
|
+
|
|
143
|
+
# Copy skills
|
|
144
|
+
print("Copying skills...")
|
|
145
|
+
skill_files = copy_skills()
|
|
146
|
+
|
|
147
|
+
# Copy scripts
|
|
148
|
+
print("Copying scripts...")
|
|
149
|
+
script_files = copy_scripts()
|
|
150
|
+
|
|
151
|
+
# Copy commands
|
|
152
|
+
print("Copying commands...")
|
|
153
|
+
command_files = copy_commands()
|
|
154
|
+
|
|
155
|
+
# Report results
|
|
156
|
+
all_files = agent_files + global_files + skill_files + script_files + command_files
|
|
157
|
+
print(f"\nGenerated {len(all_files)} files:")
|
|
158
|
+
|
|
159
|
+
total_lines = 0
|
|
160
|
+
for file_path in sorted(all_files):
|
|
161
|
+
rel_path = file_path.relative_to(ROOT)
|
|
162
|
+
lines = count_lines(file_path)
|
|
163
|
+
total_lines += lines
|
|
164
|
+
print(f" {rel_path} ({lines} lines)")
|
|
165
|
+
|
|
166
|
+
print(f"\nTotal: {total_lines} lines across {len(all_files)} files")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == '__main__':
|
|
170
|
+
build()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Health check for agent-notes installation."""
|
|
2
|
+
|
|
3
|
+
# Re-export for backward compatibility. New code should import from agent_notes.domain.
|
|
4
|
+
from ..domain.diagnostics import Issue, FixAction # noqa: F401
|
|
5
|
+
|
|
6
|
+
# Re-export config constants that tests mock
|
|
7
|
+
from ..config import (
|
|
8
|
+
ROOT, DIST_CLAUDE_DIR, DIST_OPENCODE_DIR, DIST_GITHUB_DIR, DIST_RULES_DIR,
|
|
9
|
+
DIST_SKILLS_DIR, DIST_SCRIPTS_DIR, SCRIPTS_DIR, BIN_HOME,
|
|
10
|
+
CLAUDE_HOME, OPENCODE_HOME, GITHUB_HOME, AGENTS_HOME,
|
|
11
|
+
Color, info, issue, ok, warn, fail
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Re-export all diagnostic functions from services
|
|
15
|
+
from ..services.diagnostics import (
|
|
16
|
+
check_stale_files,
|
|
17
|
+
check_broken_symlinks,
|
|
18
|
+
check_shadowed_files,
|
|
19
|
+
check_missing_files,
|
|
20
|
+
check_content_drift,
|
|
21
|
+
check_build_freshness,
|
|
22
|
+
count_stale,
|
|
23
|
+
print_summary,
|
|
24
|
+
print_issues,
|
|
25
|
+
do_fix,
|
|
26
|
+
_check_role_models,
|
|
27
|
+
_find_dist_source,
|
|
28
|
+
_count_agents,
|
|
29
|
+
_count_skills,
|
|
30
|
+
_count_scripts,
|
|
31
|
+
_count_rules,
|
|
32
|
+
_check_config,
|
|
33
|
+
_cli_base_dir,
|
|
34
|
+
_print_status
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Re-export filesystem utilities from services/fs for backward compatibility
|
|
38
|
+
from ..services.fs import (
|
|
39
|
+
resolve_symlink,
|
|
40
|
+
symlink_target_exists,
|
|
41
|
+
files_differ
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _check_session_hook(scope: str, issues: list) -> None:
|
|
45
|
+
"""Check that the Claude Code SessionStart hook is registered in settings.json."""
|
|
46
|
+
from ..services.settings_writer import has_hook
|
|
47
|
+
from ..services.installer import _session_hook_paths
|
|
48
|
+
from ..domain.diagnostics import Issue
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
from ..registries.cli_registry import load_registry
|
|
52
|
+
registry = load_registry()
|
|
53
|
+
claude_backend = registry.get("claude")
|
|
54
|
+
except KeyError:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
settings_path, _context_file, hook_command = _session_hook_paths(claude_backend, scope)
|
|
58
|
+
if not has_hook(settings_path, "SessionStart", hook_command):
|
|
59
|
+
issues.append(Issue(
|
|
60
|
+
"missing_hook",
|
|
61
|
+
str(settings_path),
|
|
62
|
+
"SessionStart hook not found — run: agent-notes install to re-add the hook",
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def diagnose(scope: str, fix: bool = False) -> bool:
|
|
67
|
+
"""Run all diagnostic checks and optionally apply fixes."""
|
|
68
|
+
from .. import install_state
|
|
69
|
+
|
|
70
|
+
print_summary(scope)
|
|
71
|
+
|
|
72
|
+
issues = []
|
|
73
|
+
fix_actions = []
|
|
74
|
+
|
|
75
|
+
# Run checks
|
|
76
|
+
check_stale_files(scope, issues, fix_actions)
|
|
77
|
+
check_broken_symlinks(scope, issues, fix_actions)
|
|
78
|
+
check_shadowed_files(scope, issues, fix_actions)
|
|
79
|
+
check_missing_files(scope, issues, fix_actions)
|
|
80
|
+
check_content_drift(scope, issues, fix_actions)
|
|
81
|
+
|
|
82
|
+
# Build freshness check (scope-independent)
|
|
83
|
+
check_build_freshness(issues, fix_actions)
|
|
84
|
+
|
|
85
|
+
# SessionStart hook check (Claude Code only)
|
|
86
|
+
_check_session_hook(scope, issues)
|
|
87
|
+
|
|
88
|
+
# Print role→model assignments
|
|
89
|
+
state = install_state.load_current_state()
|
|
90
|
+
if state is not None:
|
|
91
|
+
_check_role_models(state)
|
|
92
|
+
|
|
93
|
+
# Print issues and optionally fix
|
|
94
|
+
if print_issues(issues):
|
|
95
|
+
return True # No issues
|
|
96
|
+
|
|
97
|
+
if fix:
|
|
98
|
+
result = do_fix(issues, fix_actions)
|
|
99
|
+
# Services layer flagged that an install is needed — invoke it here
|
|
100
|
+
# (commands layer), avoiding a services→commands dependency.
|
|
101
|
+
if any(a.action == "_TRIGGER_INSTALL" for a in fix_actions):
|
|
102
|
+
from ..commands.install import install
|
|
103
|
+
install()
|
|
104
|
+
return result
|
|
105
|
+
else:
|
|
106
|
+
return False # Issues found but not fixed
|
|
107
|
+
|
|
108
|
+
# Alias for backward compatibility with CLI
|
|
109
|
+
def doctor(local: bool = False, fix: bool = False):
|
|
110
|
+
"""Main doctor function called by CLI."""
|
|
111
|
+
scope = "local" if local else "global"
|
|
112
|
+
return diagnose(scope, fix)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Info command - show installation status and component counts."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .. import install_state
|
|
6
|
+
from ..config import get_version, CLAUDE_HOME, Color
|
|
7
|
+
from ._install_helpers import count_scripts, count_skills, count_agents, count_global
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def show_info() -> None:
|
|
11
|
+
"""Show installation status and component counts."""
|
|
12
|
+
from ..registries.cli_registry import load_registry
|
|
13
|
+
|
|
14
|
+
version = get_version()
|
|
15
|
+
print(f"agent-notes {version}")
|
|
16
|
+
print("")
|
|
17
|
+
print("Components:")
|
|
18
|
+
print(f" Scripts: {count_scripts()}")
|
|
19
|
+
print(f" Skills: {count_skills()}")
|
|
20
|
+
|
|
21
|
+
# Show agent counts per backend
|
|
22
|
+
registry = load_registry()
|
|
23
|
+
for backend in registry.all():
|
|
24
|
+
if backend.supports("agents"):
|
|
25
|
+
agent_count = count_agents(backend)
|
|
26
|
+
print(f" Agents ({backend.label}): {agent_count}")
|
|
27
|
+
|
|
28
|
+
print(f" Global config: {count_global()} files")
|
|
29
|
+
print("")
|
|
30
|
+
print("Install targets:")
|
|
31
|
+
print(" Scripts: ~/.local/bin/")
|
|
32
|
+
|
|
33
|
+
# Show CLI install targets
|
|
34
|
+
for backend in registry.all():
|
|
35
|
+
print(f" {backend.label}: {backend.global_home}")
|
|
36
|
+
|
|
37
|
+
print(" Universal: ~/.agents/")
|
|
38
|
+
print("")
|
|
39
|
+
|
|
40
|
+
# Check install status
|
|
41
|
+
global_ok = (CLAUDE_HOME / "agents").exists() and any((CLAUDE_HOME / "agents").iterdir())
|
|
42
|
+
local_ok = Path(".claude/agents").exists() and any(Path(".claude/agents").iterdir())
|
|
43
|
+
|
|
44
|
+
print("Status:")
|
|
45
|
+
if global_ok:
|
|
46
|
+
print(f" Global: {Color.GREEN}installed{Color.NC} (use doctor for details)")
|
|
47
|
+
else:
|
|
48
|
+
print(f" Global: {Color.YELLOW}not installed{Color.NC}")
|
|
49
|
+
if local_ok:
|
|
50
|
+
print(f" Local: {Color.GREEN}detected{Color.NC}")
|
|
51
|
+
else:
|
|
52
|
+
print(f" Local: {Color.CYAN}not detected{Color.NC}")
|
|
53
|
+
|
|
54
|
+
# State info
|
|
55
|
+
st = install_state.load_current_state()
|
|
56
|
+
if st is not None:
|
|
57
|
+
print("")
|
|
58
|
+
print("Last install:")
|
|
59
|
+
|
|
60
|
+
# Global install
|
|
61
|
+
if st.global_install:
|
|
62
|
+
gs = st.global_install
|
|
63
|
+
print(f" Global: installed {gs.installed_at}, {gs.mode}")
|
|
64
|
+
# Count backends and items
|
|
65
|
+
if gs.clis:
|
|
66
|
+
backend_summaries = []
|
|
67
|
+
for backend_name, bs in sorted(gs.clis.items()):
|
|
68
|
+
counts = []
|
|
69
|
+
for component_type, items in bs.installed.items():
|
|
70
|
+
if items:
|
|
71
|
+
counts.append(f"{len(items)} {component_type}")
|
|
72
|
+
if counts:
|
|
73
|
+
backend_summaries.append(f"{backend_name} ({', '.join(counts)})")
|
|
74
|
+
if backend_summaries:
|
|
75
|
+
print(f" Backends: {', '.join(backend_summaries)}")
|
|
76
|
+
else:
|
|
77
|
+
print(" Global: none")
|
|
78
|
+
|
|
79
|
+
# Local installs
|
|
80
|
+
if st.local_installs:
|
|
81
|
+
for project_path, ls in sorted(st.local_installs.items()):
|
|
82
|
+
print(f" Local: {project_path} (installed {ls.installed_at}, {ls.mode})")
|
|
83
|
+
if ls.clis:
|
|
84
|
+
backend_summaries = []
|
|
85
|
+
for backend_name, bs in sorted(ls.clis.items()):
|
|
86
|
+
counts = []
|
|
87
|
+
for component_type, items in bs.installed.items():
|
|
88
|
+
if items:
|
|
89
|
+
counts.append(f"{len(items)} {component_type}")
|
|
90
|
+
if counts:
|
|
91
|
+
backend_summaries.append(f"{backend_name} ({', '.join(counts)})")
|
|
92
|
+
if backend_summaries:
|
|
93
|
+
print(f" Backends: {', '.join(backend_summaries)}")
|
|
94
|
+
else:
|
|
95
|
+
print(" Local: none")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Install command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..config import Color, PKG_DIR
|
|
6
|
+
from .. import install_state
|
|
7
|
+
from ._install_helpers import _verify_install
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def install(local: bool = False, copy: bool = False, reconfigure: bool = False) -> None:
|
|
11
|
+
"""Build from source and install to targets."""
|
|
12
|
+
from ..state import get_scope, state_file
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
scope = "local" if local else "global"
|
|
16
|
+
project_path = Path.cwd().resolve() if local else None
|
|
17
|
+
|
|
18
|
+
state = install_state.load_current_state()
|
|
19
|
+
existing = get_scope(state, scope, project_path) if state else None
|
|
20
|
+
|
|
21
|
+
if existing and not reconfigure:
|
|
22
|
+
# Print existing-install summary
|
|
23
|
+
print(f"Found existing {scope} installation at {state_file()}")
|
|
24
|
+
print(f" Installed: {existing.installed_at}")
|
|
25
|
+
cli_labels = []
|
|
26
|
+
from ..registries.cli_registry import load_registry
|
|
27
|
+
registry = load_registry()
|
|
28
|
+
for cli_name in existing.clis.keys():
|
|
29
|
+
try:
|
|
30
|
+
cli_labels.append(registry.get(cli_name).label)
|
|
31
|
+
except KeyError:
|
|
32
|
+
cli_labels.append(cli_name)
|
|
33
|
+
print(f" CLIs: {', '.join(cli_labels)}")
|
|
34
|
+
print(f" Mode: {existing.mode}")
|
|
35
|
+
print()
|
|
36
|
+
print("Verifying ...")
|
|
37
|
+
# Run verification. Use doctor_checks or a new helper.
|
|
38
|
+
issues = _verify_install(existing, scope, project_path, registry)
|
|
39
|
+
if not issues:
|
|
40
|
+
print()
|
|
41
|
+
print("Installation is healthy.")
|
|
42
|
+
print()
|
|
43
|
+
print("Tip: To reinstall with different choices, run:")
|
|
44
|
+
print(" agent-notes uninstall")
|
|
45
|
+
print(" agent-notes install")
|
|
46
|
+
print()
|
|
47
|
+
print(" Or to re-run the wizard and overwrite in place:")
|
|
48
|
+
print(" agent-notes install --reconfigure")
|
|
49
|
+
else:
|
|
50
|
+
print()
|
|
51
|
+
print(f"Installation has {len(issues)} issue(s).")
|
|
52
|
+
print()
|
|
53
|
+
print("Tip: Run `agent-notes doctor --fix` to repair, or `agent-notes install --reconfigure` to rewizard.")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if existing and reconfigure:
|
|
57
|
+
print(f"Clearing existing {scope} state (--reconfigure) ...")
|
|
58
|
+
install_state.remove_install_state(scope, project_path)
|
|
59
|
+
# Fall through to normal install flow
|
|
60
|
+
|
|
61
|
+
# Validate args
|
|
62
|
+
if copy and not local:
|
|
63
|
+
print("Error: --copy is only valid with --local installs.")
|
|
64
|
+
print("Global installs always use symlinks.")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Build first
|
|
68
|
+
print("Building from source...")
|
|
69
|
+
try:
|
|
70
|
+
from ..commands.build import build
|
|
71
|
+
build()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"{Color.RED}Build failed: {e}{Color.NC}")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Execute
|
|
77
|
+
print(f"Installing ({'local' if local else 'global'}, {'copy' if copy else 'symlink'}) ...")
|
|
78
|
+
print("")
|
|
79
|
+
|
|
80
|
+
from ..services import installer
|
|
81
|
+
scope = "local" if local else "global"
|
|
82
|
+
copy_mode = copy
|
|
83
|
+
installer.install_all(scope, copy_mode)
|
|
84
|
+
|
|
85
|
+
print("")
|
|
86
|
+
print(f"{Color.GREEN}Done.{Color.NC} Restart Claude Code / OpenCode to pick up changes.")
|
|
87
|
+
|
|
88
|
+
# Record state
|
|
89
|
+
try:
|
|
90
|
+
project_path = Path.cwd() if local else None
|
|
91
|
+
st = install_state.build_install_state(
|
|
92
|
+
mode="copy" if copy else "symlink",
|
|
93
|
+
scope="local" if local else "global",
|
|
94
|
+
repo_root=PKG_DIR.parent, # repo root (parent of agent_notes pkg)
|
|
95
|
+
project_path=project_path,
|
|
96
|
+
)
|
|
97
|
+
install_state.record_install_state(st)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"{Color.YELLOW}Warning: failed to write state.json: {e}{Color.NC}")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""List installed components."""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
from ..registries import default_skill_registry, default_rule_registry
|
|
8
|
+
from ..config import Color, DATA_DIR, find_skill_dirs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_clis() -> None:
|
|
12
|
+
"""Print all registered CLIs."""
|
|
13
|
+
from ..registries.cli_registry import load_registry
|
|
14
|
+
registry = load_registry()
|
|
15
|
+
backends = sorted(registry.all(), key=lambda b: b.name)
|
|
16
|
+
print(f"CLIs ({len(backends)}):")
|
|
17
|
+
for b in backends:
|
|
18
|
+
# Pad name to ~10 chars, label to ~20 chars
|
|
19
|
+
print(f" {b.name:<10} {b.label:<25} → {b.global_home}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_models() -> None:
|
|
23
|
+
"""Print all available models with CLI compatibility."""
|
|
24
|
+
from ..registries.model_registry import load_model_registry
|
|
25
|
+
from ..registries.cli_registry import load_registry
|
|
26
|
+
models = load_model_registry().all()
|
|
27
|
+
registry = load_registry()
|
|
28
|
+
print(f"Models ({len(models)}):")
|
|
29
|
+
for m in sorted(models, key=lambda m: m.id):
|
|
30
|
+
compat = [b.name for b in registry.all() if b.first_alias_for(m.aliases) is not None]
|
|
31
|
+
label = f" {m.label}" if hasattr(m, 'label') and m.label else ""
|
|
32
|
+
model_class = f" [{m.model_class}]" if hasattr(m, 'model_class') and m.model_class else ""
|
|
33
|
+
if compat:
|
|
34
|
+
compat_str = ", ".join(compat)
|
|
35
|
+
print(f" {m.id:<22}{label:<22}{model_class:<10} compatible: {compat_str}")
|
|
36
|
+
else:
|
|
37
|
+
print(f" {m.id:<22}{label:<22}{model_class:<10} compatible: (none)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def list_roles() -> None:
|
|
41
|
+
"""Print all roles."""
|
|
42
|
+
from ..registries.role_registry import load_role_registry
|
|
43
|
+
roles = load_role_registry().all()
|
|
44
|
+
print(f"Roles ({len(roles)}):")
|
|
45
|
+
for r in sorted(roles, key=lambda r: r.name):
|
|
46
|
+
typical = f" (typical: {r.typical_class})" if hasattr(r, 'typical_class') and r.typical_class else ""
|
|
47
|
+
print(f" {r.name:<15} {r.description}{typical}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def list_agents() -> None:
|
|
51
|
+
"""List all agents with metadata from YAML."""
|
|
52
|
+
print(f"{Color.CYAN}Agents:{Color.NC}")
|
|
53
|
+
source_agents_dir = DATA_DIR / "agents"
|
|
54
|
+
agents_yaml = DATA_DIR / "agents" / "agents.yaml"
|
|
55
|
+
|
|
56
|
+
# Load agents metadata from YAML if available
|
|
57
|
+
agents_metadata: Dict[str, Dict[str, Any]] = {}
|
|
58
|
+
if agents_yaml.exists():
|
|
59
|
+
try:
|
|
60
|
+
with open(agents_yaml, 'r') as f:
|
|
61
|
+
yaml_data = yaml.safe_load(f)
|
|
62
|
+
if yaml_data and 'agents' in yaml_data:
|
|
63
|
+
agents_metadata = yaml_data['agents']
|
|
64
|
+
except (yaml.YAMLError, FileNotFoundError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if source_agents_dir.exists():
|
|
68
|
+
for f in sorted(source_agents_dir.glob("*.md")):
|
|
69
|
+
name = f.stem
|
|
70
|
+
|
|
71
|
+
if name in agents_metadata:
|
|
72
|
+
role = agents_metadata[name].get('role', agents_metadata[name].get('tier', '')) # role or fallback to tier
|
|
73
|
+
description = agents_metadata[name].get('description', '')
|
|
74
|
+
print(f" {name:<22} {Color.DIM}({role:<8}){Color.NC} {description}")
|
|
75
|
+
else:
|
|
76
|
+
print(f" {name}")
|
|
77
|
+
|
|
78
|
+
print("")
|
|
79
|
+
|
|
80
|
+
def list_skills() -> None:
|
|
81
|
+
"""List all skills."""
|
|
82
|
+
print(f"{Color.CYAN}Skills:{Color.NC}")
|
|
83
|
+
|
|
84
|
+
skill_dirs = find_skill_dirs()
|
|
85
|
+
if skill_dirs:
|
|
86
|
+
for skill_path in sorted(skill_dirs):
|
|
87
|
+
print(f" {skill_path.name}")
|
|
88
|
+
else:
|
|
89
|
+
# If no skills found via find_skill_dirs, try the registry
|
|
90
|
+
try:
|
|
91
|
+
registry = default_skill_registry()
|
|
92
|
+
skills = registry.all()
|
|
93
|
+
if skills:
|
|
94
|
+
for skill in sorted(skills, key=lambda s: s.name):
|
|
95
|
+
print(f" {skill.name}")
|
|
96
|
+
except Exception:
|
|
97
|
+
pass # Ignore registry errors
|
|
98
|
+
|
|
99
|
+
print("")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def list_rules() -> None:
|
|
103
|
+
"""List all rules and global configs."""
|
|
104
|
+
print(f"{Color.CYAN}Rules:{Color.NC}")
|
|
105
|
+
|
|
106
|
+
# Try registry first, but fall back to filesystem if needed for testing
|
|
107
|
+
try:
|
|
108
|
+
registry = default_rule_registry()
|
|
109
|
+
rules = registry.all()
|
|
110
|
+
|
|
111
|
+
# If we get an empty registry but there are files in DATA_DIR/rules, use fallback
|
|
112
|
+
if not rules:
|
|
113
|
+
source_rules_dir = DATA_DIR / "rules"
|
|
114
|
+
if source_rules_dir.exists() and list(source_rules_dir.glob("*.md")):
|
|
115
|
+
# Use fallback
|
|
116
|
+
for f in sorted(source_rules_dir.glob("*.md")):
|
|
117
|
+
print(f" {f.stem}")
|
|
118
|
+
# else: truly no rules
|
|
119
|
+
else:
|
|
120
|
+
for rule in sorted(rules, key=lambda r: r.name):
|
|
121
|
+
print(f" {rule.name}")
|
|
122
|
+
except Exception:
|
|
123
|
+
# Fallback to old behavior if registry fails
|
|
124
|
+
source_rules_dir = DATA_DIR / "rules"
|
|
125
|
+
if source_rules_dir.exists():
|
|
126
|
+
for f in sorted(source_rules_dir.glob("*.md")):
|
|
127
|
+
print(f" {f.stem}")
|
|
128
|
+
|
|
129
|
+
print("")
|
|
130
|
+
|
|
131
|
+
print(f"{Color.CYAN}Global configs:{Color.NC}")
|
|
132
|
+
from ..registries.cli_registry import load_registry
|
|
133
|
+
registry = load_registry()
|
|
134
|
+
for backend in registry.all():
|
|
135
|
+
if backend.global_template:
|
|
136
|
+
print(f" {backend.global_template}")
|
|
137
|
+
print("")
|
|
138
|
+
|
|
139
|
+
def list_all() -> None:
|
|
140
|
+
"""Show grouped summary of everything."""
|
|
141
|
+
list_clis()
|
|
142
|
+
print()
|
|
143
|
+
list_models()
|
|
144
|
+
print()
|
|
145
|
+
list_roles()
|
|
146
|
+
print()
|
|
147
|
+
list_agents()
|
|
148
|
+
list_skills()
|
|
149
|
+
list_rules()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def list_components(filter_type: str = "all") -> None:
|
|
153
|
+
"""List installed components."""
|
|
154
|
+
dispatch = {
|
|
155
|
+
"agents": list_agents,
|
|
156
|
+
"skills": list_skills,
|
|
157
|
+
"rules": list_rules,
|
|
158
|
+
"clis": list_clis,
|
|
159
|
+
"models": list_models,
|
|
160
|
+
"roles": list_roles,
|
|
161
|
+
"all": list_all,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if filter_type in dispatch:
|
|
165
|
+
dispatch[filter_type]()
|
|
166
|
+
else:
|
|
167
|
+
print(f"Unknown filter: {filter_type}")
|
|
168
|
+
print("Usage: agent-notes list [agents|skills|rules|clis|models|roles|all]")
|
|
169
|
+
exit(1)
|