@hopla/claude-setup 1.14.1 → 1.15.0

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.
@@ -0,0 +1,41 @@
1
+ {
2
+ "_comment": "Copy this file to your project root as .mcp.json and uncomment the servers you want Claude Code to use. Each server is standalone — enable only what you need. Set the required env vars in your shell or .envrc (NOT committed). Restart Claude Code after editing.",
3
+
4
+ "mcpServers": {
5
+ "_playwright_example": {
6
+ "_note": "Browser automation for E2E testing and UI inspection. No credentials needed.",
7
+ "command": "npx",
8
+ "args": ["-y", "@playwright/mcp"]
9
+ },
10
+
11
+ "_github_example": {
12
+ "_note": "GitHub API access for reading repos, issues, PRs. Requires a token with the scopes you need.",
13
+ "command": "npx",
14
+ "args": ["-y", "@modelcontextprotocol/server-github"],
15
+ "env": {
16
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
17
+ }
18
+ },
19
+
20
+ "_linear_example": {
21
+ "_note": "Linear workspace access (issues, projects, cycles). Get an API key at linear.app/settings/api.",
22
+ "command": "npx",
23
+ "args": ["-y", "@tacticlaunch/mcp-linear"],
24
+ "env": {
25
+ "LINEAR_API_KEY": "${LINEAR_API_KEY}"
26
+ }
27
+ },
28
+
29
+ "_postgres_example": {
30
+ "_note": "Read-only Postgres access for database introspection and ad-hoc queries.",
31
+ "command": "npx",
32
+ "args": ["-y", "@modelcontextprotocol/server-postgres", "${DATABASE_URL}"]
33
+ },
34
+
35
+ "_filesystem_example": {
36
+ "_note": "Constrained filesystem access outside the current workspace (e.g. to inspect ~/Documents/specs).",
37
+ "command": "npx",
38
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/ABSOLUTE/PATH/TO/ALLOWED/DIR"]
39
+ }
40
+ }
41
+ }
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "hopla",
11
11
  "description": "Agentic coding system: PIV loop, TDD, debugging, brainstorming, subagent execution, and team workflows",
12
- "version": "1.14.1",
12
+ "version": "1.15.0",
13
13
  "source": "./",
14
14
  "author": {
15
15
  "name": "Hopla Tools",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hopla",
3
3
  "description": "Agentic coding system for Claude Code: PIV loop (Plan → Implement → Validate), TDD, debugging, brainstorming, subagent execution, and team workflows",
4
- "version": "1.14.1",
4
+ "version": "1.15.0",
5
5
  "author": {
6
6
  "name": "Hopla Tools",
7
7
  "email": "julio@hopla.tools"
package/README.md CHANGED
@@ -101,10 +101,32 @@ Removes `~/.claude/CLAUDE.md` plus legacy `hopla-*` files from older installs.
101
101
  | `claude-setup --force` | Install without prompts |
102
102
  | `claude-setup --migrate` | Remove legacy CLI-installed duplicates only |
103
103
  | `claude-setup --uninstall` | Remove global rules + legacy files |
104
+ | `claude-setup --dry-run` | Preview changes without touching disk (composes with other flags) |
104
105
  | `claude-setup --version` | Print package version |
105
106
 
106
107
  ---
107
108
 
109
+ ## Optional: Hopla statusline
110
+
111
+ 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.
112
+
113
+ Enable it by adding to `~/.claude/settings.json`:
114
+
115
+ ```json
116
+ {
117
+ "statusLine": {
118
+ "type": "command",
119
+ "command": "node ~/.claude/plugins/marketplaces/hopla-marketplace/hooks/statusline.js"
120
+ }
121
+ }
122
+ ```
123
+
124
+ Then run `/reload-plugins` or restart Claude Code.
125
+
126
+ Sample output: ` feature/auth · 3M · 📋 add-authentication`
127
+
128
+ ---
129
+
108
130
  ## Naming Convention
109
131
 
110
132
  Skills and commands use short names in source (e.g., `prime`, `execute`, `git`). The plugin namespaces them automatically:
package/global-rules.md CHANGED
@@ -85,11 +85,21 @@ When suggesting a commit, explain in plain language why it's a good moment, adap
85
85
  ---
86
86
 
87
87
  ## 🔌 MCP Servers
88
- <!-- List your configured MCP servers here so the agent knows what tools are available -->
89
- <!-- Example: -->
90
- <!-- - Playwright: Browser automation for E2E testing -->
91
- <!-- - Supabase: Database management -->
92
- <!-- When planning features, explicitly include MCP integration points in the plan -->
88
+
89
+ Declare MCP servers in your project's `.mcp.json` so Claude Code picks them up automatically. Copy the starter template from the plugin:
90
+
91
+ ```bash
92
+ # Inside your project root
93
+ cp ~/.claude/plugins/marketplaces/hopla-marketplace/.claude-plugin/.mcp.json.example .mcp.json
94
+ # Then edit — uncomment the servers you need, set env vars in your shell
95
+ ```
96
+
97
+ List the servers you actually enabled here so the agent knows what tools are available, e.g.:
98
+ - Playwright — browser automation for E2E testing and UI inspection
99
+ - GitHub — PR/issue access (read and write depending on token scope)
100
+ - Linear — workspace tickets, cycles, projects
101
+
102
+ When planning features, explicitly include MCP integration points in the plan.
93
103
 
94
104
  ---
95
105
 
package/hooks/hooks.json CHANGED
@@ -35,6 +35,28 @@
35
35
  }
36
36
  ]
37
37
  }
38
+ ],
39
+ "UserPromptSubmit": [
40
+ {
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/prompt-route.js\"",
45
+ "async": false
46
+ }
47
+ ]
48
+ }
49
+ ],
50
+ "PreCompact": [
51
+ {
52
+ "hooks": [
53
+ {
54
+ "type": "command",
55
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/precompact-snapshot.js\"",
56
+ "async": false
57
+ }
58
+ ]
59
+ }
38
60
  ]
39
61
  }
40
62
  }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // PreCompact hook: snapshot the session's work state so it survives /compact.
3
+ // The SessionStart hook (session-prime.js) re-injects this when it exists and is recent.
4
+
5
+ import { execSync } from "child_process";
6
+ import fs from "fs";
7
+ import path from "path";
8
+
9
+ function run(cmd) {
10
+ try {
11
+ return execSync(cmd, { cwd: process.cwd(), stdio: "pipe" }).toString().trim();
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function findActivePlan() {
18
+ const plansDir = path.join(process.cwd(), ".agents", "plans");
19
+ if (!fs.existsSync(plansDir)) return null;
20
+ try {
21
+ const files = fs
22
+ .readdirSync(plansDir)
23
+ .filter((f) => f.endsWith(".md") && !f.startsWith("."))
24
+ .map((f) => ({ name: f, mtime: fs.statSync(path.join(plansDir, f)).mtimeMs }))
25
+ .sort((a, b) => b.mtime - a.mtime);
26
+ return files[0]?.name || null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function detectWorktree() {
33
+ const gitDir = run("git rev-parse --git-dir");
34
+ const commonDir = run("git rev-parse --git-common-dir");
35
+ if (!gitDir || !commonDir) return false;
36
+ return path.resolve(gitDir) !== path.resolve(commonDir);
37
+ }
38
+
39
+ async function main() {
40
+ // Drain stdin (hook contract)
41
+ const chunks = [];
42
+ for await (const chunk of process.stdin) chunks.push(chunk);
43
+
44
+ const snapshot = {
45
+ timestamp: new Date().toISOString(),
46
+ branch: run("git branch --show-current"),
47
+ uncommitted: run("git status --short"),
48
+ activePlan: findActivePlan(),
49
+ inWorktree: detectWorktree(),
50
+ };
51
+
52
+ const targetDir = path.join(process.cwd(), ".claude");
53
+ try {
54
+ fs.mkdirSync(targetDir, { recursive: true });
55
+ fs.writeFileSync(
56
+ path.join(targetDir, "compact-snapshot.json"),
57
+ JSON.stringify(snapshot, null, 2) + "\n"
58
+ );
59
+ } catch {
60
+ // Best-effort — never block /compact
61
+ }
62
+ process.exit(0);
63
+ }
64
+
65
+ main();
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit hook: scan the user's prompt for skill keywords and inject
3
+ // a short routing hint. Keeps skill suggestions visible mid-session even after
4
+ // compaction or long conversations where the initial skill list gets buried.
5
+
6
+ const SKILL_TRIGGERS = [
7
+ {
8
+ skill: "git",
9
+ patterns: [
10
+ /\bcommit\b/i,
11
+ /save (my |the )?changes/i,
12
+ /\bcreate (a )?pr\b/i,
13
+ /pull request/i,
14
+ /\bpush\b/i,
15
+ /merge request/i,
16
+ ],
17
+ },
18
+ {
19
+ skill: "worktree",
20
+ patterns: [
21
+ /\bworktree\b/i,
22
+ /isolated branch/i,
23
+ /parallel (feature|development)/i,
24
+ ],
25
+ },
26
+ {
27
+ skill: "prime",
28
+ patterns: [
29
+ /\borient(\b| yourself)/i,
30
+ /catch me up/i,
31
+ /get context/i,
32
+ /load project/i,
33
+ /what is this project/i,
34
+ ],
35
+ },
36
+ {
37
+ skill: "brainstorm",
38
+ patterns: [
39
+ /\bbrainstorm\b/i,
40
+ /explore (options|approaches)/i,
41
+ /how should we/i,
42
+ /trade[- ]offs?/i,
43
+ ],
44
+ },
45
+ {
46
+ skill: "debug",
47
+ patterns: [
48
+ /\bbug\b/i,
49
+ /\berror\b/i,
50
+ /\bdebug\b/i,
51
+ /not working/i,
52
+ /\bfailing\b/i,
53
+ /\bbroken\b/i,
54
+ /\bno funciona\b/i,
55
+ ],
56
+ },
57
+ {
58
+ skill: "verify",
59
+ patterns: [
60
+ /\bverify\b/i,
61
+ /all tests? pass/i,
62
+ /\blisto\b/i,
63
+ /\bterminé\b/i,
64
+ /\bya está\b/i,
65
+ ],
66
+ },
67
+ {
68
+ skill: "code-review",
69
+ patterns: [
70
+ /review (the |my )?code/i,
71
+ /code review/i,
72
+ /audit (the |my )?code/i,
73
+ ],
74
+ },
75
+ {
76
+ skill: "execution-report",
77
+ patterns: [
78
+ /generate (the |a )?report/i,
79
+ /document what was done/i,
80
+ /execution report/i,
81
+ ],
82
+ },
83
+ {
84
+ skill: "tdd",
85
+ patterns: [
86
+ /\btdd\b/i,
87
+ /test[- ]first/i,
88
+ /red[- ]green[- ]refactor/i,
89
+ ],
90
+ },
91
+ ];
92
+
93
+ async function main() {
94
+ const chunks = [];
95
+ for await (const chunk of process.stdin) chunks.push(chunk);
96
+
97
+ let input;
98
+ try {
99
+ input = JSON.parse(Buffer.concat(chunks).toString());
100
+ } catch {
101
+ // Malformed payload — skip silently so we never block legitimate prompts
102
+ process.exit(0);
103
+ }
104
+
105
+ // Cap prompt length so regex matching stays cheap on large paste-ins
106
+ const prompt = (input.prompt || "").slice(0, 4000);
107
+ if (!prompt) process.exit(0);
108
+
109
+ const matched = [];
110
+ for (const { skill, patterns } of SKILL_TRIGGERS) {
111
+ if (patterns.some((p) => p.test(prompt))) matched.push(skill);
112
+ }
113
+
114
+ if (matched.length === 0) process.exit(0);
115
+
116
+ const list = matched.map((s) => `\`${s}\``).join(", ");
117
+ const plural = matched.length > 1;
118
+ process.stdout.write(
119
+ `📦 HOPLA routing hint: this prompt matches skill${plural ? "s" : ""} ${list}. ` +
120
+ `Use ${plural ? "them" : "it"} if applicable to the current task.`
121
+ );
122
+ process.exit(0);
123
+ }
124
+
125
+ main();
@@ -65,6 +65,26 @@ async function main() {
65
65
  lines.push("Working tree is clean.");
66
66
  }
67
67
 
68
+ // Re-inject pre-compact snapshot if recent (< 2 hours old)
69
+ const snapshotPath = path.join(process.cwd(), ".claude", "compact-snapshot.json");
70
+ if (fs.existsSync(snapshotPath)) {
71
+ try {
72
+ const snap = JSON.parse(fs.readFileSync(snapshotPath, "utf8"));
73
+ const ageMs = Date.now() - Date.parse(snap.timestamp);
74
+ if (Number.isFinite(ageMs) && ageMs < 2 * 60 * 60 * 1000) {
75
+ const parts = [
76
+ `Resuming from pre-compact snapshot (${Math.round(ageMs / 60000)} min ago):`,
77
+ ];
78
+ if (snap.branch) parts.push(`- branch: ${snap.branch}${snap.inWorktree ? " (worktree)" : ""}`);
79
+ if (snap.activePlan) parts.push(`- active plan: .agents/plans/${snap.activePlan}`);
80
+ if (snap.uncommitted) parts.push(`- uncommitted at snapshot:\n${snap.uncommitted}`);
81
+ lines.push(parts.join("\n"));
82
+ }
83
+ } catch {
84
+ // Ignore malformed snapshot
85
+ }
86
+ }
87
+
68
88
  // CLAUDE.md summary (first 20 lines)
69
89
  const claudeMdPath = path.join(process.cwd(), "CLAUDE.md");
70
90
  if (fs.existsSync(claudeMdPath)) {
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ // Hopla statusline: branch · worktree indicator · uncommitted count · active plan.
3
+ // Wire it up by adding to ~/.claude/settings.json:
4
+ // "statusLine": {
5
+ // "type": "command",
6
+ // "command": "node ~/.claude/plugins/marketplaces/hopla-marketplace/hooks/statusline.js"
7
+ // }
8
+
9
+ import { execSync } from "child_process";
10
+ import fs from "fs";
11
+ import path from "path";
12
+
13
+ const CYAN = "\x1b[36m";
14
+ const YELLOW = "\x1b[33m";
15
+ const MAGENTA = "\x1b[35m";
16
+ const DIM = "\x1b[2m";
17
+ const RESET = "\x1b[0m";
18
+
19
+ function run(cmd, cwd) {
20
+ try {
21
+ return execSync(cmd, { cwd, stdio: "pipe" }).toString().trim();
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function findActivePlan(cwd) {
28
+ const plansDir = path.join(cwd, ".agents", "plans");
29
+ if (!fs.existsSync(plansDir)) return null;
30
+ try {
31
+ const files = fs
32
+ .readdirSync(plansDir)
33
+ .filter((f) => f.endsWith(".md") && !f.startsWith("."))
34
+ .map((f) => ({ name: f, mtime: fs.statSync(path.join(plansDir, f)).mtimeMs }))
35
+ .sort((a, b) => b.mtime - a.mtime);
36
+ return files[0]?.name.replace(/\.md$/, "") || null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ async function main() {
43
+ const chunks = [];
44
+ for await (const chunk of process.stdin) chunks.push(chunk);
45
+
46
+ let input = {};
47
+ try {
48
+ input = JSON.parse(Buffer.concat(chunks).toString());
49
+ } catch {
50
+ // Malformed payload — render nothing
51
+ process.exit(0);
52
+ }
53
+
54
+ const cwd = input.workspace?.current_dir || input.cwd || process.cwd();
55
+ const parts = [];
56
+
57
+ const branch = run("git branch --show-current", cwd);
58
+ if (branch) {
59
+ const gitDir = run("git rev-parse --git-dir", cwd);
60
+ const commonDir = run("git rev-parse --git-common-dir", cwd);
61
+ const isWorktree =
62
+ gitDir && commonDir && path.resolve(cwd, gitDir) !== path.resolve(cwd, commonDir);
63
+ parts.push(`${CYAN}${isWorktree ? "⎇ " : " "}${branch}${RESET}`);
64
+ }
65
+
66
+ const status = run("git status --short", cwd);
67
+ if (status) {
68
+ const count = status.split("\n").length;
69
+ parts.push(`${YELLOW}${count}M${RESET}`);
70
+ }
71
+
72
+ const plan = findActivePlan(cwd);
73
+ if (plan) {
74
+ parts.push(`${MAGENTA}📋 ${plan}${RESET}`);
75
+ }
76
+
77
+ if (parts.length === 0) process.exit(0);
78
+
79
+ process.stdout.write(parts.join(` ${DIM}·${RESET} `));
80
+ process.exit(0);
81
+ }
82
+
83
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hopla/claude-setup",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
4
4
  "description": "Hopla team agentic coding system for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {