create-merlin-brain 3.11.0 → 3.13.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.
Files changed (147) hide show
  1. package/bin/install.cjs +156 -32
  2. package/bin/runtime-adapters.cjs +396 -0
  3. package/dist/server/api/types.d.ts +7 -0
  4. package/dist/server/api/types.d.ts.map +1 -1
  5. package/dist/server/cost/tracker.d.ts +38 -2
  6. package/dist/server/cost/tracker.d.ts.map +1 -1
  7. package/dist/server/cost/tracker.js +87 -15
  8. package/dist/server/cost/tracker.js.map +1 -1
  9. package/dist/server/server.d.ts.map +1 -1
  10. package/dist/server/server.js +74 -30
  11. package/dist/server/server.js.map +1 -1
  12. package/dist/server/tools/__tests__/augmentation.test.d.ts +8 -0
  13. package/dist/server/tools/__tests__/augmentation.test.d.ts.map +1 -0
  14. package/dist/server/tools/__tests__/augmentation.test.js +76 -0
  15. package/dist/server/tools/__tests__/augmentation.test.js.map +1 -0
  16. package/dist/server/tools/__tests__/route-helpers.test.d.ts +5 -0
  17. package/dist/server/tools/__tests__/route-helpers.test.d.ts.map +1 -0
  18. package/dist/server/tools/__tests__/route-helpers.test.js +49 -0
  19. package/dist/server/tools/__tests__/route-helpers.test.js.map +1 -0
  20. package/dist/server/tools/adaptive.js +1 -1
  21. package/dist/server/tools/adaptive.js.map +1 -1
  22. package/dist/server/tools/agent-spawn.d.ts +25 -0
  23. package/dist/server/tools/agent-spawn.d.ts.map +1 -0
  24. package/dist/server/tools/agent-spawn.js +95 -0
  25. package/dist/server/tools/agent-spawn.js.map +1 -0
  26. package/dist/server/tools/agents-index.js +3 -3
  27. package/dist/server/tools/agents-index.js.map +1 -1
  28. package/dist/server/tools/agents.js +5 -5
  29. package/dist/server/tools/agents.js.map +1 -1
  30. package/dist/server/tools/augmentation.d.ts +45 -0
  31. package/dist/server/tools/augmentation.d.ts.map +1 -0
  32. package/dist/server/tools/augmentation.js +167 -0
  33. package/dist/server/tools/augmentation.js.map +1 -0
  34. package/dist/server/tools/behaviors.js +4 -4
  35. package/dist/server/tools/behaviors.js.map +1 -1
  36. package/dist/server/tools/context.js +7 -7
  37. package/dist/server/tools/context.js.map +1 -1
  38. package/dist/server/tools/cost.d.ts +3 -1
  39. package/dist/server/tools/cost.d.ts.map +1 -1
  40. package/dist/server/tools/cost.js +66 -13
  41. package/dist/server/tools/cost.js.map +1 -1
  42. package/dist/server/tools/discoveries.js +6 -6
  43. package/dist/server/tools/discoveries.js.map +1 -1
  44. package/dist/server/tools/index.d.ts +4 -0
  45. package/dist/server/tools/index.d.ts.map +1 -1
  46. package/dist/server/tools/index.js +4 -0
  47. package/dist/server/tools/index.js.map +1 -1
  48. package/dist/server/tools/learning.d.ts +12 -0
  49. package/dist/server/tools/learning.d.ts.map +1 -0
  50. package/dist/server/tools/learning.js +269 -0
  51. package/dist/server/tools/learning.js.map +1 -0
  52. package/dist/server/tools/project.js +7 -7
  53. package/dist/server/tools/project.js.map +1 -1
  54. package/dist/server/tools/promote.d.ts +11 -0
  55. package/dist/server/tools/promote.d.ts.map +1 -0
  56. package/dist/server/tools/promote.js +315 -0
  57. package/dist/server/tools/promote.js.map +1 -0
  58. package/dist/server/tools/route-helpers.d.ts +45 -0
  59. package/dist/server/tools/route-helpers.d.ts.map +1 -0
  60. package/dist/server/tools/route-helpers.js +93 -0
  61. package/dist/server/tools/route-helpers.js.map +1 -0
  62. package/dist/server/tools/route.d.ts +4 -3
  63. package/dist/server/tools/route.d.ts.map +1 -1
  64. package/dist/server/tools/route.js +80 -284
  65. package/dist/server/tools/route.js.map +1 -1
  66. package/dist/server/tools/session-restore.d.ts +18 -0
  67. package/dist/server/tools/session-restore.d.ts.map +1 -0
  68. package/dist/server/tools/session-restore.js +154 -0
  69. package/dist/server/tools/session-restore.js.map +1 -0
  70. package/dist/server/tools/session-search.d.ts +16 -0
  71. package/dist/server/tools/session-search.d.ts.map +1 -0
  72. package/dist/server/tools/session-search.js +240 -0
  73. package/dist/server/tools/session-search.js.map +1 -0
  74. package/dist/server/tools/sights-index.js +2 -2
  75. package/dist/server/tools/sights-index.js.map +1 -1
  76. package/dist/server/tools/smart-route.d.ts.map +1 -1
  77. package/dist/server/tools/smart-route.js +4 -5
  78. package/dist/server/tools/smart-route.js.map +1 -1
  79. package/dist/server/tools/verification.js +1 -1
  80. package/dist/server/tools/verification.js.map +1 -1
  81. package/files/agents/code-organization-supervisor.md +1 -0
  82. package/files/agents/context-guardian.md +1 -0
  83. package/files/agents/docs-keeper.md +1 -0
  84. package/files/agents/dry-refactor.md +1 -0
  85. package/files/agents/elite-code-refactorer.md +1 -0
  86. package/files/agents/hardening-guard.md +1 -0
  87. package/files/agents/implementation-dev.md +1 -0
  88. package/files/agents/merlin-access-control-reviewer.md +248 -0
  89. package/files/agents/merlin-codebase-mapper.md +1 -1
  90. package/files/agents/merlin-dependency-auditor.md +216 -0
  91. package/files/agents/merlin-executor.md +1 -0
  92. package/files/agents/merlin-input-validator.md +247 -0
  93. package/files/agents/merlin-reviewer.md +1 -0
  94. package/files/agents/merlin-sast-reviewer.md +182 -0
  95. package/files/agents/merlin-secret-scanner.md +203 -0
  96. package/files/agents/tests-qa.md +1 -0
  97. package/files/commands/merlin/execute-phase.md +94 -197
  98. package/files/commands/merlin/execute-plan.md +116 -180
  99. package/files/commands/merlin/health.md +385 -0
  100. package/files/commands/merlin/loop-recipes.md +93 -36
  101. package/files/commands/merlin/optimize-prompts.md +158 -0
  102. package/files/commands/merlin/profiles.md +215 -0
  103. package/files/commands/merlin/promote.md +176 -0
  104. package/files/commands/merlin/quick.md +229 -0
  105. package/files/commands/merlin/resume-work.md +27 -1
  106. package/files/commands/merlin/route.md +43 -1
  107. package/files/commands/merlin/sandbox.md +359 -0
  108. package/files/commands/merlin/usage.md +55 -0
  109. package/files/docker/Dockerfile.merlin +20 -0
  110. package/files/docker/docker-compose.merlin.yml +23 -0
  111. package/files/hook-templates/auto-commit.sh +64 -0
  112. package/files/hook-templates/auto-format.sh +95 -0
  113. package/files/hook-templates/auto-test.sh +117 -0
  114. package/files/hook-templates/branch-protection.sh +72 -0
  115. package/files/hook-templates/changelog-reminder.sh +76 -0
  116. package/files/hook-templates/complexity-check.sh +112 -0
  117. package/files/hook-templates/import-audit.sh +83 -0
  118. package/files/hook-templates/license-header.sh +84 -0
  119. package/files/hook-templates/pr-description.sh +100 -0
  120. package/files/hook-templates/todo-tracker.sh +80 -0
  121. package/files/hooks/check-file-size.sh +17 -4
  122. package/files/hooks/config-change.sh +44 -16
  123. package/files/hooks/instructions-loaded.sh +22 -5
  124. package/files/hooks/notify-desktop.sh +157 -0
  125. package/files/hooks/notify-webhook.sh +141 -0
  126. package/files/hooks/pre-edit-sights-check.sh +76 -9
  127. package/files/hooks/security-scanner.sh +153 -0
  128. package/files/hooks/session-end-memory-sync.sh +97 -0
  129. package/files/hooks/session-end.sh +274 -1
  130. package/files/hooks/session-start.sh +19 -6
  131. package/files/hooks/smart-approve.sh +270 -0
  132. package/files/hooks/teammate-idle-verify.sh +87 -12
  133. package/files/hooks/worktree-create.sh +20 -3
  134. package/files/hooks/worktree-remove.sh +21 -3
  135. package/files/merlin/references/plan-format.md +37 -9
  136. package/files/merlin/sandbox.json +9 -0
  137. package/files/merlin/security.json +11 -0
  138. package/files/merlin/templates/ci/docs-update.yml +81 -0
  139. package/files/merlin/templates/ci/pr-review.yml +50 -0
  140. package/files/merlin/templates/ci/security-audit.yml +74 -0
  141. package/files/merlin/templates/config.json +9 -1
  142. package/files/rules/api-rules.md +30 -0
  143. package/files/rules/frontend-rules.md +25 -0
  144. package/files/rules/hooks-rules.md +36 -0
  145. package/files/rules/mcp-rules.md +30 -0
  146. package/files/rules/worker-rules.md +29 -0
  147. package/package.json +5 -2
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Hook Template: todo-tracker.sh
4
+ # Event: PostToolUse (Write, Edit)
5
+ #
6
+ # Scans modified files for new TODO, FIXME, HACK, XXX, and TEMP comments.
7
+ # Reports them so they don't silently accumulate without notice.
8
+ #
9
+ # HOW TO INSTALL:
10
+ # Copy this file to ~/.claude/merlin/hooks/todo-tracker.sh
11
+ # Then add to your .claude/settings.local.json:
12
+ #
13
+ # {
14
+ # "hooks": {
15
+ # "PostToolUse": [
16
+ # {
17
+ # "matcher": "Write|Edit",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/merlin/hooks/todo-tracker.sh" }]
19
+ # }
20
+ # ]
21
+ # }
22
+ # }
23
+ #
24
+ # BEHAVIOR: Advisory only. Lists new TODO-style comments found in the changed file.
25
+ # Exits 0 (no blocking) — just surfaces the information.
26
+ #
27
+ set -euo pipefail
28
+ trap 'echo "{}"; exit 0' ERR
29
+
30
+ TODO_PATTERN='TODO|FIXME|HACK|XXX|TEMP|NOCOMMIT|REMOVEME'
31
+
32
+ # Read tool input from stdin
33
+ input=""
34
+ if [ ! -t 0 ]; then
35
+ input=$(cat 2>/dev/null || true)
36
+ fi
37
+
38
+ [ -z "$input" ] && { echo '{}'; exit 0; }
39
+
40
+ # Extract file path
41
+ file_path=""
42
+ if command -v jq >/dev/null 2>&1; then
43
+ file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)
44
+ fi
45
+
46
+ [ -z "$file_path" ] || [ ! -f "$file_path" ] && { echo '{}'; exit 0; }
47
+
48
+ # Only scan source code files
49
+ case "$file_path" in
50
+ *.js|*.jsx|*.ts|*.tsx|*.mjs|*.py|*.go|*.java|*.rb|*.rs|*.swift|*.c|*.cpp|*.h|*.sh|*.md)
51
+ ;;
52
+ *)
53
+ echo '{}'
54
+ exit 0
55
+ ;;
56
+ esac
57
+
58
+ # Skip node_modules and generated files
59
+ case "$file_path" in
60
+ *node_modules*|*dist/*|*build/*|*vendor/*)
61
+ echo '{}'
62
+ exit 0
63
+ ;;
64
+ esac
65
+
66
+ # Find TODO-style comments with line numbers
67
+ todos=$(grep -nE "$TODO_PATTERN" "$file_path" 2>/dev/null | head -20 || true)
68
+
69
+ if [ -n "$todos" ]; then
70
+ count=$(echo "$todos" | wc -l | tr -d ' ')
71
+ echo "TODO TRACKER: ${count} annotation(s) found in ${file_path}:" >&2
72
+ echo "$todos" | while IFS= read -r line; do
73
+ echo " $line" >&2
74
+ done
75
+ echo " Track these in your issue tracker to avoid tech debt accumulation." >&2
76
+ fi
77
+
78
+ # Always exit 0 — informational only, never blocking
79
+ echo '{}'
80
+ exit 0
@@ -4,16 +4,29 @@
4
4
  # Checks if the modified file exceeds the 400-line convention.
5
5
  # Exits with code 2 to inject feedback when file is too large.
6
6
  #
7
- # Agent-type awareness: docs-keeper and merlin-verifier agents are exempt
8
- # from the 400-line limit (they work with doc/verification files).
7
+ # Agent-type awareness: only enforce for implementation agents.
8
+ # Non-code agents (docs, review, verify, etc.) are fully exempt.
9
9
  #
10
10
  set -euo pipefail
11
11
  trap 'echo "{}"; exit 0' ERR
12
12
 
13
- # Check agent typeskip enforcement for non-implementation agents
13
+ # Read CLAUDE_AGENT_TYPE from env support both var names
14
14
  AGENT_TYPE="${CLAUDE_AGENT_TYPE:-${CLAUDE_CODE_AGENT_TYPE:-main}}"
15
+
16
+ # Only enforce for implementation agents
17
+ # Skip for all doc/review/verification/analysis agents
15
18
  case "$AGENT_TYPE" in
16
- docs-keeper|merlin-verifier)
19
+ implementation-dev|merlin-executor)
20
+ # Enforcement is active for these agents — continue
21
+ ;;
22
+ docs-keeper|merlin-reviewer|merlin-verifier|merlin-milestone-auditor|\
23
+ merlin-integration-checker|merlin-work-verifier|code-organization-supervisor|\
24
+ context-guardian|dry-refactor|tests-qa|merlin-codebase-mapper)
25
+ echo '{}'
26
+ exit 0
27
+ ;;
28
+ *)
29
+ # For all other agents (including 'main'), skip enforcement
17
30
  echo '{}'
18
31
  exit 0
19
32
  ;;
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # Merlin Hook: ConfigChange
4
4
  # Purpose: Validate Merlin MCP key still works after config change.
5
- # Also logs config changes to Sights for enterprise audit trail.
5
+ # Logs config change events to Sights for audit trail.
6
6
  #
7
7
  # Always exits 0 — never blocks Claude Code.
8
8
  #
@@ -13,31 +13,59 @@ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
 
14
14
  # Source shared libs if available
15
15
  [ -f "${HOOK_DIR}/lib/analytics.sh" ] && . "${HOOK_DIR}/lib/analytics.sh"
16
+ [ -f "${HOOK_DIR}/lib/sights-check.sh" ] && . "${HOOK_DIR}/lib/sights-check.sh"
16
17
 
17
- MERLIN_API_KEY="${MERLIN_API_KEY:-}"
18
18
  AGENT_ID="${CLAUDE_AGENT_ID:-unknown}"
19
19
  AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
20
+ CLAUDE_DIR="${HOME}/.claude"
21
+
22
+ # Resolve API key: env var takes priority, fall back to config.json
23
+ MERLIN_API_KEY="${MERLIN_API_KEY:-}"
24
+ if [ -z "$MERLIN_API_KEY" ] && declare -f get_merlin_api_key >/dev/null 2>&1; then
25
+ MERLIN_API_KEY="$(get_merlin_api_key)"
26
+ fi
27
+
28
+ # Detect which MCP servers are configured for audit trail
29
+ MCP_SERVERS=""
30
+ if [ -f "${CLAUDE_DIR}/config.json" ] && command -v jq >/dev/null 2>&1; then
31
+ MCP_SERVERS=$(jq -r '(.mcpServers // {}) | keys | join(",")' \
32
+ "${CLAUDE_DIR}/config.json" 2>/dev/null || echo "")
33
+ fi
34
+
35
+ HAS_KEY="$([ -n "$MERLIN_API_KEY" ] && echo true || echo false)"
36
+ KEY_VALID="unknown"
37
+
38
+ # Validate key format if present (valid prefixes: mrln_ or ccw_)
39
+ if [ -n "$MERLIN_API_KEY" ]; then
40
+ case "$MERLIN_API_KEY" in
41
+ mrln_*|ccw_*)
42
+ KEY_VALID="true"
43
+ ;;
44
+ *)
45
+ KEY_VALID="false"
46
+ echo "Merlin: API key has unexpected format after config change" >&2
47
+ ;;
48
+ esac
49
+ fi
50
+
51
+ # Write audit trail entry to local log
52
+ AUDIT_DIR="${HOME}/.claude/merlin/audit"
53
+ mkdir -p "$AUDIT_DIR" 2>/dev/null || true
54
+ printf '{"ts":"%s","agent_id":"%s","agent_type":"%s","has_key":%s,"key_valid":"%s","mcp_servers":"%s"}\n' \
55
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
56
+ "$AGENT_ID" "$AGENT_TYPE" "$HAS_KEY" "$KEY_VALID" "$MCP_SERVERS" \
57
+ >> "${AUDIT_DIR}/config-changes.log" 2>/dev/null || true
20
58
 
21
59
  # Log config change event if analytics available
22
60
  if declare -f log_event >/dev/null 2>&1; then
23
- log_event "config_change" "$(printf '{"agent_id":"%s","agent_type":"%s","has_key":"%s"}' "$AGENT_ID" "$AGENT_TYPE" "$([ -n "$MERLIN_API_KEY" ] && echo true || echo false)")"
61
+ log_event "config_change" "$(printf \
62
+ '{"agent_id":"%s","agent_type":"%s","has_key":%s,"key_valid":"%s","mcp_servers":"%s"}' \
63
+ "$AGENT_ID" "$AGENT_TYPE" "$HAS_KEY" "$KEY_VALID" "$MCP_SERVERS")"
24
64
  fi
25
65
 
26
66
  if [ -z "$MERLIN_API_KEY" ]; then
27
- echo "Merlin: No API key configured after config change" >&2
28
- echo '{}'
29
- exit 0
67
+ echo "Merlin: No API key configured Sights features disabled" >&2
30
68
  fi
31
69
 
32
- # Quick validation — check key format (valid prefixes: mrln_ or ccw_)
33
- case "$MERLIN_API_KEY" in
34
- mrln_*|ccw_*)
35
- # Valid prefix — no-op
36
- ;;
37
- *)
38
- echo "Merlin: API key has unexpected format after config change" >&2
39
- ;;
40
- esac
41
-
42
70
  echo '{}'
43
71
  exit 0
@@ -4,6 +4,9 @@
4
4
  # Purpose: Pre-warm Merlin Sights connection when CLAUDE.md loads.
5
5
  # Fires BEFORE session-start, giving us a head start on context loading.
6
6
  #
7
+ # Cold-start optimization: touching the sights-check timestamp file signals
8
+ # that Merlin is active so the first pre-edit check doesn't fire a warning.
9
+ #
7
10
  # Always exits 0 — never blocks Claude Code startup.
8
11
  #
9
12
  set -euo pipefail
@@ -17,18 +20,32 @@ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
20
  # shellcheck source=lib/sights-check.sh
18
21
  [ -f "${HOOK_DIR}/lib/sights-check.sh" ] && . "${HOOK_DIR}/lib/sights-check.sh"
19
22
 
20
- # Log agent context if available
21
23
  AGENT_ID="${CLAUDE_AGENT_ID:-unknown}"
22
24
  AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
25
+ MERLIN_DIR="${HOME}/.claude/merlin"
26
+
27
+ # Cold-start pre-warm: record that Sights is active so the pre-edit hook
28
+ # doesn't fire a "Sights not consulted" warning immediately after boot.
29
+ # Only touch if the file is stale (>300s old) or missing — avoids
30
+ # re-stamping on every CLAUDE.md reload within the same session.
31
+ if declare -f record_sights_call >/dev/null 2>&1; then
32
+ if ! sights_was_checked_recently 300 2>/dev/null; then
33
+ record_sights_call 2>/dev/null || true
34
+ fi
35
+ fi
36
+
37
+ # Write a lightweight cold-start marker so session-start can detect
38
+ # that instructions were already loaded (avoids double context fetch).
39
+ mkdir -p "${MERLIN_DIR}" 2>/dev/null || true
40
+ printf '%s' "$(date +%s)" > "${MERLIN_DIR}/.instructions-loaded-ts" 2>/dev/null || true
23
41
 
24
42
  # Log the instructions-loaded event if analytics is available
25
43
  if declare -f log_event >/dev/null 2>&1; then
26
- log_event "instructions_loaded" "$(printf '{"agent_id":"%s","agent_type":"%s","cwd":"%s"}' "$AGENT_ID" "$AGENT_TYPE" "${PWD:-}")"
44
+ log_event "instructions_loaded" "$(printf '{"agent_id":"%s","agent_type":"%s","cwd":"%s"}' \
45
+ "$AGENT_ID" "$AGENT_TYPE" "${PWD:-}")"
27
46
  fi
28
47
 
29
- # Signal that Merlin instructions were loaded
30
- # The MCP server will handle actual Sights connection
31
- echo "Merlin instructions loaded — Sights pre-warming (agent: ${AGENT_TYPE})" >&2
48
+ echo "Merlin instructions loaded — Sights pre-warmed (agent: ${AGENT_TYPE})" >&2
32
49
 
33
50
  echo '{}'
34
51
  exit 0
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Merlin Hook: notify-desktop.sh
4
+ # Events: Notification, Stop
5
+ #
6
+ # Sends a desktop notification when Claude finishes work or needs input.
7
+ # Reads notification config from .merlin/config.json in the project root.
8
+ # Always exits 0 — notifications must never block the main session.
9
+ #
10
+ set -euo pipefail
11
+ trap 'echo "{}"; exit 0' ERR
12
+
13
+ # ── Config resolution ─────────────────────────────────────────────
14
+ # Resolve project root: prefer CLAUDE_WORKTREE_PATH, fall back to PWD
15
+ PROJECT_ROOT="${CLAUDE_WORKTREE_PATH:-${PWD:-$(pwd)}}"
16
+ CONFIG_FILE="${PROJECT_ROOT}/.merlin/config.json"
17
+
18
+ # Read a value from .merlin/config.json using jq or python3 as fallback
19
+ _read_config() {
20
+ local key="${1}" default="${2:-}"
21
+ if [ ! -f "${CONFIG_FILE}" ]; then
22
+ echo "${default}"
23
+ return 0
24
+ fi
25
+ local val
26
+ if command -v jq >/dev/null 2>&1; then
27
+ val=$(jq -r "${key} // \"${default}\"" "${CONFIG_FILE}" 2>/dev/null) || val="${default}"
28
+ elif command -v python3 >/dev/null 2>&1; then
29
+ val=$(python3 -c "
30
+ import json, sys
31
+ try:
32
+ d = json.load(open('${CONFIG_FILE}'))
33
+ keys = '${key}'.lstrip('.').split('.')
34
+ v = d
35
+ for k in keys:
36
+ v = v.get(k, None)
37
+ if v is None:
38
+ break
39
+ print(v if v is not None else '${default}')
40
+ except:
41
+ print('${default}')
42
+ " 2>/dev/null) || val="${default}"
43
+ else
44
+ val="${default}"
45
+ fi
46
+ echo "${val}"
47
+ }
48
+
49
+ # Check if desktop notifications are enabled (default: true)
50
+ DESKTOP_ENABLED=$(_read_config '.notifications.desktop' 'true')
51
+ if [ "${DESKTOP_ENABLED}" = "false" ]; then
52
+ echo '{}'
53
+ exit 0
54
+ fi
55
+
56
+ # Check notify_on list to see if this event type is included
57
+ HOOK_EVENT="${CLAUDE_HOOK_EVENT:-Stop}"
58
+ NOTIFY_ON=$(_read_config '.notifications.notify_on' '["stop","needs_input","error"]')
59
+
60
+ # Map hook event names to notify_on values
61
+ case "${HOOK_EVENT}" in
62
+ Stop) EVENT_KEY="stop" ;;
63
+ Notification) EVENT_KEY="needs_input" ;;
64
+ *) EVENT_KEY="stop" ;;
65
+ esac
66
+
67
+ # Check if this event is in the notify_on list
68
+ EVENT_ENABLED=false
69
+ if echo "${NOTIFY_ON}" | grep -q "\"${EVENT_KEY}\"" 2>/dev/null; then
70
+ EVENT_ENABLED=true
71
+ fi
72
+ # Default to true if we couldn't parse (degraded gracefully)
73
+ if [ "${NOTIFY_ON}" = '["stop","needs_input","error"]' ]; then
74
+ EVENT_ENABLED=true
75
+ fi
76
+
77
+ if [ "${EVENT_ENABLED}" = "false" ]; then
78
+ echo '{}'
79
+ exit 0
80
+ fi
81
+
82
+ # ── Build notification message ─────────────────────────────────────
83
+ # Read stdin for hook context (non-blocking, best-effort)
84
+ HOOK_INPUT=""
85
+ if [ -t 0 ]; then
86
+ # stdin is a terminal, no piped input
87
+ HOOK_INPUT=""
88
+ else
89
+ HOOK_INPUT=$(cat 2>/dev/null || true)
90
+ fi
91
+
92
+ # Determine message based on event type
93
+ case "${HOOK_EVENT}" in
94
+ Stop)
95
+ # Extract agent type for context if available
96
+ AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
97
+ if [ "${AGENT_TYPE}" != "main" ] && [ "${AGENT_TYPE}" != "unknown" ]; then
98
+ MESSAGE="Task complete (${AGENT_TYPE} agent)"
99
+ else
100
+ MESSAGE="Task complete — Claude has finished working"
101
+ fi
102
+ ;;
103
+ Notification)
104
+ MESSAGE="Claude needs your input"
105
+ ;;
106
+ *)
107
+ MESSAGE="Merlin: Task complete"
108
+ ;;
109
+ esac
110
+
111
+ TITLE="Merlin"
112
+ SOUND_ENABLED=$(_read_config '.notifications.sound' 'false')
113
+
114
+ # ── Detect OS and send notification ───────────────────────────────
115
+ OS_TYPE="$(uname -s 2>/dev/null || echo 'unknown')"
116
+
117
+ case "${OS_TYPE}" in
118
+ Darwin)
119
+ # macOS — use osascript
120
+ osascript -e "display notification \"${MESSAGE}\" with title \"${TITLE}\" sound name \"Glass\"" \
121
+ >/dev/null 2>&1 &
122
+
123
+ # Optional extra sound (beyond what osascript plays)
124
+ if [ "${SOUND_ENABLED}" = "true" ]; then
125
+ SOUND_FILE="/System/Library/Sounds/Glass.aiff"
126
+ if [ -f "${SOUND_FILE}" ]; then
127
+ afplay "${SOUND_FILE}" >/dev/null 2>&1 &
128
+ fi
129
+ fi
130
+ ;;
131
+
132
+ Linux)
133
+ # Linux — use notify-send if available
134
+ if command -v notify-send >/dev/null 2>&1; then
135
+ notify-send "${TITLE}" "${MESSAGE}" --icon=dialog-information \
136
+ >/dev/null 2>&1 &
137
+ fi
138
+
139
+ # Optional sound on Linux
140
+ if [ "${SOUND_ENABLED}" = "true" ]; then
141
+ SOUND_FILE="/usr/share/sounds/freedesktop/stereo/complete.oga"
142
+ if [ -f "${SOUND_FILE}" ] && command -v paplay >/dev/null 2>&1; then
143
+ paplay "${SOUND_FILE}" >/dev/null 2>&1 &
144
+ fi
145
+ fi
146
+ ;;
147
+
148
+ *)
149
+ # Unknown OS — exit silently
150
+ echo '{}'
151
+ exit 0
152
+ ;;
153
+ esac
154
+
155
+ # Claude Code command hooks must output valid JSON to stdout
156
+ echo '{}'
157
+ exit 0
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Merlin Hook: notify-webhook.sh
4
+ # Event: Stop
5
+ #
6
+ # Fires Slack and/or Discord webhooks when Claude finishes a task.
7
+ # Reads webhook URLs from .merlin/config.json in the project root.
8
+ # All curl calls are fire-and-forget (background, 5s timeout).
9
+ # Always exits 0 — webhooks must never block the main session.
10
+ #
11
+ set -euo pipefail
12
+ trap 'echo "{}"; exit 0' ERR
13
+
14
+ # ── Require curl ──────────────────────────────────────────────────
15
+ if ! command -v curl >/dev/null 2>&1; then
16
+ echo '{}'
17
+ exit 0
18
+ fi
19
+
20
+ # ── Config resolution ─────────────────────────────────────────────
21
+ PROJECT_ROOT="${CLAUDE_WORKTREE_PATH:-${PWD:-$(pwd)}}"
22
+ CONFIG_FILE="${PROJECT_ROOT}/.merlin/config.json"
23
+
24
+ _read_config() {
25
+ local key="${1}" default="${2:-}"
26
+ if [ ! -f "${CONFIG_FILE}" ]; then
27
+ echo "${default}"
28
+ return 0
29
+ fi
30
+ local val
31
+ if command -v jq >/dev/null 2>&1; then
32
+ val=$(jq -r "${key} // \"${default}\"" "${CONFIG_FILE}" 2>/dev/null) || val="${default}"
33
+ elif command -v python3 >/dev/null 2>&1; then
34
+ val=$(python3 -c "
35
+ import json, sys
36
+ try:
37
+ d = json.load(open('${CONFIG_FILE}'))
38
+ keys = '${key}'.lstrip('.').split('.')
39
+ v = d
40
+ for k in keys:
41
+ v = v.get(k, None)
42
+ if v is None:
43
+ break
44
+ print(v if v is not None else '${default}')
45
+ except:
46
+ print('${default}')
47
+ " 2>/dev/null) || val="${default}"
48
+ else
49
+ val="${default}"
50
+ fi
51
+ echo "${val}"
52
+ }
53
+
54
+ SLACK_WEBHOOK=$(_read_config '.notifications.slack_webhook' '')
55
+ DISCORD_WEBHOOK=$(_read_config '.notifications.discord_webhook' '')
56
+
57
+ # Exit early if no webhooks are configured
58
+ if [ -z "${SLACK_WEBHOOK}" ] && [ -z "${DISCORD_WEBHOOK}" ]; then
59
+ echo '{}'
60
+ exit 0
61
+ fi
62
+
63
+ # ── Build context ─────────────────────────────────────────────────
64
+ AGENT_NAME="${CLAUDE_AGENT_TYPE:-main}"
65
+ if [ "${AGENT_NAME}" = "unknown" ] || [ -z "${AGENT_NAME}" ]; then
66
+ AGENT_NAME="main"
67
+ fi
68
+
69
+ # Calculate session duration from session analytics file if available
70
+ SESSION_ID="${MERLIN_SESSION_ID:-}"
71
+ DURATION_STR="unknown"
72
+ if [ -n "${SESSION_ID}" ]; then
73
+ SESSION_FILE="${HOME}/.claude/merlin/analytics/session-${SESSION_ID}.json"
74
+ if [ -f "${SESSION_FILE}" ] && command -v jq >/dev/null 2>&1; then
75
+ START_TS=$(jq -r '.startTime // ""' "${SESSION_FILE}" 2>/dev/null || echo "")
76
+ if [ -n "${START_TS}" ]; then
77
+ START_EPOCH=$(date -d "${START_TS}" +%s 2>/dev/null \
78
+ || python3 -c "import datetime; print(int(datetime.datetime.fromisoformat('${START_TS}'.replace('Z','+00:00')).timestamp()))" 2>/dev/null \
79
+ || echo "")
80
+ if [ -n "${START_EPOCH}" ]; then
81
+ NOW_EPOCH=$(date +%s)
82
+ ELAPSED=$(( NOW_EPOCH - START_EPOCH ))
83
+ MINUTES=$(( ELAPSED / 60 ))
84
+ SECONDS=$(( ELAPSED % 60 ))
85
+ DURATION_STR="${MINUTES}m ${SECONDS}s"
86
+ fi
87
+ fi
88
+ fi
89
+ fi
90
+
91
+ STATUS_TEXT="Success"
92
+ STATUS_ICON="OK"
93
+
94
+ # ── Slack webhook ─────────────────────────────────────────────────
95
+ if [ -n "${SLACK_WEBHOOK}" ]; then
96
+ SLACK_BODY=$(cat <<SLACK_JSON
97
+ {
98
+ "text": "Merlin: Task completed",
99
+ "blocks": [
100
+ {
101
+ "type": "section",
102
+ "text": {
103
+ "type": "mrkdwn",
104
+ "text": "*Merlin Task Complete*\n\nAgent: ${AGENT_NAME}\nDuration: ${DURATION_STR}\nStatus: ${STATUS_ICON} ${STATUS_TEXT}"
105
+ }
106
+ }
107
+ ]
108
+ }
109
+ SLACK_JSON
110
+ )
111
+ curl \
112
+ --max-time 5 \
113
+ --silent \
114
+ --output /dev/null \
115
+ --request POST \
116
+ --header "Content-Type: application/json" \
117
+ --data "${SLACK_BODY}" \
118
+ "${SLACK_WEBHOOK}" &
119
+ fi
120
+
121
+ # ── Discord webhook ───────────────────────────────────────────────
122
+ if [ -n "${DISCORD_WEBHOOK}" ]; then
123
+ DISCORD_BODY=$(cat <<DISCORD_JSON
124
+ {
125
+ "content": "**Merlin Task Complete**\n\nAgent: ${AGENT_NAME}\nDuration: ${DURATION_STR}\nStatus: ${STATUS_ICON} ${STATUS_TEXT}"
126
+ }
127
+ DISCORD_JSON
128
+ )
129
+ curl \
130
+ --max-time 5 \
131
+ --silent \
132
+ --output /dev/null \
133
+ --request POST \
134
+ --header "Content-Type: application/json" \
135
+ --data "${DISCORD_BODY}" \
136
+ "${DISCORD_WEBHOOK}" &
137
+ fi
138
+
139
+ # Claude Code command hooks must output valid JSON to stdout
140
+ echo '{}'
141
+ exit 0
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
3
  # Merlin Hook: PreToolUse (Edit/Write)
4
- # Checks if Sights was consulted recently before file edits.
5
- # Advisory onlyalways exits 0, never blocks edits.
4
+ # Two responsibilities:
5
+ # 1. Secret detectionblocks writes that contain leaked credentials.
6
+ # Exit code 2 with a clear message. This is the only hard block.
7
+ # 2. Sights consultation check — advisory warning if Sights was not
8
+ # consulted recently. Always exits 0 (never blocks on this).
6
9
  #
7
10
  set -euo pipefail
8
11
  trap 'echo "{}"; exit 0' ERR
@@ -11,9 +14,9 @@ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
14
 
12
15
  # Source shared libraries
13
16
  # shellcheck source=lib/analytics.sh
14
- . "${HOOKS_DIR}/lib/analytics.sh"
17
+ [ -f "${HOOKS_DIR}/lib/analytics.sh" ] && . "${HOOKS_DIR}/lib/analytics.sh"
15
18
  # shellcheck source=lib/sights-check.sh
16
- . "${HOOKS_DIR}/lib/sights-check.sh"
19
+ [ -f "${HOOKS_DIR}/lib/sights-check.sh" ] && . "${HOOKS_DIR}/lib/sights-check.sh"
17
20
 
18
21
  # Read tool input from stdin (Claude Code pipes JSON)
19
22
  input=""
@@ -21,19 +24,83 @@ if [ ! -t 0 ]; then
21
24
  input=$(cat 2>/dev/null || true)
22
25
  fi
23
26
 
24
- # Extract file path from tool input
27
+ # Extract file path and content from tool input
25
28
  file_path=""
29
+ content_to_write=""
26
30
  if [ -n "$input" ] && command -v jq >/dev/null 2>&1; then
27
31
  file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)
32
+ # Write tool has 'content', Edit tool has 'new_string'
33
+ content_to_write=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // empty' 2>/dev/null || true)
28
34
  fi
29
35
 
30
- # Check if Sights was consulted recently (within 120 seconds)
31
- if ! sights_was_checked_recently 120; then
32
- log_event "sights_skip_warning" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
36
+ # ─────────────────────────────────────────────────────────────────────────────
37
+ # SECRET DETECTION
38
+ # Scans the content being written for common credential patterns.
39
+ # Uses grep with POSIX ERE for speed (<5ms on typical file sizes).
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ if [ -n "$content_to_write" ]; then
42
+ SECRET_FOUND=""
43
+ SECRET_TYPE=""
44
+
45
+ # AWS access key: AKIA followed by 16 uppercase alphanumeric chars
46
+ if echo "$content_to_write" | grep -qE 'AKIA[0-9A-Z]{16}' 2>/dev/null; then
47
+ SECRET_FOUND=1
48
+ SECRET_TYPE="AWS access key (AKIA...)"
49
+ fi
50
+
51
+ # OpenAI / Anthropic style API key: sk- followed by 48+ alphanumeric chars
52
+ if [ -z "$SECRET_FOUND" ] && echo "$content_to_write" | grep -qE 'sk-[a-zA-Z0-9]{48,}' 2>/dev/null; then
53
+ SECRET_FOUND=1
54
+ SECRET_TYPE="API secret key (sk-...)"
55
+ fi
56
+
57
+ # PEM private key header
58
+ if [ -z "$SECRET_FOUND" ] && echo "$content_to_write" | \
59
+ grep -qE '-----BEGIN (RSA|EC|DSA|OPENSSH|PRIVATE) KEY-----' 2>/dev/null; then
60
+ SECRET_FOUND=1
61
+ SECRET_TYPE="PEM private key"
62
+ fi
63
+
64
+ # Generic password assignment in config-like contexts
65
+ # Matches: password=, PASSWORD=, "password": "...", passwd=
66
+ # Requires the value to be non-empty and at least 8 chars to avoid false positives
67
+ if [ -z "$SECRET_FOUND" ] && echo "$content_to_write" | \
68
+ grep -qiE '(password|passwd|secret|api_key|apikey)\s*[=:]\s*["\x27]?[A-Za-z0-9@#$%^&*!_\-]{8,}' 2>/dev/null; then
69
+ # Exclude known placeholder patterns: <password>, ${PASSWORD}, %(password)s, REPLACE_ME, etc.
70
+ if ! echo "$content_to_write" | \
71
+ grep -qiE '(password|passwd|secret|api_key|apikey)\s*[=:]\s*["\x27]?(<[^>]+>|\$\{[^}]+\}|%\([^)]+\)s|REPLACE_ME|YOUR_|TODO|CHANGEME|example|placeholder|xxx+|test)' 2>/dev/null; then
72
+ SECRET_FOUND=1
73
+ SECRET_TYPE="credential assignment (password/secret/api_key)"
74
+ fi
75
+ fi
76
+
77
+ if [ -n "$SECRET_FOUND" ]; then
78
+ if declare -f log_event >/dev/null 2>&1; then
79
+ log_event "secret_detected" "$(printf '{"file":"%s","type":"%s"}' \
80
+ "${file_path:-unknown}" "$SECRET_TYPE")"
81
+ fi
82
+ echo "SECRET DETECTED: ${SECRET_TYPE} found in write to '${file_path:-unknown}'. Remove the secret before committing." >&2
83
+ # Output block decision — Claude Code reads this JSON when exit code is 2
84
+ echo '{"decision":"block","reason":"Secret detected in file write. Remove the secret before committing."}'
85
+ exit 2
86
+ fi
87
+ fi
88
+
89
+ # ─────────────────────────────────────────────────────────────────────────────
90
+ # SIGHTS CONSULTATION CHECK (advisory only)
91
+ # ─────────────────────────────────────────────────────────────────────────────
92
+ if declare -f sights_was_checked_recently >/dev/null 2>&1; then
93
+ if ! sights_was_checked_recently 120; then
94
+ if declare -f log_event >/dev/null 2>&1; then
95
+ log_event "sights_skip_warning" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
96
+ fi
97
+ fi
33
98
  fi
34
99
 
35
100
  # Log the pre-edit event
36
- log_event "pre_edit" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
101
+ if declare -f log_event >/dev/null 2>&1; then
102
+ log_event "pre_edit" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
103
+ fi
37
104
 
38
105
  # Claude Code command hooks must output valid JSON to stdout
39
106
  echo '{}'