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.
Files changed (162) hide show
  1. agent_notes/VERSION +1 -0
  2. agent_notes/__init__.py +1 -0
  3. agent_notes/__main__.py +4 -0
  4. agent_notes/cli.py +348 -0
  5. agent_notes/commands/__init__.py +27 -0
  6. agent_notes/commands/_install_helpers.py +262 -0
  7. agent_notes/commands/build.py +170 -0
  8. agent_notes/commands/doctor.py +112 -0
  9. agent_notes/commands/info.py +95 -0
  10. agent_notes/commands/install.py +99 -0
  11. agent_notes/commands/list.py +169 -0
  12. agent_notes/commands/memory.py +430 -0
  13. agent_notes/commands/regenerate.py +152 -0
  14. agent_notes/commands/set_role.py +143 -0
  15. agent_notes/commands/uninstall.py +26 -0
  16. agent_notes/commands/update.py +169 -0
  17. agent_notes/commands/validate.py +199 -0
  18. agent_notes/commands/wizard.py +720 -0
  19. agent_notes/config.py +154 -0
  20. agent_notes/data/agents/agents.yaml +352 -0
  21. agent_notes/data/agents/analyst.md +45 -0
  22. agent_notes/data/agents/api-reviewer.md +47 -0
  23. agent_notes/data/agents/architect.md +46 -0
  24. agent_notes/data/agents/coder.md +28 -0
  25. agent_notes/data/agents/database-specialist.md +45 -0
  26. agent_notes/data/agents/debugger.md +47 -0
  27. agent_notes/data/agents/devil.md +47 -0
  28. agent_notes/data/agents/devops.md +38 -0
  29. agent_notes/data/agents/explorer.md +23 -0
  30. agent_notes/data/agents/integrations.md +44 -0
  31. agent_notes/data/agents/lead.md +216 -0
  32. agent_notes/data/agents/performance-profiler.md +44 -0
  33. agent_notes/data/agents/refactorer.md +48 -0
  34. agent_notes/data/agents/reviewer.md +44 -0
  35. agent_notes/data/agents/security-auditor.md +44 -0
  36. agent_notes/data/agents/system-auditor.md +38 -0
  37. agent_notes/data/agents/tech-writer.md +32 -0
  38. agent_notes/data/agents/test-runner.md +36 -0
  39. agent_notes/data/agents/test-writer.md +39 -0
  40. agent_notes/data/cli/claude.yaml +25 -0
  41. agent_notes/data/cli/copilot.yaml +18 -0
  42. agent_notes/data/cli/opencode.yaml +22 -0
  43. agent_notes/data/commands/brainstorm.md +8 -0
  44. agent_notes/data/commands/debug.md +9 -0
  45. agent_notes/data/commands/review.md +10 -0
  46. agent_notes/data/global-claude.md +290 -0
  47. agent_notes/data/global-copilot.md +27 -0
  48. agent_notes/data/global-opencode.md +40 -0
  49. agent_notes/data/hooks/session-context.md.tpl +19 -0
  50. agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
  51. agent_notes/data/models/claude-opus-4-1.yaml +16 -0
  52. agent_notes/data/models/claude-opus-4-5.yaml +16 -0
  53. agent_notes/data/models/claude-opus-4-6.yaml +16 -0
  54. agent_notes/data/models/claude-opus-4-7.yaml +15 -0
  55. agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
  56. agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
  57. agent_notes/data/models/claude-sonnet-4.yaml +16 -0
  58. agent_notes/data/pricing.yaml +33 -0
  59. agent_notes/data/roles/orchestrator.yaml +5 -0
  60. agent_notes/data/roles/reasoner.yaml +5 -0
  61. agent_notes/data/roles/scout.yaml +5 -0
  62. agent_notes/data/roles/worker.yaml +5 -0
  63. agent_notes/data/rules/code-quality.md +9 -0
  64. agent_notes/data/rules/safety.md +10 -0
  65. agent_notes/data/scripts/cost-report +211 -0
  66. agent_notes/data/skills/brainstorming/SKILL.md +57 -0
  67. agent_notes/data/skills/code-review/SKILL.md +64 -0
  68. agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
  69. agent_notes/data/skills/docker-compose/SKILL.md +318 -0
  70. agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
  71. agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
  72. agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
  73. agent_notes/data/skills/git/SKILL.md +87 -0
  74. agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
  75. agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
  76. agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
  77. agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
  78. agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
  79. agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
  80. agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
  81. agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
  82. agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
  83. agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
  84. agent_notes/data/skills/rails-lib/SKILL.md +101 -0
  85. agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
  86. agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
  87. agent_notes/data/skills/rails-models/SKILL.md +459 -0
  88. agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
  89. agent_notes/data/skills/rails-routes/SKILL.md +804 -0
  90. agent_notes/data/skills/rails-style/SKILL.md +538 -0
  91. agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
  92. agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
  93. agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
  94. agent_notes/data/skills/rails-validations/SKILL.md +108 -0
  95. agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
  96. agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
  97. agent_notes/data/skills/rails-views/SKILL.md +413 -0
  98. agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
  99. agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
  100. agent_notes/data/skills/tdd/SKILL.md +57 -0
  101. agent_notes/data/templates/__init__.py +1 -0
  102. agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
  103. agent_notes/data/templates/frontmatter/__init__.py +1 -0
  104. agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
  105. agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
  106. agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
  107. agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
  108. agent_notes/data/templates/frontmatter/claude.py +44 -0
  109. agent_notes/data/templates/frontmatter/opencode.py +104 -0
  110. agent_notes/doctor_checks.py +189 -0
  111. agent_notes/domain/__init__.py +17 -0
  112. agent_notes/domain/agent.py +34 -0
  113. agent_notes/domain/cli_backend.py +40 -0
  114. agent_notes/domain/diagnostics.py +29 -0
  115. agent_notes/domain/diff.py +44 -0
  116. agent_notes/domain/model.py +27 -0
  117. agent_notes/domain/role.py +13 -0
  118. agent_notes/domain/rule.py +13 -0
  119. agent_notes/domain/skill.py +15 -0
  120. agent_notes/domain/state.py +46 -0
  121. agent_notes/install_state.py +11 -0
  122. agent_notes/registries/__init__.py +16 -0
  123. agent_notes/registries/_base.py +46 -0
  124. agent_notes/registries/agent_registry.py +107 -0
  125. agent_notes/registries/cli_registry.py +89 -0
  126. agent_notes/registries/model_registry.py +85 -0
  127. agent_notes/registries/role_registry.py +64 -0
  128. agent_notes/registries/rule_registry.py +80 -0
  129. agent_notes/registries/skill_registry.py +141 -0
  130. agent_notes/services/__init__.py +8 -0
  131. agent_notes/services/diagnostics/__init__.py +47 -0
  132. agent_notes/services/diagnostics/_checks.py +272 -0
  133. agent_notes/services/diagnostics/_display.py +346 -0
  134. agent_notes/services/diagnostics/_fix.py +169 -0
  135. agent_notes/services/diff.py +349 -0
  136. agent_notes/services/fs.py +195 -0
  137. agent_notes/services/install_state_builder.py +210 -0
  138. agent_notes/services/installer.py +293 -0
  139. agent_notes/services/memory_backend.py +155 -0
  140. agent_notes/services/rendering.py +329 -0
  141. agent_notes/services/session_context.py +23 -0
  142. agent_notes/services/settings_writer.py +79 -0
  143. agent_notes/services/state_store.py +249 -0
  144. agent_notes/services/ui.py +419 -0
  145. agent_notes/services/user_config.py +62 -0
  146. agent_notes/services/validation.py +67 -0
  147. agent_notes/state.py +21 -0
  148. agent_notes-2.0.4.dist-info/METADATA +14 -0
  149. agent_notes-2.0.4.dist-info/RECORD +162 -0
  150. agent_notes-2.0.4.dist-info/WHEEL +5 -0
  151. agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
  152. agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
  153. agent_notes-2.0.4.dist-info/top_level.txt +2 -0
  154. tests/conftest.py +20 -0
  155. tests/functional/__init__.py +0 -0
  156. tests/functional/test_build_commands.py +88 -0
  157. tests/functional/test_registries.py +128 -0
  158. tests/integration/__init__.py +0 -0
  159. tests/integration/test_build_output.py +129 -0
  160. tests/plugins/__init__.py +0 -0
  161. tests/plugins/test_agents.py +93 -0
  162. tests/plugins/test_skills.py +77 -0
agent_notes/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.4
@@ -0,0 +1 @@
1
+ """agent-notes — AI agent configuration manager."""
@@ -0,0 +1,4 @@
1
+ """Allow running as: python3 -m agent_notes"""
2
+ from agent_notes.cli import main
3
+
4
+ main()
agent_notes/cli.py ADDED
@@ -0,0 +1,348 @@
1
+ """CLI entry point with argument parsing."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from .config import Color
7
+
8
+
9
+ DESCRIPTION = "AgentNotes is a hub for installing AI best-practices (agents, skills, rules) across AI CLIs."
10
+
11
+ # Pre-colored usage line: "agent-notes <command> [options]"
12
+ # - "agent-notes" magenta (violet)
13
+ # - "<command>" cyan (matches the command-name color in the Commands section)
14
+ # - "[options]" green (matches the flag color)
15
+ USAGE = (
16
+ f"{Color.MAGENTA}agent-notes{Color.NC} "
17
+ f"{Color.CYAN}<command>{Color.NC} "
18
+ f"{Color.GREEN}[options]{Color.NC}"
19
+ )
20
+
21
+ # Per-command default-behavior hints shown inline next to the description.
22
+ # These describe what happens when the command is run with no flags.
23
+ COMMAND_DEFAULTS = {
24
+ "install": "default: interactive wizard",
25
+ "uninstall": "default: global",
26
+ "update": "default: pull, show diff, ask to apply",
27
+ "doctor": "default: global scope, read-only",
28
+ }
29
+
30
+ # (command, explanation) pairs — rendered with color in _build_epilog()
31
+ EXAMPLES = [
32
+ ("agent-notes install", "Interactive wizard (recommended)"),
33
+ ("agent-notes install --local", "Install into current project (Claude + OpenCode, symlinks)"),
34
+ ("agent-notes install --local --copy", "Same, but copy files (allows local edits)"),
35
+ ("agent-notes update --dry-run", "Show what would change, don't apply"),
36
+ ("agent-notes update --only agents --yes", "Apply only agent changes, no prompt"),
37
+ ("agent-notes doctor --fix", "Check and repair installation"),
38
+ ("agent-notes list agents", "List all configured agents"),
39
+ ]
40
+
41
+
42
+ def _heading(label: str) -> str:
43
+ """Section heading — no color (terminal default)."""
44
+ return label
45
+
46
+
47
+ def _colorize_command(cmd: str) -> str:
48
+ """Color a command line with the same scheme as the Usage: line.
49
+
50
+ - "agent-notes" → magenta
51
+ - tokens starting with "-" → green (flags, including their values when joined)
52
+ - other tokens → cyan (subcommand names, positional values)
53
+ """
54
+ parts = cmd.split(" ")
55
+ out = []
56
+ for tok in parts:
57
+ if tok == "agent-notes":
58
+ out.append(f"{Color.MAGENTA}{tok}{Color.NC}")
59
+ elif tok.startswith("-"):
60
+ out.append(f"{Color.GREEN}{tok}{Color.NC}")
61
+ else:
62
+ out.append(f"{Color.CYAN}{tok}{Color.NC}")
63
+ return " ".join(out)
64
+
65
+
66
+ def _build_epilog() -> str:
67
+ """Render the Examples section with ANSI colors (auto-stripped on non-TTY)."""
68
+ # Width is based on the visible (uncolored) length so columns align.
69
+ width = max(len(cmd) for cmd, _ in EXAMPLES)
70
+ lines = [_heading("Examples:")]
71
+ for cmd, note in EXAMPLES:
72
+ padding = " " * (width - len(cmd))
73
+ colored = _colorize_command(cmd)
74
+ lines.append(
75
+ f" {colored}{padding} {Color.DIM}{note}{Color.NC}"
76
+ )
77
+ return "\n".join(lines)
78
+
79
+
80
+ def _collect_flags(sub_parser: argparse.ArgumentParser) -> list[tuple[str, str, str]]:
81
+ """Extract (flag_string, help_text, default_repr) tuples from a subparser.
82
+
83
+ - Skips -h/--help.
84
+ - Joins short/long forms: `-y, --yes`.
85
+ - Positionals become `<name>` or `[{choice1|choice2}]`.
86
+ - default_repr is `(default: X)` for positionals with a non-None default, else "".
87
+ """
88
+ items: list[tuple[str, str, str]] = []
89
+ for action in sub_parser._actions:
90
+ if isinstance(action, argparse._HelpAction):
91
+ continue
92
+
93
+ default_repr = ""
94
+ if action.default is not None and action.default is not False and not isinstance(action, argparse._StoreTrueAction):
95
+ # Show default only if it is a real value (not False from store_true).
96
+ default_repr = f"(default: {action.default})"
97
+
98
+ if action.option_strings:
99
+ flag = ", ".join(action.option_strings)
100
+ # Show what value the flag accepts (choices or free-form metavar)
101
+ if action.choices:
102
+ flag = f"{flag} {{{','.join(map(str, action.choices))}}}"
103
+ elif action.nargs != 0 and not isinstance(action, argparse._StoreTrueAction) \
104
+ and not isinstance(action, argparse._StoreFalseAction) \
105
+ and not isinstance(action, argparse._StoreConstAction):
106
+ metavar = action.metavar or f"<{action.dest}>"
107
+ flag = f"{flag} {metavar}"
108
+ items.append((flag, action.help or "", default_repr))
109
+ else:
110
+ if action.choices:
111
+ name = "{" + "|".join(map(str, action.choices)) + "}"
112
+ else:
113
+ name = f"<{action.dest}>"
114
+ if action.nargs == "?":
115
+ name = f"[{name}]"
116
+ items.append((name, action.help or "", default_repr))
117
+ return items
118
+
119
+
120
+ def _render_commands_section(subparsers_action: argparse._SubParsersAction) -> str:
121
+ """Build the Commands section manually, with per-command flags inlined."""
122
+ lines = [_heading("Commands:")]
123
+ for name, sub in subparsers_action.choices.items():
124
+ help_text = ""
125
+ for action in subparsers_action._choices_actions:
126
+ if action.dest == name:
127
+ help_text = action.help or ""
128
+ break
129
+ default_hint = COMMAND_DEFAULTS.get(name, "")
130
+ suffix = f" {Color.DIM}({default_hint}){Color.NC}" if default_hint else ""
131
+ lines.append(
132
+ f" {Color.CYAN}{name:<11}{Color.NC} {help_text}{suffix}"
133
+ )
134
+ flags = _collect_flags(sub)
135
+ # Compute per-command flag column width (min 20 for visual rhythm).
136
+ flag_col = max([20, *(len(flag) for flag, _, _ in flags)])
137
+ for flag, flag_help, default_repr in flags:
138
+ default_tail = f" {Color.DIM}{default_repr}{Color.NC}" if default_repr else ""
139
+ padding = " " * (flag_col - len(flag))
140
+ lines.append(
141
+ f" {Color.GREEN}{flag}{Color.NC}{padding} "
142
+ f"{Color.DIM}{flag_help}{Color.NC}{default_tail}"
143
+ )
144
+ lines.append("")
145
+ while lines and lines[-1] == "":
146
+ lines.pop()
147
+ return "\n".join(lines)
148
+
149
+
150
+ class _AgentNotesHelp(argparse.RawDescriptionHelpFormatter):
151
+ """Help formatter: plain 'Usage:' prefix and colored global options."""
152
+
153
+ def add_usage(self, usage, actions, groups, prefix=None):
154
+ if prefix is None:
155
+ prefix = "Usage: "
156
+ return super().add_usage(usage, actions, groups, prefix)
157
+
158
+ def _format_action_invocation(self, action):
159
+ """Colorize -h/--help and -v/--version in green."""
160
+ text = super()._format_action_invocation(action)
161
+ if action.option_strings and any(
162
+ opt in ("-h", "--help", "-v", "--version") for opt in action.option_strings
163
+ ):
164
+ text = f"{Color.GREEN}{text}{Color.NC}"
165
+ return text
166
+
167
+
168
+ class _AgentNotesParser(argparse.ArgumentParser):
169
+ """Parser with custom help layout: description → usage → commands → options → examples."""
170
+
171
+ def format_help(self) -> str:
172
+ formatter = self._get_formatter()
173
+
174
+ # 1. Description (blue)
175
+ if self.description:
176
+ formatter.add_text(f"{Color.BLUE}{self.description}{Color.NC}")
177
+
178
+ # 2. Usage (plain "Usage:" prefix; usage line has its parts colored)
179
+ formatter.add_usage(
180
+ self.usage, self._actions, self._mutually_exclusive_groups,
181
+ prefix="Usage: ",
182
+ )
183
+
184
+ # 3. Commands — hand-rolled to show per-command flags
185
+ subparsers_action = next(
186
+ (a for a in self._actions if isinstance(a, argparse._SubParsersAction)),
187
+ None,
188
+ )
189
+ if subparsers_action is not None:
190
+ formatter.add_text(_render_commands_section(subparsers_action))
191
+
192
+ # 4. Options (global) — custom heading color
193
+ optional_group = next(
194
+ (g for g in self._action_groups
195
+ if g.title in ("options", "optional arguments") and g._group_actions),
196
+ None,
197
+ )
198
+ if optional_group is not None:
199
+ formatter.start_section(_heading("Options"))
200
+ formatter.add_text(optional_group.description)
201
+ formatter.add_arguments(optional_group._group_actions)
202
+ formatter.end_section()
203
+
204
+ # 5. Examples
205
+ formatter.add_text(_build_epilog())
206
+
207
+ return formatter.format_help()
208
+
209
+
210
+ def main():
211
+ parser = _AgentNotesParser(
212
+ prog="agent-notes",
213
+ description=DESCRIPTION,
214
+ usage=USAGE,
215
+ formatter_class=_AgentNotesHelp,
216
+ )
217
+ parser.add_argument("-v", "--version", action="store_true", help="Show version")
218
+
219
+ subparsers = parser.add_subparsers(
220
+ dest="command",
221
+ title="Commands",
222
+ metavar="",
223
+ parser_class=argparse.ArgumentParser, # subparsers use default formatting
224
+ )
225
+
226
+ # install
227
+ p_install = subparsers.add_parser("install", help="Build and install components")
228
+ p_install.add_argument("--local", action="store_true", help="Install to current project")
229
+ p_install.add_argument("--copy", action="store_true", help="Copy instead of symlink (with --local)")
230
+ p_install.add_argument("--reconfigure", action="store_true",
231
+ help="Clear existing state for this scope and re-run the wizard")
232
+
233
+ # build
234
+ subparsers.add_parser("build", help="Build agent configuration files from source")
235
+
236
+ # uninstall
237
+ p_uninstall = subparsers.add_parser("uninstall", help="Remove installed components")
238
+ p_uninstall.add_argument("--local", action="store_true", help="Remove from current project")
239
+
240
+ # update
241
+ p_update = subparsers.add_parser("update", help="Pull latest, show diff, reinstall")
242
+ p_update.add_argument("--dry-run", action="store_true", help="Show diff only, do not reinstall")
243
+ p_update.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
244
+ p_update.add_argument("--only", action="append", choices=["agents","skills","rules","commands","config","settings"],
245
+ help="Filter diff to these component types (repeatable)")
246
+ p_update.add_argument("--since", help="Override 'before' commit label (cosmetic only for now)")
247
+ p_update.add_argument("--skip-pull", action="store_true", help="Skip git pull")
248
+
249
+ # doctor
250
+ p_doctor = subparsers.add_parser("doctor", help="Check installation health")
251
+ p_doctor.add_argument("--local", action="store_true", help="Check local installation")
252
+ p_doctor.add_argument("--fix", action="store_true", help="Fix found issues")
253
+
254
+ # info
255
+ subparsers.add_parser("info", help="Show status and component counts")
256
+
257
+ # list
258
+ p_list = subparsers.add_parser("list", help="List installed components")
259
+ p_list.add_argument("filter", nargs="?", default="all",
260
+ choices=["agents", "skills", "rules", "clis", "models", "roles", "all"],
261
+ help="Which components to list")
262
+
263
+ # validate
264
+ subparsers.add_parser("validate", help="Lint source configuration files")
265
+
266
+ # set
267
+ p_set = subparsers.add_parser("set", help="Configure installation")
268
+ p_set_subparsers = p_set.add_subparsers(dest="entity", help="What to configure")
269
+ p_set_role = p_set_subparsers.add_parser("role", help="Set role→model assignment")
270
+ p_set_role.add_argument("role_name", help="Role name")
271
+ p_set_role.add_argument("model_id", help="Model ID")
272
+ p_set_role.add_argument("--cli", help="Target CLI (auto-detect if omitted)")
273
+ p_set_role.add_argument("--scope", choices=["global", "local"], help="Install scope")
274
+ p_set_role.add_argument("--local", action="store_true", help="Use local scope")
275
+
276
+ # regenerate
277
+ p_regen = subparsers.add_parser("regenerate", help="Rebuild files from state")
278
+ p_regen.add_argument("--scope", choices=["global", "local"], help="Install scope")
279
+ p_regen.add_argument("--cli", help="Regenerate specific CLI only")
280
+ p_regen.add_argument("--local", action="store_true", help="Use local scope")
281
+
282
+ # memory
283
+ p_memory = subparsers.add_parser("memory", help="Manage agent memory")
284
+ p_memory.add_argument("action", nargs="?", default="list",
285
+ choices=["init", "list", "vault", "index", "add", "size", "show", "reset", "export", "import"],
286
+ help="Memory action")
287
+ p_memory.add_argument("name", nargs="?", help="Agent name / note title (for show/reset/add)")
288
+ p_memory.add_argument("extra", nargs="*", help="Additional args (for add: body [type] [agent] [project])")
289
+
290
+ args = parser.parse_args()
291
+
292
+ if args.version:
293
+ from .config import get_version
294
+ print(f"agent-notes {get_version()}")
295
+ return
296
+
297
+ if not args.command:
298
+ parser.print_help()
299
+ return
300
+
301
+ # Route to modules
302
+ if args.command == "build":
303
+ from .commands.build import build
304
+ build()
305
+ elif args.command == "install":
306
+ if args.local or args.copy:
307
+ from .commands.install import install
308
+ install(local=args.local, copy=args.copy, reconfigure=args.reconfigure)
309
+ else:
310
+ from .commands.wizard import interactive_install
311
+ interactive_install()
312
+ elif args.command == "uninstall":
313
+ from .commands.install import uninstall
314
+ uninstall(local=args.local)
315
+ elif args.command == "update":
316
+ from .commands.update import update
317
+ update(
318
+ dry_run=args.dry_run,
319
+ yes=args.yes,
320
+ only=args.only,
321
+ since=args.since,
322
+ skip_pull=args.skip_pull,
323
+ )
324
+ elif args.command == "doctor":
325
+ from .commands.doctor import doctor
326
+ doctor(local=args.local, fix=args.fix)
327
+ elif args.command == "info":
328
+ from .commands.info import show_info
329
+ show_info()
330
+ elif args.command == "list":
331
+ from .commands.list import list_components
332
+ list_components(args.filter)
333
+ elif args.command == "validate":
334
+ from .commands.validate import validate
335
+ validate()
336
+ elif args.command == "set":
337
+ if args.entity == "role":
338
+ from .commands.set_role import set_role
339
+ set_role(args.role_name, args.model_id, cli=args.cli, scope=args.scope, local=args.local)
340
+ elif args.command == "regenerate":
341
+ from .commands.regenerate import regenerate
342
+ regenerate(scope=args.scope, cli=args.cli, local=args.local)
343
+ elif args.command == "memory":
344
+ from .commands.memory import memory
345
+ memory(args.action, args.name, getattr(args, "extra", None))
346
+
347
+ if __name__ == "__main__":
348
+ main()
@@ -0,0 +1,27 @@
1
+ """User-facing commands. Each module is a thin orchestrator.
2
+
3
+ Commands may import from: services/, registries/, domain/, config.
4
+ Commands MUST NOT import other commands (use services to share logic).
5
+
6
+ Exception: install/uninstall/info share helpers via _install_helpers.py since
7
+ they are sibling members of one logical command group.
8
+ """
9
+ from .install import install
10
+ from .uninstall import uninstall
11
+ from .info import show_info
12
+ from .build import build
13
+ from .doctor import doctor
14
+ from .validate import validate
15
+ from .update import update
16
+ from .regenerate import regenerate
17
+ from .set_role import set_role
18
+ from .wizard import interactive_install
19
+ from . import list as list_cmd
20
+ from . import memory as memory_cmd
21
+
22
+ __all__ = [
23
+ "install", "uninstall", "show_info",
24
+ "build", "doctor", "validate", "update",
25
+ "regenerate", "set_role", "interactive_install",
26
+ "list_cmd", "memory_cmd",
27
+ ]
@@ -0,0 +1,262 @@
1
+ """Shared installation helpers for install/uninstall/info commands."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from ..config import (
8
+ ROOT, DIST_CLAUDE_DIR, DIST_OPENCODE_DIR, DIST_GITHUB_DIR, DIST_RULES_DIR, DIST_SKILLS_DIR, DIST_SCRIPTS_DIR,
9
+ CLAUDE_HOME, OPENCODE_HOME, GITHUB_HOME, AGENTS_HOME, BIN_HOME,
10
+ linked, removed, skipped, info, get_version, Color, PKG_DIR
11
+ )
12
+ from ..services.fs import (
13
+ files_identical as _files_identical,
14
+ handle_existing as _handle_existing,
15
+ place_file, place_dir_contents, remove_symlink,
16
+ remove_all_symlinks_in_dir, remove_dir_if_empty
17
+ )
18
+
19
+
20
+ def install_scripts_global() -> None:
21
+ """Install scripts to ~/.local/bin/."""
22
+ from ..services.installer import install_scripts_global as _service_impl
23
+ _service_impl()
24
+
25
+
26
+ def _install_skills_to(targets: List[Path], dist_skills_dir: Path, copy_mode: bool) -> None:
27
+ """Install skills from dist_skills_dir to each directory in targets."""
28
+ if not dist_skills_dir.exists():
29
+ return
30
+ for target_dir in targets:
31
+ print(f"Installing skills to {target_dir} ...")
32
+ target_dir.mkdir(parents=True, exist_ok=True)
33
+ for skill_dir in sorted(dist_skills_dir.iterdir()):
34
+ if skill_dir.is_dir():
35
+ place_file(skill_dir, target_dir / skill_dir.name, copy_mode)
36
+
37
+
38
+ def install_skills_global(copy_mode: bool = False) -> None:
39
+ """Install skills globally."""
40
+ targets = [CLAUDE_HOME / "skills", OPENCODE_HOME / "skills", AGENTS_HOME / "skills"]
41
+ _install_skills_to(targets, DIST_SKILLS_DIR, copy_mode)
42
+
43
+
44
+ def install_skills_local(copy_mode: bool = False) -> None:
45
+ """Install skills locally."""
46
+ targets = [Path(".claude/skills"), Path(".opencode/skills")]
47
+ _install_skills_to(targets, DIST_SKILLS_DIR, copy_mode)
48
+
49
+
50
+ def install_agents_global(copy_mode: bool = False) -> None:
51
+ """Install agents globally."""
52
+ print("Installing Claude Code agents to ~/.claude/agents/ ...")
53
+ place_dir_contents(DIST_CLAUDE_DIR / "agents", CLAUDE_HOME / "agents", "*.md", copy_mode)
54
+
55
+ print("Installing OpenCode agents to ~/.config/opencode/agents/ ...")
56
+ place_dir_contents(DIST_OPENCODE_DIR / "agents", OPENCODE_HOME / "agents", "*.md", copy_mode)
57
+
58
+
59
+ def install_agents_local(copy_mode: bool = False) -> None:
60
+ """Install agents locally."""
61
+ print("Installing Claude Code agents to .claude/agents/ ...")
62
+ place_dir_contents(DIST_CLAUDE_DIR / "agents", Path(".claude/agents"), "*.md", copy_mode)
63
+
64
+ print("Installing OpenCode agents to .opencode/agents/ ...")
65
+ place_dir_contents(DIST_OPENCODE_DIR / "agents", Path(".opencode/agents"), "*.md", copy_mode)
66
+
67
+
68
+ def install_rules_global(copy_mode: bool = False) -> None:
69
+ """Install global config and rules."""
70
+ print("Installing global config ...")
71
+
72
+ # CLAUDE.md → ~/.claude/CLAUDE.md
73
+ claude_global = DIST_CLAUDE_DIR / "CLAUDE.md"
74
+ if claude_global.exists():
75
+ place_file(claude_global, CLAUDE_HOME / "CLAUDE.md", copy_mode)
76
+
77
+ # AGENTS.md → ~/.config/opencode/AGENTS.md
78
+ agents_global = DIST_OPENCODE_DIR / "AGENTS.md"
79
+ if agents_global.exists():
80
+ place_file(agents_global, OPENCODE_HOME / "AGENTS.md", copy_mode)
81
+
82
+ # Rules → ~/.claude/rules/
83
+ if DIST_RULES_DIR.exists():
84
+ place_dir_contents(DIST_RULES_DIR, CLAUDE_HOME / "rules", "*.md", copy_mode)
85
+
86
+ # Copilot → ~/.github/copilot-instructions.md
87
+ copilot_global = DIST_GITHUB_DIR / "copilot-instructions.md"
88
+ if copilot_global.exists():
89
+ place_file(copilot_global, GITHUB_HOME / "copilot-instructions.md", copy_mode)
90
+
91
+
92
+ def install_rules_local(copy_mode: bool = False) -> None:
93
+ """Install local config and rules."""
94
+ print("Installing project rules ...")
95
+
96
+ # CLAUDE.md → ./CLAUDE.md
97
+ claude_global = DIST_CLAUDE_DIR / "CLAUDE.md"
98
+ if claude_global.exists():
99
+ place_file(claude_global, Path("./CLAUDE.md"), copy_mode)
100
+
101
+ # AGENTS.md → ./AGENTS.md
102
+ agents_global = DIST_OPENCODE_DIR / "AGENTS.md"
103
+ if agents_global.exists():
104
+ place_file(agents_global, Path("./AGENTS.md"), copy_mode)
105
+
106
+ # Rules → .claude/rules/
107
+ if DIST_RULES_DIR.exists():
108
+ place_dir_contents(DIST_RULES_DIR, Path(".claude/rules"), "*.md", copy_mode)
109
+
110
+
111
+ def uninstall_scripts_global() -> None:
112
+ """Uninstall scripts from ~/.local/bin/."""
113
+ from ..services.installer import uninstall_scripts_global as _service_impl
114
+ _service_impl()
115
+
116
+
117
+ def _uninstall_skills_from(targets: List[Path]) -> None:
118
+ """Remove skills from each directory in targets."""
119
+ for target_dir in targets:
120
+ if target_dir.exists():
121
+ print(f"Removing skills from {target_dir} ...")
122
+ remove_all_symlinks_in_dir(target_dir)
123
+ remove_dir_if_empty(target_dir)
124
+
125
+
126
+ def uninstall_skills_global() -> None:
127
+ """Uninstall skills globally."""
128
+ from .. import config
129
+ targets = [config.CLAUDE_HOME / "skills", config.OPENCODE_HOME / "skills", config.AGENTS_HOME / "skills"]
130
+ _uninstall_skills_from(targets)
131
+
132
+
133
+ def uninstall_skills_local() -> None:
134
+ """Uninstall skills locally."""
135
+ _uninstall_skills_from([Path(".claude/skills"), Path(".opencode/skills")])
136
+
137
+
138
+ def _uninstall_agents_from(dirs: List[Path]) -> None:
139
+ """Remove agent symlinks from each directory in dirs."""
140
+ for agents_dir in dirs:
141
+ print(f"Removing agents from {agents_dir} ...")
142
+ remove_all_symlinks_in_dir(agents_dir)
143
+ remove_dir_if_empty(agents_dir)
144
+
145
+
146
+ def uninstall_agents_global() -> None:
147
+ """Uninstall agents globally."""
148
+ from .. import config
149
+ _uninstall_agents_from([config.CLAUDE_HOME / "agents", config.OPENCODE_HOME / "agents"])
150
+
151
+
152
+ def uninstall_agents_local() -> None:
153
+ """Uninstall agents locally."""
154
+ _uninstall_agents_from([Path(".claude/agents"), Path(".opencode/agents")])
155
+
156
+
157
+ def _uninstall_rules_from(config_symlinks: List[Path], rules_dir: Path, label: str) -> None:
158
+ """Remove config symlinks and rules directory."""
159
+ print(label)
160
+ for symlink in config_symlinks:
161
+ remove_symlink(symlink)
162
+ if rules_dir.exists():
163
+ remove_all_symlinks_in_dir(rules_dir)
164
+ remove_dir_if_empty(rules_dir)
165
+
166
+
167
+ def uninstall_rules_global() -> None:
168
+ """Uninstall global config and rules."""
169
+ from .. import config
170
+ _uninstall_rules_from(
171
+ [config.CLAUDE_HOME / "CLAUDE.md", config.OPENCODE_HOME / "AGENTS.md", config.GITHUB_HOME / "copilot-instructions.md"],
172
+ config.CLAUDE_HOME / "rules",
173
+ "Removing global config ...",
174
+ )
175
+
176
+
177
+ def uninstall_rules_local() -> None:
178
+ """Uninstall local config and rules."""
179
+ _uninstall_rules_from(
180
+ [Path("./CLAUDE.md"), Path("./AGENTS.md")],
181
+ Path(".claude/rules"),
182
+ "Removing project rules ...",
183
+ )
184
+
185
+
186
+ def count_scripts() -> int:
187
+ """Count script files."""
188
+ return len([f for f in DIST_SCRIPTS_DIR.iterdir() if f.is_file()]) if DIST_SCRIPTS_DIR.exists() else 0
189
+
190
+
191
+ def count_skills() -> int:
192
+ """Count skill directories."""
193
+ if not DIST_SKILLS_DIR.exists():
194
+ return 0
195
+ return len([d for d in DIST_SKILLS_DIR.iterdir() if d.is_dir()])
196
+
197
+
198
+ def count_agents(backend) -> int:
199
+ """Count agent *.md files in backend's dist directory. Returns 0 if backend
200
+ doesn't support agents."""
201
+ from ..services import installer
202
+ from ..domain.cli_backend import CLIBackend
203
+ if not backend.supports("agents"):
204
+ return 0
205
+ src = installer.dist_source_for(backend, "agents")
206
+ if src is None or not src.exists():
207
+ return 0
208
+ return len(list(src.glob("*.md")))
209
+
210
+
211
+ def count_global() -> int:
212
+ """Count global config files."""
213
+ count = 0
214
+
215
+ # Check each potential global config file (maintaining backward compatibility)
216
+ if (DIST_CLAUDE_DIR / "CLAUDE.md").exists():
217
+ count += 1
218
+ if (DIST_OPENCODE_DIR / "AGENTS.md").exists():
219
+ count += 1
220
+ if (DIST_GITHUB_DIR / "copilot-instructions.md").exists():
221
+ count += 1
222
+
223
+ # Count rules files
224
+ if DIST_RULES_DIR.exists():
225
+ count += len(list(DIST_RULES_DIR.glob("*.md")))
226
+
227
+ return count
228
+
229
+
230
+ def _verify_install(scope_state, scope, project_path, registry) -> list[str]:
231
+ """Check each file recorded in scope_state.installed exists. Return list of missing issues."""
232
+ from pathlib import Path
233
+ issues = []
234
+ for cli_name, backend_state in scope_state.clis.items():
235
+ try:
236
+ backend = registry.get(cli_name)
237
+ except KeyError:
238
+ issues.append(f"CLI '{cli_name}' no longer in registry")
239
+ continue
240
+ # Print per-component counts
241
+ for component_type, items in backend_state.installed.items():
242
+ present = 0
243
+ missing_names = []
244
+ for name, item in items.items():
245
+ if Path(item.target).exists() or Path(item.target).is_symlink():
246
+ present += 1
247
+ else:
248
+ missing_names.append(name)
249
+ total = len(items)
250
+ if missing_names:
251
+ comp_label = f"{backend.label} {component_type}"
252
+ print(f" ✗ {comp_label}: {len(missing_names)} missing ({', '.join(missing_names[:3])}{'...' if len(missing_names) > 3 else ''})")
253
+ for m in missing_names:
254
+ issues.append(f"{comp_label}: {m} missing")
255
+ else:
256
+ if total > 0:
257
+ if component_type == "config":
258
+ comp_label = f"{backend.label} config"
259
+ else:
260
+ comp_label = f"{backend.label} {component_type}"
261
+ print(f" ✓ {total} {comp_label} present")
262
+ return issues