@hopla/claude-setup 1.19.0 → 2.1.1
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.
- package/.claude-plugin/marketplace.json +2 -1
- package/.claude-plugin/plugin.json +4 -3
- package/CHANGELOG.md +123 -0
- package/LICENSE +21 -0
- package/README.md +49 -34
- package/cli.js +146 -7
- package/commands/guides/ai-optimized-codebase.md +4 -0
- package/commands/guides/data-audit.md +4 -0
- package/commands/guides/hooks-reference.md +4 -0
- package/commands/guides/mcp-integration.md +4 -0
- package/commands/guides/remote-coding.md +4 -0
- package/commands/guides/review-checklist.md +4 -0
- package/commands/guides/scaling-beyond-engineering.md +4 -0
- package/commands/guides/validation-pyramid.md +4 -0
- package/commands/guides/write-skill.md +4 -0
- package/hooks/prompt-route.js +244 -91
- package/hooks/statusline.js +3 -5
- package/hooks/tsc-check.js +40 -3
- package/package.json +16 -6
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/code-review/SKILL.md +5 -0
- package/skills/debug/SKILL.md +1 -1
- package/skills/performance/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-marketplace.json",
|
|
2
3
|
"name": "hopla-marketplace",
|
|
3
4
|
"description": "Hopla Tools plugin marketplace for Claude Code",
|
|
4
5
|
"owner": {
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
{
|
|
10
11
|
"name": "hopla",
|
|
11
12
|
"description": "Agentic coding system: PIV loop, TDD, debugging, brainstorming, subagent execution, and team workflows",
|
|
12
|
-
"version": "1.
|
|
13
|
+
"version": "2.1.1",
|
|
13
14
|
"source": "./",
|
|
14
15
|
"author": {
|
|
15
16
|
"name": "Hopla Tools",
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
2
3
|
"name": "hopla",
|
|
3
4
|
"description": "Agentic coding system for Claude Code: PIV loop (Plan → Implement → Validate), TDD, debugging, brainstorming, subagent execution, and team workflows",
|
|
4
|
-
"version": "1.
|
|
5
|
+
"version": "2.1.1",
|
|
5
6
|
"author": {
|
|
6
7
|
"name": "Hopla Tools",
|
|
7
8
|
"email": "julio@hopla.tools"
|
|
8
9
|
},
|
|
9
|
-
"homepage": "https://github.com/
|
|
10
|
-
"repository": "https://github.com/
|
|
10
|
+
"homepage": "https://github.com/HOPLAtools/claude-setup#readme",
|
|
11
|
+
"repository": "https://github.com/HOPLAtools/claude-setup",
|
|
11
12
|
"license": "MIT",
|
|
12
13
|
"keywords": [
|
|
13
14
|
"claude",
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **@hopla/claude-setup** are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [2.1.1] - 2026-05-12
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `hopla-claude-setup --migrate` and `--uninstall` now clean up legacy guide duplicates left in `~/.claude/commands/guides/` by pre-plugin CLI versions (≤ v1.11.x). Removes only files whose name matches a plugin-shipped guide — custom user guides in the same directory are preserved. Empty `guides/` directories are then removed.
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
- This addresses the duplicate `/guides:*` entries (marked `(user)`) that appeared alongside `/hopla:guides:*` (marked `(hopla)`) in the slash-command autocomplete after upgrading from a pre-plugin install.
|
|
16
|
+
|
|
17
|
+
## [2.1.0] - 2026-05-12
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `hopla-claude-setup --setup-statusline` and `--remove-statusline` flags: the CLI now wires the Hopla statusline into `~/.claude/settings.json` automatically. `uninstall` removes it. Manual JSON snippet is documented as a fallback only.
|
|
21
|
+
- `node:test` test suite (`npm test`) — 34 unit + integration tests covering `cli.js` helpers (`parseSettingsFile`, `safeWrite`, the new `setupStatusline`/`removeStatusline`) and the three core hooks (`env-protect`, `tsc-check`, `prompt-route`). Zero external dependencies — uses Node's built-in test runner.
|
|
22
|
+
- GitHub Actions CI (`.github/workflows/ci.yml`): runs on every PR and push to main — JSON validation, version sync check, `npm test`, CLI dry-runs, and the `hook-audit` manual smoke test.
|
|
23
|
+
- `CONTRIBUTING.md` with local development, testing, and PR guidelines.
|
|
24
|
+
- `SECURITY.md` with the vulnerability disclosure process and the plugin's threat model (which hooks intercept which tool calls).
|
|
25
|
+
- `CODE_OF_CONDUCT.md` (Contributor Covenant v2.1).
|
|
26
|
+
- `.github/PULL_REQUEST_TEMPLATE.md` and `.github/ISSUE_TEMPLATE/{bug_report,feature_request}.md`.
|
|
27
|
+
- Frontmatter `description:` on the 9 guides in `commands/guides/` so they appear with hints in the slash-command autocomplete.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **i18n cleanup** — the plugin is now language-agnostic. Spanish trigger phrases removed from the descriptions of `brainstorm`, `verify`, `performance`, `debug` skills. Users configure their preferred response language in their own `~/.claude/CLAUDE.md`.
|
|
31
|
+
- **`hooks/prompt-route.js` refactored to a hybrid matcher**: the hardcoded `SKILL_TRIGGERS` array is gone. The hook discovers skills at runtime by reading `SKILL.md` files; matching uses an optional `triggers:` frontmatter override, falling back to auto-derive from the skill name and quoted phrases in the description. New skills are matched without code changes.
|
|
32
|
+
- **`hooks/tsc-check.js` filters by file extension**: editing a `.md` file no longer triggers `tsc --noEmit`. Only `.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs` files invoke the check.
|
|
33
|
+
- `cli.js`: `parseSettingsFile` and `safeWrite` are now exported for testing. The main dispatcher is gated by an `isMainModule` check so the file can be imported as a library by tests.
|
|
34
|
+
- `hooks/statusline.js` header comment points users at the new CLI flag rather than a hardcoded JSON snippet.
|
|
35
|
+
- README "Optional: Hopla statusline" section recommends the CLI flag; manual JSON setup kept as a fallback note.
|
|
36
|
+
- `CLAUDE.md` §5 (Testing) and §6 (Release flow) now describe `npm test` and the CI verification step.
|
|
37
|
+
- `hooks/prompt-route.js` `triggers:` override added to `skills/code-review/SKILL.md` so phrases like "review my code" (with words between "review" and "code") match the skill.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
- Temporary `.DS_Store` artifact at repo root (already in `.gitignore`).
|
|
41
|
+
|
|
42
|
+
### Notes for plugin authors
|
|
43
|
+
- Skill discovery in Claude Code v1.24 does **not** support nested `skills/<subdir>/<name>/SKILL.md` paths. The 9 reference guides remain in `commands/guides/*.md` and ship with `description:` frontmatter. A future migration to `skills/` will revisit this if the platform adds support.
|
|
44
|
+
|
|
45
|
+
## [2.0.0] - 2026-05-12
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- `LICENSE` (MIT) in repo root for legal clarity in npm + GitHub.
|
|
49
|
+
- `description`, `repository`, `homepage`, `bugs` fields in `package.json` for proper npm registry display.
|
|
50
|
+
- `$schema` field in `plugin.json` and `marketplace.json` for JSON Schema validation in editors.
|
|
51
|
+
- This `CHANGELOG.md` (full history back to 1.13.0 below).
|
|
52
|
+
- `LICENSE` and `CHANGELOG.md` are now included in the published npm tarball via `files[]`.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- **Breaking:** repository slug in `plugin.json` corrected from `hopla-tools/claude-setup` (404) to `HOPLAtools/claude-setup` (the real GitHub org).
|
|
56
|
+
- `plugin.json` `homepage` differentiated from `repository`: now points to `#readme` anchor.
|
|
57
|
+
- README install snippet simplified: `/plugin marketplace add HOPLAtools/claude-setup` (one argument — the canonical form per Anthropic docs).
|
|
58
|
+
- All CLI references in README updated from `claude-setup` to `hopla-claude-setup`.
|
|
59
|
+
|
|
60
|
+
### Removed
|
|
61
|
+
- **Breaking:** bin alias `claude-setup` removed from `package.json`. The package now exposes only `hopla-claude-setup`. Migration: replace any script that calls `claude-setup` with `hopla-claude-setup` — same flags, same behavior. Rationale: the generic name risked colliding with other npm packages distributed publicly.
|
|
62
|
+
|
|
63
|
+
## [1.19.0] - 2026-05-11
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- New `hopla:hook-audit` skill — mechanical pre-commit checks for new React hooks against 4 documented bug classes (P-5 memoization, S-8 stale-id guard, E-1 error matching, D-1 cache + dedup integrity).
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- Documentation aligned with the canonical `/plugin marketplace update <name>` flow introduced in Claude Code v1.24+. The older `cd … && git pull` dance is no longer prescribed.
|
|
70
|
+
|
|
71
|
+
## [1.18.0] - 2026-05-11
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
- OpenSpec wave 1: `AGENTS.md` (canonical project rules) with a CLAUDE.md alias path.
|
|
75
|
+
- `.agents/specs/canonical/` for living requirements documents.
|
|
76
|
+
- `/hopla:archive` command — folds delta-specs from completed plans into canonical specs and moves artifacts to archive locations.
|
|
77
|
+
- `claude-setup status` subcommand — read-only inspection of the current project's `.agents/` workflow (plans, specs, reviews, suggested next step). JSON output available via `--json`.
|
|
78
|
+
|
|
79
|
+
## [1.17.1] - 2026-05-01
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- `plan-feature` now enforces UX iteration budget declaration when the feature touches a visible UI surface.
|
|
83
|
+
- `plan-feature` now requires a `## Domain Assumptions` section when the feature uses project-specific vocabulary.
|
|
84
|
+
|
|
85
|
+
## [1.17.0] - 2026-04-17
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- New skills: `refactoring`, `performance`, `migration`.
|
|
89
|
+
- Spec linking — plans can reference and update canonical specs.
|
|
90
|
+
- `.agents/audits/` directory for audit artifacts.
|
|
91
|
+
|
|
92
|
+
## [1.16.0] - 2026-04-17
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
- Refactor: progressive disclosure across skills, shared references between skills (e.g. `worktree` cites `git/flow-detection.md`), cleaner telemetry.
|
|
96
|
+
|
|
97
|
+
## [1.15.0] - 2026-04-17
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
- MCP server template (`.mcp.json.template`).
|
|
101
|
+
- New hooks: `UserPromptSubmit` (skill routing hints), `PreCompact` (session state snapshot).
|
|
102
|
+
- Optional statusline renderer (`hooks/statusline.js`) showing branch · worktree indicator · uncommitted count · active plan.
|
|
103
|
+
|
|
104
|
+
## [1.14.1] - 2026-04-17
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
- 7 critical audit findings (see commit `69c025b` for the full list).
|
|
108
|
+
|
|
109
|
+
## [1.14.0] - 2026-04-17
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
- CLI `--dry-run` flag that composes with any other flag.
|
|
113
|
+
- CLI cleans legacy agents (`code-reviewer.md`, `codebase-researcher.md`, `system-reviewer.md`) and legacy `PLANNING_PERMISSIONS` from older installs.
|
|
114
|
+
- CLI prints an advisory when the plugin or marketplace cache is still present (the CLI cannot remove those — the user does).
|
|
115
|
+
|
|
116
|
+
## [1.13.0] - 2026-04-17
|
|
117
|
+
|
|
118
|
+
### Added
|
|
119
|
+
- `git` skill integrated with `worktree` skill via centralized Git Flow detection in `flow-detection.md`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
For releases prior to 1.13.0, see the git history (`git log --oneline`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Hopla Tools
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The full setup is **plugin + CLI** — they deliver different layers:
|
|
|
24
24
|
**Step 1 — Register the marketplace and install the plugin** (inside Claude Code):
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
/plugin marketplace add
|
|
27
|
+
/plugin marketplace add HOPLAtools/claude-setup
|
|
28
28
|
/plugin install hopla@hopla-marketplace
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -32,11 +32,15 @@ The full setup is **plugin + CLI** — they deliver different layers:
|
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
npm install -g @hopla/claude-setup
|
|
35
|
-
claude-setup --force
|
|
35
|
+
hopla-claude-setup --force
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
That's it. Commands show as `/hopla:<name>`, skills auto-trigger when relevant.
|
|
39
39
|
|
|
40
|
+
**Recommended — enable auto-update for the marketplace** (one-time, inside Claude Code):
|
|
41
|
+
|
|
42
|
+
Open `/plugin` → **Marketplaces** tab → select `hopla-marketplace` → enable auto-update. From then on, new releases are fetched automatically at session start; Claude Code will prompt you to run `/reload-plugins` when an update is ready. Skip this step and you'll need to refresh manually (see [Update](#update) below).
|
|
43
|
+
|
|
40
44
|
### Plugin only (skip global rules)
|
|
41
45
|
|
|
42
46
|
If you don't want the machine-wide `~/.claude/CLAUDE.md`, just do Step 1. The plugin works on its own.
|
|
@@ -51,28 +55,25 @@ If you only want the global rules and not the plugin's commands/skills, just do
|
|
|
51
55
|
|
|
52
56
|
### Plugin channel
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
cd ~/.claude/plugins/marketplaces/hopla-marketplace && git pull
|
|
58
|
-
```
|
|
58
|
+
**If auto-update is enabled** (see Quick Start Step 2.5): nothing to do. Claude Code fetches new releases at session start and prompts you to run `/reload-plugins`. That's it.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
**Manual refresh** (when auto-update is off, or you want to pull immediately):
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
/plugin
|
|
64
|
-
/plugin
|
|
63
|
+
/plugin marketplace update hopla-marketplace
|
|
64
|
+
/plugin disable hopla@hopla-marketplace
|
|
65
|
+
/plugin enable hopla@hopla-marketplace
|
|
65
66
|
/reload-plugins
|
|
66
67
|
```
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
`/plugin marketplace update` refreshes the cached marketplace listing. `disable`/`enable` cycles the plugin so the new files are picked up — faster than a full `uninstall`/`install` because no scope re-selection is needed. `/reload-plugins` ensures hooks and commands are re-registered in the current session.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
> The older `cd ~/.claude/plugins/marketplaces/hopla-marketplace && git pull` workflow still works but is no longer recommended — the built-in `/plugin marketplace update` is the canonical path since Claude Code v1.24.
|
|
71
72
|
|
|
72
73
|
### CLI channel (global rules)
|
|
73
74
|
|
|
74
75
|
```bash
|
|
75
|
-
npm install -g @hopla/claude-setup@latest --prefer-online && claude-setup --force
|
|
76
|
+
npm install -g @hopla/claude-setup@latest --prefer-online && hopla-claude-setup --force
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
---
|
|
@@ -88,7 +89,7 @@ npm install -g @hopla/claude-setup@latest --prefer-online && claude-setup --forc
|
|
|
88
89
|
**CLI:**
|
|
89
90
|
|
|
90
91
|
```bash
|
|
91
|
-
claude-setup --uninstall
|
|
92
|
+
hopla-claude-setup --uninstall
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
Removes `~/.claude/CLAUDE.md` plus legacy `hopla-*` files from older installs.
|
|
@@ -97,14 +98,14 @@ Removes `~/.claude/CLAUDE.md` plus legacy `hopla-*` files from older installs.
|
|
|
97
98
|
|
|
98
99
|
| Flag | Purpose |
|
|
99
100
|
|---|---|
|
|
100
|
-
| `claude-setup` | Interactive install of global rules + permissions |
|
|
101
|
-
| `claude-setup --force` | Install without prompts |
|
|
102
|
-
| `claude-setup --migrate` | Remove legacy CLI-installed duplicates only |
|
|
103
|
-
| `claude-setup --uninstall` | Remove global rules + legacy files |
|
|
104
|
-
| `claude-setup status` | Read-only inspection of the current project's `.agents/` workflow state (plans, specs, reviews, suggested next step) |
|
|
105
|
-
| `claude-setup status --json` | Same as above, JSON output for agents to parse |
|
|
106
|
-
| `claude-setup --dry-run` | Preview changes without touching disk (composes with other flags) |
|
|
107
|
-
| `claude-setup --version` | Print package version |
|
|
101
|
+
| `hopla-claude-setup` | Interactive install of global rules + permissions |
|
|
102
|
+
| `hopla-claude-setup --force` | Install without prompts |
|
|
103
|
+
| `hopla-claude-setup --migrate` | Remove legacy CLI-installed duplicates only |
|
|
104
|
+
| `hopla-claude-setup --uninstall` | Remove global rules + legacy files |
|
|
105
|
+
| `hopla-claude-setup status` | Read-only inspection of the current project's `.agents/` workflow state (plans, specs, reviews, suggested next step) |
|
|
106
|
+
| `hopla-claude-setup status --json` | Same as above, JSON output for agents to parse |
|
|
107
|
+
| `hopla-claude-setup --dry-run` | Preview changes without touching disk (composes with other flags) |
|
|
108
|
+
| `hopla-claude-setup --version` | Print package version |
|
|
108
109
|
|
|
109
110
|
---
|
|
110
111
|
|
|
@@ -112,21 +113,33 @@ Removes `~/.claude/CLAUDE.md` plus legacy `hopla-*` files from older installs.
|
|
|
112
113
|
|
|
113
114
|
The plugin ships a statusline script that shows your branch, worktree indicator, uncommitted count, and active plan file (`📋 plan-name`) in Claude Code's status bar.
|
|
114
115
|
|
|
115
|
-
Enable it
|
|
116
|
+
**Enable it (one-time):**
|
|
116
117
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
"statusLine": {
|
|
120
|
-
"type": "command",
|
|
121
|
-
"command": "node ~/.claude/plugins/marketplaces/hopla-marketplace/hooks/statusline.js"
|
|
122
|
-
}
|
|
123
|
-
}
|
|
118
|
+
```bash
|
|
119
|
+
hopla-claude-setup --setup-statusline
|
|
124
120
|
```
|
|
125
121
|
|
|
126
|
-
|
|
122
|
+
This writes the right `statusLine` block to `~/.claude/settings.json`, pointing at the installed plugin's `statusline.js`. Run `/reload-plugins` (or restart Claude Code) to see it.
|
|
123
|
+
|
|
124
|
+
**Disable it:**
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
hopla-claude-setup --remove-statusline
|
|
128
|
+
```
|
|
127
129
|
|
|
128
130
|
Sample output: ` feature/auth · 3M · 📋 add-authentication`
|
|
129
131
|
|
|
132
|
+
> **Manual setup (fallback):** if you don't have the CLI installed, add this to `~/.claude/settings.json` yourself:
|
|
133
|
+
> ```json
|
|
134
|
+
> {
|
|
135
|
+
> "statusLine": {
|
|
136
|
+
> "type": "command",
|
|
137
|
+
> "command": "node ~/.claude/plugins/marketplaces/hopla-marketplace/hooks/statusline.js"
|
|
138
|
+
> }
|
|
139
|
+
> }
|
|
140
|
+
> ```
|
|
141
|
+
> Replace `hopla-marketplace` with the actual marketplace name if you registered it differently.
|
|
142
|
+
|
|
130
143
|
---
|
|
131
144
|
|
|
132
145
|
## Naming Convention
|
|
@@ -379,7 +392,7 @@ Commands are modular — the output of one becomes the input of the next. Some a
|
|
|
379
392
|
### `/plugin install` doesn't show the `hopla` plugin
|
|
380
393
|
|
|
381
394
|
- Run `/plugin marketplace list` — confirm `hopla-marketplace` is registered
|
|
382
|
-
- If missing, re-run `/plugin marketplace add
|
|
395
|
+
- If missing, re-run `/plugin marketplace add HOPLAtools/claude-setup`
|
|
383
396
|
- Restart Claude Code after registering a new marketplace
|
|
384
397
|
|
|
385
398
|
### Commands appear as both `hopla-*` and `hopla:*`
|
|
@@ -387,7 +400,7 @@ Commands are modular — the output of one becomes the input of the next. Some a
|
|
|
387
400
|
You have legacy CLI-installed files from before the plugin refactor. Clean up:
|
|
388
401
|
|
|
389
402
|
```bash
|
|
390
|
-
claude-setup --migrate
|
|
403
|
+
hopla-claude-setup --migrate
|
|
391
404
|
```
|
|
392
405
|
|
|
393
406
|
This removes `hopla-*` duplicates from `~/.claude/commands/` and `~/.claude/skills/` without touching your global rules.
|
|
@@ -401,7 +414,7 @@ cd ~/.claude/plugins/marketplaces/hopla-marketplace && git pull
|
|
|
401
414
|
|
|
402
415
|
You never registered the marketplace. Run Quick Start Step 1 first.
|
|
403
416
|
|
|
404
|
-
### `claude-setup: command not found`
|
|
417
|
+
### `hopla-claude-setup: command not found`
|
|
405
418
|
|
|
406
419
|
The npm global bin is not on your PATH. Check with:
|
|
407
420
|
|
|
@@ -412,6 +425,8 @@ npm config get prefix
|
|
|
412
425
|
|
|
413
426
|
Alternatively, run via npx: `npx @hopla/claude-setup --force`.
|
|
414
427
|
|
|
428
|
+
> **Migrating from v1.x?** The `claude-setup` bin alias was removed in v2.0.0. Replace any script that calls `claude-setup` with `hopla-claude-setup` (same flags, same behavior).
|
|
429
|
+
|
|
415
430
|
### Changes to skills/commands don't take effect
|
|
416
431
|
|
|
417
432
|
Claude Code caches plugin content. After a plugin update:
|
package/cli.js
CHANGED
|
@@ -13,6 +13,8 @@ const VERSION = process.argv.includes("--version") || process.argv.includes("-v"
|
|
|
13
13
|
const DRY_RUN = process.argv.includes("--dry-run");
|
|
14
14
|
const STATUS = process.argv.includes("status");
|
|
15
15
|
const JSON_OUT = process.argv.includes("--json");
|
|
16
|
+
const SETUP_STATUSLINE = process.argv.includes("--setup-statusline");
|
|
17
|
+
const REMOVE_STATUSLINE = process.argv.includes("--remove-statusline");
|
|
16
18
|
|
|
17
19
|
if (VERSION) {
|
|
18
20
|
const pkg = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8"));
|
|
@@ -27,6 +29,10 @@ const HOOKS_DIR = path.join(CLAUDE_DIR, "hooks");
|
|
|
27
29
|
const AGENTS_DIR = path.join(CLAUDE_DIR, "agents");
|
|
28
30
|
const PLUGINS_DIR = path.join(CLAUDE_DIR, "plugins");
|
|
29
31
|
const MARKETPLACE_CACHE = path.join(PLUGINS_DIR, "marketplaces", "hopla-marketplace");
|
|
32
|
+
const STATUSLINE_SCRIPT = path.join(MARKETPLACE_CACHE, "hooks", "statusline.js");
|
|
33
|
+
// Substring used to identify a statusLine block managed by this plugin.
|
|
34
|
+
// Survives manual renames of the marketplace as long as users keep "hopla" in the path.
|
|
35
|
+
const STATUSLINE_MARKER = "hopla-marketplace";
|
|
30
36
|
const SETTINGS_FILES = [
|
|
31
37
|
path.join(CLAUDE_DIR, "settings.json"),
|
|
32
38
|
path.join(CLAUDE_DIR, "settings.local.json"),
|
|
@@ -55,8 +61,9 @@ function safeRm(target, opts = {}) {
|
|
|
55
61
|
|
|
56
62
|
// Atomic write: stage to a tmp file and rename over the target.
|
|
57
63
|
// Protects ~/.claude/settings.json from corruption if the process crashes mid-write.
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
// Exported for testing — when imported as a library, callers can pass dryRun explicitly.
|
|
65
|
+
export function safeWrite(target, content, { dryRun = DRY_RUN } = {}) {
|
|
66
|
+
if (dryRun) return;
|
|
60
67
|
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
61
68
|
try {
|
|
62
69
|
fs.writeFileSync(tmp, content);
|
|
@@ -94,7 +101,8 @@ function logRemoved(label) {
|
|
|
94
101
|
// missing. Warns (and returns null) when the file exists but is not valid JSON
|
|
95
102
|
// — previously these failures were silently swallowed, causing cleanup and
|
|
96
103
|
// permission updates to skip with no signal to the user.
|
|
97
|
-
|
|
104
|
+
// Exported for unit testing.
|
|
105
|
+
export function parseSettingsFile(settingsPath) {
|
|
98
106
|
if (!fs.existsSync(settingsPath)) return null;
|
|
99
107
|
try {
|
|
100
108
|
return JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
@@ -147,6 +155,23 @@ const LEGACY_HOOK_COMMANDS = [
|
|
|
147
155
|
"session-prime.js",
|
|
148
156
|
];
|
|
149
157
|
|
|
158
|
+
// Guide files the pre-plugin CLI (≤ v1.11.x) copied to ~/.claude/commands/guides/.
|
|
159
|
+
// The plugin now ships them via commands/guides/, namespaced as /hopla:guides:<name>,
|
|
160
|
+
// so the user-level copies show up as duplicates in autocomplete (no "(hopla)" suffix).
|
|
161
|
+
// Cleanup removes only files whose name matches a plugin-shipped guide, leaving any
|
|
162
|
+
// custom user guides in ~/.claude/commands/guides/ untouched.
|
|
163
|
+
const LEGACY_GUIDE_FILES = [
|
|
164
|
+
"ai-optimized-codebase.md",
|
|
165
|
+
"data-audit.md",
|
|
166
|
+
"hooks-reference.md",
|
|
167
|
+
"mcp-integration.md",
|
|
168
|
+
"remote-coding.md",
|
|
169
|
+
"review-checklist.md",
|
|
170
|
+
"scaling-beyond-engineering.md",
|
|
171
|
+
"validation-pyramid.md",
|
|
172
|
+
"write-skill.md",
|
|
173
|
+
];
|
|
174
|
+
|
|
150
175
|
// Agents installed directly by v1.11.0 and v1.12.0 (no hopla- prefix)
|
|
151
176
|
// Must be cleaned up so the plugin-provided versions are the only source of truth
|
|
152
177
|
const LEGACY_AGENT_FILES = [
|
|
@@ -191,6 +216,26 @@ function removeLegacyFiles() {
|
|
|
191
216
|
}
|
|
192
217
|
}
|
|
193
218
|
|
|
219
|
+
// Legacy guide duplicates in ~/.claude/commands/guides/ (pre-plugin CLI).
|
|
220
|
+
// Only remove files matching a guide the plugin currently ships; preserve
|
|
221
|
+
// any custom user-created guides in the same directory.
|
|
222
|
+
const legacyGuidesDir = path.join(COMMANDS_DIR, "guides");
|
|
223
|
+
if (fs.existsSync(legacyGuidesDir)) {
|
|
224
|
+
for (const guideFile of LEGACY_GUIDE_FILES) {
|
|
225
|
+
const guidePath = path.join(legacyGuidesDir, guideFile);
|
|
226
|
+
if (fs.existsSync(guidePath)) {
|
|
227
|
+
safeRm(guidePath);
|
|
228
|
+
removed.push(`~/.claude/commands/guides/${guideFile}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!DRY_RUN) {
|
|
232
|
+
try {
|
|
233
|
+
const remaining = fs.readdirSync(legacyGuidesDir);
|
|
234
|
+
if (remaining.length === 0) fs.rmSync(legacyGuidesDir, { recursive: true });
|
|
235
|
+
} catch { /* ignore */ }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
194
239
|
// hopla-* skills
|
|
195
240
|
if (fs.existsSync(SKILLS_DIR)) {
|
|
196
241
|
for (const entry of fs.readdirSync(SKILLS_DIR)) {
|
|
@@ -372,6 +417,11 @@ async function uninstall() {
|
|
|
372
417
|
logRemoved(item);
|
|
373
418
|
}
|
|
374
419
|
|
|
420
|
+
const statuslineRemoved = removeStatuslineFromSettings();
|
|
421
|
+
for (const item of statuslineRemoved) {
|
|
422
|
+
logRemoved(item);
|
|
423
|
+
}
|
|
424
|
+
|
|
375
425
|
log(`\n${GREEN}${BOLD}Done!${RESET} ${DRY_RUN ? "Dry-run complete — no files were changed." : "CLI-managed files removed."}\n`);
|
|
376
426
|
|
|
377
427
|
if (pluginActive) {
|
|
@@ -425,6 +475,82 @@ async function setupPermissions() {
|
|
|
425
475
|
log(` ${GREEN}✓${RESET} Permissions configured.\n`);
|
|
426
476
|
}
|
|
427
477
|
|
|
478
|
+
async function setupStatusline() {
|
|
479
|
+
log(`\n${BOLD}@hopla/claude-setup${RESET} — Setup statusline${dryTag()}\n`);
|
|
480
|
+
|
|
481
|
+
if (!fs.existsSync(STATUSLINE_SCRIPT)) {
|
|
482
|
+
log(`${RED}✕${RESET} Plugin not detected at ${STATUSLINE_SCRIPT}`);
|
|
483
|
+
log(` Install the plugin first inside Claude Code:`);
|
|
484
|
+
log(` ${CYAN}/plugin marketplace add HOPLAtools/claude-setup${RESET}`);
|
|
485
|
+
log(` ${CYAN}/plugin install hopla@hopla-marketplace${RESET}\n`);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
490
|
+
let settings = parseSettingsFile(settingsPath);
|
|
491
|
+
if (!settings) {
|
|
492
|
+
if (fs.existsSync(settingsPath)) {
|
|
493
|
+
log(` ${YELLOW}↷${RESET} Skipped — settings.json is not valid JSON. Fix it and re-run.\n`);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
settings = {};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const command = `node ${STATUSLINE_SCRIPT}`;
|
|
500
|
+
const newBlock = { type: "command", command };
|
|
501
|
+
|
|
502
|
+
const existing = settings.statusLine;
|
|
503
|
+
if (existing && typeof existing === "object") {
|
|
504
|
+
const isOurs = typeof existing.command === "string" && existing.command.includes(STATUSLINE_MARKER);
|
|
505
|
+
if (isOurs && existing.command === command) {
|
|
506
|
+
log(`${GREEN}✓${RESET} Statusline already configured (idempotent — no changes).\n`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!isOurs) {
|
|
510
|
+
log(` ${YELLOW}⚠${RESET} Existing statusLine points elsewhere:`);
|
|
511
|
+
log(` ${existing.command || JSON.stringify(existing)}`);
|
|
512
|
+
const ok = await confirm(` Overwrite with Hopla statusline? (y/N) `);
|
|
513
|
+
if (!ok) {
|
|
514
|
+
log(` ${YELLOW}↷${RESET} Kept existing statusLine. No changes.\n`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
settings.statusLine = newBlock;
|
|
521
|
+
safeWrite(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
522
|
+
log(` ${GREEN}✓${RESET} ${DRY_RUN ? "Would write" : "Wrote"} statusLine to ~/.claude/settings.json:`);
|
|
523
|
+
log(` ${CYAN}${command}${RESET}\n`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function removeStatuslineFromSettings() {
|
|
527
|
+
const removed = [];
|
|
528
|
+
const settingsPath = path.join(CLAUDE_DIR, "settings.json");
|
|
529
|
+
const settings = parseSettingsFile(settingsPath);
|
|
530
|
+
if (!settings || !settings.statusLine) return removed;
|
|
531
|
+
|
|
532
|
+
const cmd = settings.statusLine.command;
|
|
533
|
+
if (typeof cmd === "string" && cmd.includes(STATUSLINE_MARKER)) {
|
|
534
|
+
delete settings.statusLine;
|
|
535
|
+
safeWrite(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
536
|
+
removed.push(`statusLine from settings.json`);
|
|
537
|
+
}
|
|
538
|
+
return removed;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function removeStatusline() {
|
|
542
|
+
log(`\n${BOLD}@hopla/claude-setup${RESET} — Remove statusline${dryTag()}\n`);
|
|
543
|
+
const removed = removeStatuslineFromSettings();
|
|
544
|
+
if (removed.length === 0) {
|
|
545
|
+
log(`${GREEN}✓${RESET} No Hopla statusline found. Nothing to remove.\n`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
for (const item of removed) {
|
|
549
|
+
logRemoved(item);
|
|
550
|
+
}
|
|
551
|
+
log("");
|
|
552
|
+
}
|
|
553
|
+
|
|
428
554
|
async function install() {
|
|
429
555
|
log(`\n${BOLD}@hopla/claude-setup${RESET} — Global Rules Setup${dryTag()}\n`);
|
|
430
556
|
|
|
@@ -594,8 +720,21 @@ function status() {
|
|
|
594
720
|
|
|
595
721
|
const run = STATUS
|
|
596
722
|
? async () => status()
|
|
723
|
+
: SETUP_STATUSLINE
|
|
724
|
+
? setupStatusline
|
|
725
|
+
: REMOVE_STATUSLINE
|
|
726
|
+
? removeStatusline
|
|
597
727
|
: (UNINSTALL ? uninstall : (MIGRATE ? migrate : install));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
728
|
+
|
|
729
|
+
// Only invoke the dispatcher when this file is executed directly (e.g. via
|
|
730
|
+
// `node cli.js` or the npm bin). When the file is imported as a library
|
|
731
|
+
// (tests), skip — tests call the exported helpers themselves.
|
|
732
|
+
import { pathToFileURL } from "url";
|
|
733
|
+
const isMainModule = process.argv[1]
|
|
734
|
+
&& import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
735
|
+
if (isMainModule) {
|
|
736
|
+
run().catch((err) => {
|
|
737
|
+
console.error("Failed:", err.message);
|
|
738
|
+
process.exit(1);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Reference for auditing existing data sources (schema, value semantics, null cases, derived value propagation) before implementing data-consuming features.
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Guide: Data Audit and Value Semantics
|
|
2
6
|
|
|
3
7
|
Use this guide when a feature reads from, calculates with, or references existing data or code patterns. Applies during planning (full audit) and execution (verification of documented findings).
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Guide for creating a project-specific code review checklist (.agents/guides/review-checklist.md) consumed by the code-review skill.
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Guide: Creating a Project-Specific Review Checklist
|
|
2
6
|
|
|
3
7
|
Use this guide to create a `.agents/guides/review-checklist.md` file in your project with code review checks specific to your tech stack, domain, and known anti-patterns. The the `code-review` skill command loads this file automatically when it exists, applying your custom checks alongside the standard review categories.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Shared reference for the full validation sequence (lint, types, tests, code review, manual smoke). Used by execute, validate, and verify.
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# Validation Pyramid
|
|
2
6
|
|
|
3
7
|
Shared reference for the full validation sequence. Callers (`commands/execute.md`, `commands/validate.md`, `skills/verify/SKILL.md`, plus plans' `Validation Checklist`) pick the levels that apply to their scope.
|
package/hooks/prompt-route.js
CHANGED
|
@@ -1,94 +1,242 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// UserPromptSubmit hook: scan the user's prompt for skill keywords and inject
|
|
3
|
-
// a short routing hint.
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
3
|
+
// a short routing hint. Reads skill catalog from disk at runtime — no
|
|
4
|
+
// hardcoded skill list — so adding/removing/renaming a skill needs no edit here.
|
|
5
|
+
//
|
|
6
|
+
// Matching strategy (hybrid):
|
|
7
|
+
// 1. If a SKILL.md declares `triggers: [regex, ...]` in its frontmatter,
|
|
8
|
+
// use those patterns literally.
|
|
9
|
+
// 2. Otherwise auto-derive patterns from the skill name (word-boundary,
|
|
10
|
+
// case-insensitive) and the first significant words of the description.
|
|
11
|
+
//
|
|
12
|
+
// Skill discovery walks `../skills/` recursively so nested skills
|
|
13
|
+
// (e.g. skills/guides/<name>/SKILL.md) participate without code changes.
|
|
14
|
+
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
|
|
18
|
+
const SKILLS_DIR = path.join(import.meta.dirname, "..", "skills");
|
|
19
|
+
const STOPWORDS = new Set([
|
|
20
|
+
"use", "when", "with", "that", "this", "from", "into", "your", "have",
|
|
21
|
+
"also", "user", "only", "before", "after", "during", "than", "such",
|
|
22
|
+
"more", "less", "most", "least", "some", "many", "much", "still", "just",
|
|
23
|
+
"what", "which", "would", "could", "should", "needs", "need", "make",
|
|
24
|
+
"made", "does", "done", "doing", "wants", "want", "next", "previous",
|
|
25
|
+
"very", "really", "always", "never", "must", "may", "might", "shall",
|
|
26
|
+
"trigger", "triggers", "phrase", "phrases", "skill", "skills", "do",
|
|
27
|
+
"not", "the", "and", "for", "are", "was", "were", "but", "any", "all",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Walk skills/ recursively. Return [{ name, descriptors, triggers, path }].
|
|
31
|
+
// Each entry already exposes either a literal triggers list (from frontmatter)
|
|
32
|
+
// or a derived list (from name + description tokens).
|
|
33
|
+
function loadSkillCatalog(skillsRoot) {
|
|
34
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
35
|
+
const catalog = [];
|
|
36
|
+
|
|
37
|
+
function walk(dir) {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const fullPath = path.join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
const candidate = path.join(fullPath, "SKILL.md");
|
|
48
|
+
if (fs.existsSync(candidate)) {
|
|
49
|
+
const parsed = parseSkillFrontmatter(candidate);
|
|
50
|
+
if (parsed) catalog.push(parsed);
|
|
51
|
+
}
|
|
52
|
+
// Continue walking — skills may be nested under sub-directories.
|
|
53
|
+
walk(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
walk(skillsRoot);
|
|
59
|
+
return catalog;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extract `name`, `description`, optional `triggers` from a SKILL.md frontmatter.
|
|
63
|
+
// Tolerant parser — handles double/single-quoted scalars and YAML-style array
|
|
64
|
+
// `triggers: [...]` on a single line or with each item on its own line.
|
|
65
|
+
function parseSkillFrontmatter(filePath) {
|
|
66
|
+
let content;
|
|
67
|
+
try {
|
|
68
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!content.startsWith("---")) return null;
|
|
73
|
+
const end = content.indexOf("\n---", 3);
|
|
74
|
+
if (end === -1) return null;
|
|
75
|
+
const block = content.slice(3, end);
|
|
76
|
+
|
|
77
|
+
const data = {};
|
|
78
|
+
const lines = block.split("\n");
|
|
79
|
+
let i = 0;
|
|
80
|
+
while (i < lines.length) {
|
|
81
|
+
const line = lines[i];
|
|
82
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
83
|
+
if (!m) { i++; continue; }
|
|
84
|
+
const key = m[1];
|
|
85
|
+
let rest = m[2];
|
|
86
|
+
|
|
87
|
+
if (key === "triggers") {
|
|
88
|
+
// Inline form: triggers: [a, b] or block form across lines.
|
|
89
|
+
const triggers = parseYamlArray(rest, lines, i);
|
|
90
|
+
data.triggers = triggers.values;
|
|
91
|
+
i = triggers.nextIndex;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Strip surrounding quotes if any.
|
|
96
|
+
if ((rest.startsWith('"') && rest.endsWith('"')) ||
|
|
97
|
+
(rest.startsWith("'") && rest.endsWith("'"))) {
|
|
98
|
+
rest = rest.slice(1, -1);
|
|
99
|
+
}
|
|
100
|
+
data[key] = rest;
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!data.name) {
|
|
105
|
+
// Fall back to the parent directory name when frontmatter omits it.
|
|
106
|
+
data.name = path.basename(path.dirname(filePath));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: data.name,
|
|
111
|
+
description: data.description || "",
|
|
112
|
+
triggers: Array.isArray(data.triggers) ? data.triggers : null,
|
|
113
|
+
path: filePath,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Parses either `[a, b, c]` on the same line as `triggers:`, or a block form:
|
|
118
|
+
// triggers:
|
|
119
|
+
// - "regex one"
|
|
120
|
+
// - "regex two"
|
|
121
|
+
// Returns { values, nextIndex } so the caller knows where to resume scanning.
|
|
122
|
+
function parseYamlArray(inlineRest, lines, currentIndex) {
|
|
123
|
+
const inline = inlineRest.trim();
|
|
124
|
+
if (inline.startsWith("[") && inline.endsWith("]")) {
|
|
125
|
+
const body = inline.slice(1, -1).trim();
|
|
126
|
+
if (!body) return { values: [], nextIndex: currentIndex + 1 };
|
|
127
|
+
const values = splitYamlInlineList(body);
|
|
128
|
+
return { values, nextIndex: currentIndex + 1 };
|
|
129
|
+
}
|
|
130
|
+
// Block form
|
|
131
|
+
const values = [];
|
|
132
|
+
let j = currentIndex + 1;
|
|
133
|
+
while (j < lines.length) {
|
|
134
|
+
const ln = lines[j];
|
|
135
|
+
const m = ln.match(/^\s*-\s*(.*)$/);
|
|
136
|
+
if (!m) break;
|
|
137
|
+
let item = m[1].trim();
|
|
138
|
+
if ((item.startsWith('"') && item.endsWith('"')) ||
|
|
139
|
+
(item.startsWith("'") && item.endsWith("'"))) {
|
|
140
|
+
item = item.slice(1, -1);
|
|
141
|
+
}
|
|
142
|
+
values.push(item);
|
|
143
|
+
j++;
|
|
144
|
+
}
|
|
145
|
+
return { values, nextIndex: j };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Splits `"a, b", c, "d"` respecting double-quoted strings so commas inside
|
|
149
|
+
// quotes don't break the split.
|
|
150
|
+
function splitYamlInlineList(body) {
|
|
151
|
+
const out = [];
|
|
152
|
+
let current = "";
|
|
153
|
+
let inQuote = null;
|
|
154
|
+
for (let k = 0; k < body.length; k++) {
|
|
155
|
+
const ch = body[k];
|
|
156
|
+
if (inQuote) {
|
|
157
|
+
if (ch === inQuote) {
|
|
158
|
+
inQuote = null;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
current += ch;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (ch === '"' || ch === "'") {
|
|
165
|
+
inQuote = ch;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === ",") {
|
|
169
|
+
const item = current.trim();
|
|
170
|
+
if (item) out.push(item);
|
|
171
|
+
current = "";
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
current += ch;
|
|
175
|
+
}
|
|
176
|
+
const tail = current.trim();
|
|
177
|
+
if (tail) out.push(tail);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build the regex list a skill matches against. When the SKILL.md provides
|
|
182
|
+
// `triggers:`, use those literal patterns. Otherwise auto-derive from:
|
|
183
|
+
// 1. The skill name (case-insensitive, word boundary).
|
|
184
|
+
// 2. Every single-quoted phrase in the description — descriptions in this
|
|
185
|
+
// project document trigger phrases as `'commit', 'pull request', ...`,
|
|
186
|
+
// so this captures idiomatic user wording the skill name alone misses.
|
|
187
|
+
// 3. The first 3 significant tokens of the description (length ≥ 4, not
|
|
188
|
+
// a stopword) — a fallback when no quoted phrases exist.
|
|
189
|
+
function buildPatterns(skill) {
|
|
190
|
+
if (skill.triggers && skill.triggers.length > 0) {
|
|
191
|
+
return skill.triggers.map(toRegex).filter(Boolean);
|
|
192
|
+
}
|
|
193
|
+
const patterns = [];
|
|
194
|
+
const description = skill.description || "";
|
|
195
|
+
|
|
196
|
+
if (skill.name) {
|
|
197
|
+
const escaped = skill.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
198
|
+
patterns.push(new RegExp(`\\b${escaped}\\b`, "i"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Extract single-quoted phrases like 'commit', 'pull request'. Limited to
|
|
202
|
+
// 12 to bound the per-prompt regex work.
|
|
203
|
+
const quoted = [...description.matchAll(/'([^']{2,60})'/g)].map((m) => m[1]);
|
|
204
|
+
const seenPhrase = new Set();
|
|
205
|
+
for (const phrase of quoted) {
|
|
206
|
+
const norm = phrase.toLowerCase().trim();
|
|
207
|
+
if (!norm || seenPhrase.has(norm)) continue;
|
|
208
|
+
seenPhrase.add(norm);
|
|
209
|
+
const escaped = norm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
210
|
+
patterns.push(new RegExp(`\\b${escaped}\\b`, "i"));
|
|
211
|
+
if (seenPhrase.size >= 12) break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (seenPhrase.size === 0) {
|
|
215
|
+
// Fallback: significant tokens.
|
|
216
|
+
const tokens = description
|
|
217
|
+
.toLowerCase()
|
|
218
|
+
.split(/[^a-z0-9-]+/)
|
|
219
|
+
.filter((t) => t.length >= 4 && !STOPWORDS.has(t));
|
|
220
|
+
const seenTok = new Set();
|
|
221
|
+
for (const tok of tokens) {
|
|
222
|
+
if (seenTok.has(tok)) continue;
|
|
223
|
+
seenTok.add(tok);
|
|
224
|
+
const escaped = tok.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
225
|
+
patterns.push(new RegExp(`\\b${escaped}\\b`, "i"));
|
|
226
|
+
if (seenTok.size >= 3) break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return patterns;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function toRegex(source) {
|
|
233
|
+
if (!source || typeof source !== "string") return null;
|
|
234
|
+
try {
|
|
235
|
+
return new RegExp(source, "i");
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
92
240
|
|
|
93
241
|
async function main() {
|
|
94
242
|
const chunks = [];
|
|
@@ -106,9 +254,14 @@ async function main() {
|
|
|
106
254
|
const prompt = (input.prompt || "").slice(0, 4000);
|
|
107
255
|
if (!prompt) process.exit(0);
|
|
108
256
|
|
|
257
|
+
const catalog = loadSkillCatalog(SKILLS_DIR);
|
|
258
|
+
if (catalog.length === 0) process.exit(0);
|
|
259
|
+
|
|
109
260
|
const matched = [];
|
|
110
|
-
for (const
|
|
111
|
-
|
|
261
|
+
for (const skill of catalog) {
|
|
262
|
+
const patterns = buildPatterns(skill);
|
|
263
|
+
if (patterns.length === 0) continue;
|
|
264
|
+
if (patterns.some((p) => p.test(prompt))) matched.push(skill.name);
|
|
112
265
|
}
|
|
113
266
|
|
|
114
267
|
if (matched.length === 0) process.exit(0);
|
package/hooks/statusline.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Hopla statusline: branch · worktree indicator · uncommitted count · active plan.
|
|
3
|
-
// Wire it up by
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// "command": "node ~/.claude/plugins/marketplaces/hopla-marketplace/hooks/statusline.js"
|
|
7
|
-
// }
|
|
3
|
+
// Wire it up by running:
|
|
4
|
+
// hopla-claude-setup --setup-statusline
|
|
5
|
+
// Remove with: hopla-claude-setup --remove-statusline
|
|
8
6
|
|
|
9
7
|
import { execSync } from "child_process";
|
|
10
8
|
import fs from "fs";
|
package/hooks/tsc-check.js
CHANGED
|
@@ -7,6 +7,25 @@ import { execSync } from "child_process";
|
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
9
|
|
|
10
|
+
const TS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
|
|
11
|
+
|
|
12
|
+
// Pulls every file_path the hook payload references. Covers Write/Edit (single
|
|
13
|
+
// file_path) and MultiEdit (edits[]). Returns null when no payload was available
|
|
14
|
+
// — callers treat null as "can't tell, run tsc to be safe".
|
|
15
|
+
function extractFilePaths(payload) {
|
|
16
|
+
const input = payload?.tool_input;
|
|
17
|
+
if (!input) return null;
|
|
18
|
+
const collected = new Set();
|
|
19
|
+
if (typeof input.file_path === "string") collected.add(input.file_path);
|
|
20
|
+
if (typeof input.path === "string") collected.add(input.path);
|
|
21
|
+
if (Array.isArray(input.edits)) {
|
|
22
|
+
for (const edit of input.edits) {
|
|
23
|
+
if (edit && typeof edit.file_path === "string") collected.add(edit.file_path);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return collected.size > 0 ? [...collected] : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
function resolveTscCommand(cwd) {
|
|
11
30
|
const localBin = path.join(cwd, "node_modules", ".bin", "tsc");
|
|
12
31
|
if (fs.existsSync(localBin)) return `"${localBin}"`;
|
|
@@ -16,10 +35,30 @@ function resolveTscCommand(cwd) {
|
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
async function main() {
|
|
19
|
-
// Drain stdin (hook contract) but we don't need the payload for tsc --noEmit
|
|
20
38
|
const chunks = [];
|
|
21
39
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
22
40
|
|
|
41
|
+
let payload = null;
|
|
42
|
+
if (chunks.length > 0) {
|
|
43
|
+
try {
|
|
44
|
+
payload = JSON.parse(Buffer.concat(chunks).toString());
|
|
45
|
+
} catch {
|
|
46
|
+
// Malformed payload — fall through; treat as "can't tell"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Filter: if every touched file is non-TS/JS, skip invoking tsc.
|
|
51
|
+
// When the payload is missing or unparseable we default to running tsc
|
|
52
|
+
// (safe behavior — matches the pre-filter implementation).
|
|
53
|
+
const touched = extractFilePaths(payload);
|
|
54
|
+
if (touched !== null) {
|
|
55
|
+
const hasCompilable = touched.some((f) => {
|
|
56
|
+
const ext = path.extname(f).toLowerCase();
|
|
57
|
+
return TS_EXTENSIONS.includes(ext);
|
|
58
|
+
});
|
|
59
|
+
if (!hasCompilable) process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
const cwd = process.cwd();
|
|
24
63
|
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
25
64
|
if (!fs.existsSync(tsconfigPath)) {
|
|
@@ -31,14 +70,12 @@ async function main() {
|
|
|
31
70
|
|
|
32
71
|
try {
|
|
33
72
|
execSync(`${tsc} --noEmit`, { cwd, stdio: "pipe" });
|
|
34
|
-
// Compile clean — exit silently
|
|
35
73
|
process.exit(0);
|
|
36
74
|
} catch (err) {
|
|
37
75
|
const output = (err.stdout || "").toString() + (err.stderr || "").toString();
|
|
38
76
|
if (output.trim()) {
|
|
39
77
|
process.stdout.write("TypeScript errors detected:\n" + output + "\n");
|
|
40
78
|
}
|
|
41
|
-
// PostToolUse hooks cannot block; stdout is fed back to Claude
|
|
42
79
|
process.exit(0);
|
|
43
80
|
}
|
|
44
81
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hopla/claude-setup",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "Agentic coding system for Claude Code — CLI for global rules + permissions (plugin available via /plugin marketplace add HOPLAtools/claude-setup)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"claude-setup": "cli.js",
|
|
8
7
|
"hopla-claude-setup": "cli.js"
|
|
9
8
|
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/HOPLAtools/claude-setup.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/HOPLAtools/claude-setup#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/HOPLAtools/claude-setup/issues"
|
|
16
|
+
},
|
|
10
17
|
"files": [
|
|
11
18
|
"cli.js",
|
|
12
19
|
"global-rules.md",
|
|
@@ -14,11 +21,14 @@
|
|
|
14
21
|
"skills/",
|
|
15
22
|
"agents/",
|
|
16
23
|
"hooks/",
|
|
17
|
-
".claude-plugin/"
|
|
24
|
+
".claude-plugin/",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"CHANGELOG.md"
|
|
18
27
|
],
|
|
19
28
|
"scripts": {
|
|
20
|
-
"prepublishOnly": "node scripts/check-versions.js",
|
|
21
|
-
"check-versions": "node scripts/check-versions.js"
|
|
29
|
+
"prepublishOnly": "node scripts/check-versions.js && npm test",
|
|
30
|
+
"check-versions": "node scripts/check-versions.js",
|
|
31
|
+
"test": "node --test tests/*.test.js tests/hooks/*.test.js"
|
|
22
32
|
},
|
|
23
33
|
"engines": {
|
|
24
34
|
"node": ">=18"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: brainstorm
|
|
3
|
-
description: "Design exploration and brainstorming before planning. Use when the user wants to explore options for a new feature, discuss approaches, design a solution, brainstorm ideas, or evaluate trade-offs. Trigger on: 'new feature', 'brainstorm', 'explore options', 'design', 'how should we', 'what approach', 'trade-offs'
|
|
3
|
+
description: "Design exploration and brainstorming before planning. Use when the user wants to explore options for a new feature, discuss approaches, design a solution, brainstorm ideas, or evaluate trade-offs. Trigger on: 'new feature', 'brainstorm', 'explore options', 'design', 'how should we', 'what approach', 'trade-offs'. Do NOT use when the user already has a clear plan or is asking to execute existing work."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Brainstorming: Design Exploration Before Planning
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-review
|
|
3
3
|
description: "Technical code review on changed files. Use when the user says 'review code', 'code review', 'check my code', 'review changes', 'look for bugs', or 'audit code'. Also use after completing implementation when validation passes. Do NOT use for reviewing plans or documents — only code."
|
|
4
|
+
triggers:
|
|
5
|
+
- "review (my |the |this )?code"
|
|
6
|
+
- "code review"
|
|
7
|
+
- "audit (my |the |this )?code"
|
|
8
|
+
- "look for bugs"
|
|
4
9
|
allowed-tools: Read, Grep, Glob, Bash
|
|
5
10
|
---
|
|
6
11
|
|
package/skills/debug/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug
|
|
3
|
-
description: "Systematic debugging methodology for finding and fixing bugs. Use when encountering errors, bugs, failures, unexpected behavior, or when the user says 'bug', 'error', 'not working', 'failing', 'debug', 'fix', 'broken'
|
|
3
|
+
description: "Systematic debugging methodology for finding and fixing bugs. Use when encountering errors, bugs, failures, unexpected behavior, or when the user says 'bug', 'error', 'not working', 'failing', 'debug', 'fix', 'broken'. Do NOT use for planned feature work or refactoring — only for diagnosing and fixing unexpected problems."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Systematic Debugging
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: performance
|
|
3
|
-
description: "Measured performance optimization workflow. Use when the user says 'slow', 'optimize', 'performance', 'bottleneck', 'too slow', 'high memory', 'high CPU',
|
|
3
|
+
description: "Measured performance optimization workflow. Use when the user says 'slow', 'optimize', 'performance', 'bottleneck', 'too slow', 'high memory', 'high CPU', or when asking to make something faster. Do NOT use for correctness bugs or new features — use the debug or plan-feature skills instead."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
> 🌐 **Language:** All user-facing output must match the user's language. Code, paths, and commands stay in English.
|
package/skills/verify/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: verify
|
|
3
|
-
description: "Verification gate that ensures all completion claims are backed by fresh evidence. Use when the agent is about to declare work as done, finished, complete, ready, or implemented. Also use when hearing '
|
|
3
|
+
description: "Verification gate that ensures all completion claims are backed by fresh evidence. Use when the agent is about to declare work as done, finished, complete, ready, or implemented. Also use when hearing 'done', 'finished', 'all tests pass', 'everything works', or any completion claim. Do NOT use for intermediate progress updates or partial task completion."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Verification Before Completion
|