scc-universal 1.1.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.
- package/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP health-check hook.
|
|
6
|
+
*
|
|
7
|
+
* Compatible with Claude Code's existing hook events:
|
|
8
|
+
* - PreToolUse: probe MCP server health before MCP tool execution
|
|
9
|
+
* - PostToolUseFailure: mark unhealthy servers, attempt reconnect, and re-probe
|
|
10
|
+
*
|
|
11
|
+
* The hook persists health state outside the conversation context so it
|
|
12
|
+
* survives compaction and later turns.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
const { spawn, spawnSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const MAX_STDIN = 1024 * 1024;
|
|
23
|
+
const DEFAULT_TTL_MS = 2 * 60 * 1000;
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
25
|
+
const DEFAULT_BACKOFF_MS = 30 * 1000;
|
|
26
|
+
const MAX_BACKOFF_MS = 10 * 60 * 1000;
|
|
27
|
+
const HEALTHY_HTTP_CODES = new Set([200, 201, 202, 204, 301, 302, 303, 304, 307, 308, 405]);
|
|
28
|
+
const RECONNECT_STATUS_CODES = new Set([401, 403, 429, 503]);
|
|
29
|
+
const FAILURE_PATTERNS = [
|
|
30
|
+
{ code: 401, pattern: /\b401\b|unauthori[sz]ed|auth(?:entication)?\s+(?:failed|expired|invalid)/i },
|
|
31
|
+
{ code: 403, pattern: /\b403\b|forbidden|permission denied/i },
|
|
32
|
+
{ code: 429, pattern: /\b429\b|rate limit|too many requests/i },
|
|
33
|
+
{ code: 503, pattern: /\b503\b|service unavailable|overloaded|temporarily unavailable/i },
|
|
34
|
+
{ code: 'transport', pattern: /ECONNREFUSED|ENOTFOUND|EAI_AGAIN|timed? out|socket hang up|connection (?:failed|lost|reset|closed)/i }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function envNumber(name, fallback) {
|
|
38
|
+
const value = Number(process.env[name]);
|
|
39
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stateFilePath() {
|
|
43
|
+
if (process.env.SCC_MCP_HEALTH_STATE_PATH) {
|
|
44
|
+
return path.resolve(process.env.SCC_MCP_HEALTH_STATE_PATH);
|
|
45
|
+
}
|
|
46
|
+
return path.join(os.homedir(), '.claude', 'mcp-health-cache.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function configPaths() {
|
|
50
|
+
if (process.env.SCC_MCP_CONFIG_PATH) {
|
|
51
|
+
return process.env.SCC_MCP_CONFIG_PATH
|
|
52
|
+
.split(path.delimiter)
|
|
53
|
+
.map(entry => entry.trim())
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.map(entry => path.resolve(entry));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const home = os.homedir();
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
path.join(cwd, '.claude.json'),
|
|
63
|
+
path.join(cwd, '.claude', 'settings.json'),
|
|
64
|
+
path.join(home, '.claude.json'),
|
|
65
|
+
path.join(home, '.claude', 'settings.json')
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readJsonFile(filePath) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadState(filePath) {
|
|
78
|
+
const state = readJsonFile(filePath);
|
|
79
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
80
|
+
return { version: 1, servers: {} };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!state.servers || typeof state.servers !== 'object' || Array.isArray(state.servers)) {
|
|
84
|
+
state.servers = {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveState(filePath, state) {
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
93
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
94
|
+
} catch {
|
|
95
|
+
// Never block the hook on state persistence errors.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readRawStdin() {
|
|
100
|
+
return new Promise(resolve => {
|
|
101
|
+
let raw = '';
|
|
102
|
+
process.stdin.setEncoding('utf8');
|
|
103
|
+
process.stdin.on('data', chunk => {
|
|
104
|
+
if (raw.length < MAX_STDIN) {
|
|
105
|
+
const remaining = MAX_STDIN - raw.length;
|
|
106
|
+
raw += chunk.substring(0, remaining);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
process.stdin.on('end', () => resolve(raw));
|
|
110
|
+
process.stdin.on('error', () => resolve(raw));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function safeParse(raw) {
|
|
115
|
+
try {
|
|
116
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
117
|
+
} catch {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractMcpTarget(input) {
|
|
123
|
+
const toolName = String(input.tool_name || input.name || '');
|
|
124
|
+
const explicitServer = input.server
|
|
125
|
+
|| input.mcp_server
|
|
126
|
+
|| input.tool_input?.server
|
|
127
|
+
|| input.tool_input?.mcp_server
|
|
128
|
+
|| input.tool_input?.connector
|
|
129
|
+
|| null;
|
|
130
|
+
const explicitTool = input.tool
|
|
131
|
+
|| input.mcp_tool
|
|
132
|
+
|| input.tool_input?.tool
|
|
133
|
+
|| input.tool_input?.mcp_tool
|
|
134
|
+
|| null;
|
|
135
|
+
|
|
136
|
+
if (explicitServer) {
|
|
137
|
+
return {
|
|
138
|
+
server: String(explicitServer),
|
|
139
|
+
tool: explicitTool ? String(explicitTool) : toolName
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!toolName.startsWith('mcp__')) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const segments = toolName.slice(5).split('__');
|
|
148
|
+
if (segments.length < 2 || !segments[0]) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
server: segments[0],
|
|
154
|
+
tool: segments.slice(1).join('__')
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveServerConfig(serverName) {
|
|
159
|
+
for (const filePath of configPaths()) {
|
|
160
|
+
const data = readJsonFile(filePath);
|
|
161
|
+
const server = data?.mcpServers?.[serverName]
|
|
162
|
+
|| data?.mcp_servers?.[serverName]
|
|
163
|
+
|| null;
|
|
164
|
+
|
|
165
|
+
if (server && typeof server === 'object' && !Array.isArray(server)) {
|
|
166
|
+
return {
|
|
167
|
+
config: server,
|
|
168
|
+
source: filePath
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function markHealthy(state, serverName, now, details = {}) {
|
|
177
|
+
state.servers[serverName] = {
|
|
178
|
+
status: 'healthy',
|
|
179
|
+
checkedAt: now,
|
|
180
|
+
expiresAt: now + envNumber('SCC_MCP_HEALTH_TTL_MS', DEFAULT_TTL_MS),
|
|
181
|
+
failureCount: 0,
|
|
182
|
+
lastError: null,
|
|
183
|
+
lastFailureCode: null,
|
|
184
|
+
nextRetryAt: now,
|
|
185
|
+
lastRestoredAt: now,
|
|
186
|
+
...details
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function markUnhealthy(state, serverName, now, failureCode, errorMessage) {
|
|
191
|
+
const previous = state.servers[serverName] || {};
|
|
192
|
+
const failureCount = Number(previous.failureCount || 0) + 1;
|
|
193
|
+
const backoffBase = envNumber('SCC_MCP_HEALTH_BACKOFF_MS', DEFAULT_BACKOFF_MS);
|
|
194
|
+
const nextRetryDelay = Math.min(backoffBase * (2 ** Math.max(failureCount - 1, 0)), MAX_BACKOFF_MS);
|
|
195
|
+
|
|
196
|
+
state.servers[serverName] = {
|
|
197
|
+
status: 'unhealthy',
|
|
198
|
+
checkedAt: now,
|
|
199
|
+
expiresAt: now,
|
|
200
|
+
failureCount,
|
|
201
|
+
lastError: errorMessage || null,
|
|
202
|
+
lastFailureCode: failureCode || null,
|
|
203
|
+
nextRetryAt: now + nextRetryDelay,
|
|
204
|
+
lastRestoredAt: previous.lastRestoredAt || null
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function failureSummary(input) {
|
|
209
|
+
const output = input.tool_output;
|
|
210
|
+
const pieces = [
|
|
211
|
+
typeof input.error === 'string' ? input.error : '',
|
|
212
|
+
typeof input.message === 'string' ? input.message : '',
|
|
213
|
+
typeof input.tool_response === 'string' ? input.tool_response : '',
|
|
214
|
+
typeof output === 'string' ? output : '',
|
|
215
|
+
typeof output?.output === 'string' ? output.output : '',
|
|
216
|
+
typeof output?.stderr === 'string' ? output.stderr : '',
|
|
217
|
+
typeof input.tool_input?.error === 'string' ? input.tool_input.error : ''
|
|
218
|
+
].filter(Boolean);
|
|
219
|
+
|
|
220
|
+
return pieces.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function detectFailureCode(text) {
|
|
224
|
+
const summary = String(text || '');
|
|
225
|
+
for (const entry of FAILURE_PATTERNS) {
|
|
226
|
+
if (entry.pattern.test(summary)) {
|
|
227
|
+
return entry.code;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function requestHttp(urlString, headers, timeoutMs) {
|
|
234
|
+
return new Promise(resolve => {
|
|
235
|
+
let settled = false;
|
|
236
|
+
let timedOut = false;
|
|
237
|
+
|
|
238
|
+
const url = new URL(urlString);
|
|
239
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
240
|
+
|
|
241
|
+
const req = client.request(
|
|
242
|
+
url,
|
|
243
|
+
{
|
|
244
|
+
method: 'GET',
|
|
245
|
+
headers,
|
|
246
|
+
},
|
|
247
|
+
res => {
|
|
248
|
+
if (settled) return;
|
|
249
|
+
settled = true;
|
|
250
|
+
res.resume();
|
|
251
|
+
resolve({
|
|
252
|
+
ok: HEALTHY_HTTP_CODES.has(res.statusCode),
|
|
253
|
+
statusCode: res.statusCode,
|
|
254
|
+
reason: `HTTP ${res.statusCode}`
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
req.setTimeout(timeoutMs, () => {
|
|
260
|
+
timedOut = true;
|
|
261
|
+
req.destroy(new Error('timeout'));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
req.on('error', error => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
settled = true;
|
|
267
|
+
resolve({
|
|
268
|
+
ok: false,
|
|
269
|
+
statusCode: null,
|
|
270
|
+
reason: timedOut ? 'request timed out' : error.message
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
req.end();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function probeCommandServer(serverName, config) {
|
|
279
|
+
return new Promise(resolve => {
|
|
280
|
+
const command = config.command;
|
|
281
|
+
const args = Array.isArray(config.args) ? config.args.map(arg => String(arg)) : [];
|
|
282
|
+
const timeoutMs = envNumber('SCC_MCP_HEALTH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS);
|
|
283
|
+
const mergedEnv = {
|
|
284
|
+
...process.env,
|
|
285
|
+
...(config.env && typeof config.env === 'object' && !Array.isArray(config.env) ? config.env : {})
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
let stderr = '';
|
|
289
|
+
let done = false;
|
|
290
|
+
|
|
291
|
+
function finish(result) {
|
|
292
|
+
if (done) return;
|
|
293
|
+
done = true;
|
|
294
|
+
resolve(result);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let child;
|
|
298
|
+
try {
|
|
299
|
+
child = spawn(command, args, {
|
|
300
|
+
env: mergedEnv,
|
|
301
|
+
cwd: process.cwd(),
|
|
302
|
+
stdio: ['pipe', 'ignore', 'pipe']
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
finish({
|
|
306
|
+
ok: false,
|
|
307
|
+
statusCode: null,
|
|
308
|
+
reason: error.message
|
|
309
|
+
});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
child.stderr.on('data', chunk => {
|
|
314
|
+
if (stderr.length < 4000) {
|
|
315
|
+
const remaining = 4000 - stderr.length;
|
|
316
|
+
stderr += String(chunk).slice(0, remaining);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
child.on('error', error => {
|
|
321
|
+
finish({
|
|
322
|
+
ok: false,
|
|
323
|
+
statusCode: null,
|
|
324
|
+
reason: error.message
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
child.on('exit', (code, signal) => {
|
|
329
|
+
finish({
|
|
330
|
+
ok: false,
|
|
331
|
+
statusCode: code,
|
|
332
|
+
reason: stderr.trim() || `process exited before handshake (${signal || code || 'unknown'})`
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
try {
|
|
338
|
+
child.kill('SIGTERM');
|
|
339
|
+
} catch {
|
|
340
|
+
// ignore
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
try {
|
|
345
|
+
child.kill('SIGKILL');
|
|
346
|
+
} catch {
|
|
347
|
+
// ignore
|
|
348
|
+
}
|
|
349
|
+
}, 200).unref?.();
|
|
350
|
+
|
|
351
|
+
finish({
|
|
352
|
+
ok: true,
|
|
353
|
+
statusCode: null,
|
|
354
|
+
reason: `${serverName} accepted a new stdio process`
|
|
355
|
+
});
|
|
356
|
+
}, timeoutMs);
|
|
357
|
+
|
|
358
|
+
if (typeof timer.unref === 'function') {
|
|
359
|
+
timer.unref();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function probeServer(serverName, resolvedConfig) {
|
|
365
|
+
const config = resolvedConfig.config;
|
|
366
|
+
|
|
367
|
+
if (config.type === 'http' || config.url) {
|
|
368
|
+
const result = await requestHttp(config.url, config.headers || {}, envNumber('SCC_MCP_HEALTH_TIMEOUT_MS', DEFAULT_TIMEOUT_MS));
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
ok: result.ok,
|
|
372
|
+
failureCode: RECONNECT_STATUS_CODES.has(result.statusCode) ? result.statusCode : null,
|
|
373
|
+
reason: result.reason,
|
|
374
|
+
source: resolvedConfig.source
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (config.command) {
|
|
379
|
+
const result = await probeCommandServer(serverName, config);
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
ok: result.ok,
|
|
383
|
+
failureCode: RECONNECT_STATUS_CODES.has(result.statusCode) ? result.statusCode : null,
|
|
384
|
+
reason: result.reason,
|
|
385
|
+
source: resolvedConfig.source
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
failureCode: null,
|
|
392
|
+
reason: 'unsupported MCP server config',
|
|
393
|
+
source: resolvedConfig.source
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function reconnectCommand(serverName) {
|
|
398
|
+
const key = `SCC_MCP_RECONNECT_${String(serverName).toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
|
|
399
|
+
const command = process.env[key] || process.env.SCC_MCP_RECONNECT_COMMAND || '';
|
|
400
|
+
if (!command.trim()) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return command.includes('{server}')
|
|
405
|
+
? command.replace(/\{server\}/g, serverName)
|
|
406
|
+
: command;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function attemptReconnect(serverName) {
|
|
410
|
+
const command = reconnectCommand(serverName);
|
|
411
|
+
if (!command) {
|
|
412
|
+
return { attempted: false, success: false, reason: 'no reconnect command configured' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = spawnSync(command, {
|
|
416
|
+
shell: true,
|
|
417
|
+
env: process.env,
|
|
418
|
+
cwd: process.cwd(),
|
|
419
|
+
encoding: 'utf8',
|
|
420
|
+
timeout: envNumber('SCC_MCP_RECONNECT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS)
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (result.error) {
|
|
424
|
+
return { attempted: true, success: false, reason: result.error.message };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (result.status !== 0) {
|
|
428
|
+
return {
|
|
429
|
+
attempted: true,
|
|
430
|
+
success: false,
|
|
431
|
+
reason: (result.stderr || result.stdout || `reconnect exited ${result.status}`).trim()
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { attempted: true, success: true, reason: 'reconnect command completed' };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function shouldFailOpen() {
|
|
439
|
+
return /^(1|true|yes)$/i.test(String(process.env.SCC_MCP_HEALTH_FAIL_OPEN || ''));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function emitLogs(logs) {
|
|
443
|
+
for (const line of logs) {
|
|
444
|
+
process.stderr.write(`${line}\n`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function handlePreToolUse(rawInput, input, target, statePathValue, now) {
|
|
449
|
+
const logs = [];
|
|
450
|
+
const state = loadState(statePathValue);
|
|
451
|
+
const previous = state.servers[target.server] || {};
|
|
452
|
+
|
|
453
|
+
if (previous.status === 'healthy' && Number(previous.expiresAt || 0) > now) {
|
|
454
|
+
return { rawInput, exitCode: 0, logs };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (previous.status === 'unhealthy' && Number(previous.nextRetryAt || 0) > now) {
|
|
458
|
+
logs.push(
|
|
459
|
+
`[MCPHealthCheck] ${target.server} is marked unhealthy until ${new Date(previous.nextRetryAt).toISOString()}; skipping ${target.tool || 'tool'}`
|
|
460
|
+
);
|
|
461
|
+
return { rawInput, exitCode: shouldFailOpen() ? 0 : 2, logs };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const resolvedConfig = resolveServerConfig(target.server);
|
|
465
|
+
if (!resolvedConfig) {
|
|
466
|
+
logs.push(`[MCPHealthCheck] No MCP config found for ${target.server}; skipping preflight probe`);
|
|
467
|
+
return { rawInput, exitCode: 0, logs };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const probe = await probeServer(target.server, resolvedConfig);
|
|
471
|
+
if (probe.ok) {
|
|
472
|
+
markHealthy(state, target.server, now, { source: resolvedConfig.source });
|
|
473
|
+
saveState(statePathValue, state);
|
|
474
|
+
|
|
475
|
+
if (previous.status === 'unhealthy') {
|
|
476
|
+
logs.push(`[MCPHealthCheck] ${target.server} connection restored`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return { rawInput, exitCode: 0, logs };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let reconnect = { attempted: false, success: false, reason: 'probe failed' };
|
|
483
|
+
if (probe.failureCode || previous.status === 'unhealthy') {
|
|
484
|
+
reconnect = attemptReconnect(target.server);
|
|
485
|
+
if (reconnect.success) {
|
|
486
|
+
const reprobe = await probeServer(target.server, resolvedConfig);
|
|
487
|
+
if (reprobe.ok) {
|
|
488
|
+
markHealthy(state, target.server, now, {
|
|
489
|
+
source: resolvedConfig.source,
|
|
490
|
+
restoredBy: 'reconnect-command'
|
|
491
|
+
});
|
|
492
|
+
saveState(statePathValue, state);
|
|
493
|
+
logs.push(`[MCPHealthCheck] ${target.server} connection restored after reconnect`);
|
|
494
|
+
return { rawInput, exitCode: 0, logs };
|
|
495
|
+
}
|
|
496
|
+
probe.reason = `${probe.reason}; reconnect reprobe failed: ${reprobe.reason}`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
markUnhealthy(state, target.server, now, probe.failureCode, probe.reason);
|
|
501
|
+
saveState(statePathValue, state);
|
|
502
|
+
|
|
503
|
+
const reconnectSuffix = reconnect.attempted
|
|
504
|
+
? ` Reconnect attempt: ${reconnect.success ? 'ok' : reconnect.reason}.`
|
|
505
|
+
: '';
|
|
506
|
+
logs.push(
|
|
507
|
+
`[MCPHealthCheck] ${target.server} is unavailable (${probe.reason}). Blocking ${target.tool || 'tool'} so Claude can fall back to non-MCP tools.${reconnectSuffix}`
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
return { rawInput, exitCode: shouldFailOpen() ? 0 : 2, logs };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function handlePostToolUseFailure(rawInput, input, target, statePathValue, now) {
|
|
514
|
+
const logs = [];
|
|
515
|
+
const summary = failureSummary(input);
|
|
516
|
+
const failureCode = detectFailureCode(summary);
|
|
517
|
+
|
|
518
|
+
if (!failureCode) {
|
|
519
|
+
return { rawInput, exitCode: 0, logs };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const state = loadState(statePathValue);
|
|
523
|
+
markUnhealthy(state, target.server, now, failureCode, summary.slice(0, 500));
|
|
524
|
+
saveState(statePathValue, state);
|
|
525
|
+
|
|
526
|
+
logs.push(`[MCPHealthCheck] ${target.server} reported ${failureCode}; marking server unhealthy and attempting reconnect`);
|
|
527
|
+
|
|
528
|
+
const reconnect = attemptReconnect(target.server);
|
|
529
|
+
if (!reconnect.attempted) {
|
|
530
|
+
logs.push(`[MCPHealthCheck] ${target.server} reconnect skipped: ${reconnect.reason}`);
|
|
531
|
+
return { rawInput, exitCode: 0, logs };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!reconnect.success) {
|
|
535
|
+
logs.push(`[MCPHealthCheck] ${target.server} reconnect failed: ${reconnect.reason}`);
|
|
536
|
+
return { rawInput, exitCode: 0, logs };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const resolvedConfig = resolveServerConfig(target.server);
|
|
540
|
+
if (!resolvedConfig) {
|
|
541
|
+
logs.push(`[MCPHealthCheck] ${target.server} reconnect completed but no config was available for a follow-up probe`);
|
|
542
|
+
return { rawInput, exitCode: 0, logs };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const reprobe = await probeServer(target.server, resolvedConfig);
|
|
546
|
+
if (!reprobe.ok) {
|
|
547
|
+
logs.push(`[MCPHealthCheck] ${target.server} reconnect command ran, but health probe still failed: ${reprobe.reason}`);
|
|
548
|
+
return { rawInput, exitCode: 0, logs };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const refreshed = loadState(statePathValue);
|
|
552
|
+
markHealthy(refreshed, target.server, now, {
|
|
553
|
+
source: resolvedConfig.source,
|
|
554
|
+
restoredBy: 'post-failure-reconnect'
|
|
555
|
+
});
|
|
556
|
+
saveState(statePathValue, refreshed);
|
|
557
|
+
logs.push(`[MCPHealthCheck] ${target.server} connection restored`);
|
|
558
|
+
return { rawInput, exitCode: 0, logs };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function main() {
|
|
562
|
+
const rawInput = await readRawStdin();
|
|
563
|
+
const input = safeParse(rawInput);
|
|
564
|
+
const target = extractMcpTarget(input);
|
|
565
|
+
|
|
566
|
+
if (!target) {
|
|
567
|
+
process.stdout.write(rawInput);
|
|
568
|
+
process.exit(0);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const eventName = process.env.CLAUDE_HOOK_EVENT_NAME || 'PreToolUse';
|
|
573
|
+
const now = Date.now();
|
|
574
|
+
const statePathValue = stateFilePath();
|
|
575
|
+
|
|
576
|
+
const result = eventName === 'PostToolUseFailure'
|
|
577
|
+
? await handlePostToolUseFailure(rawInput, input, target, statePathValue, now)
|
|
578
|
+
: await handlePreToolUse(rawInput, input, target, statePathValue, now);
|
|
579
|
+
|
|
580
|
+
emitLogs(result.logs);
|
|
581
|
+
process.stdout.write(result.rawInput);
|
|
582
|
+
process.exit(result.exitCode);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
main().catch(error => {
|
|
586
|
+
process.stderr.write(`[MCPHealthCheck] Unexpected error: ${error.message}\n`);
|
|
587
|
+
process.exit(0);
|
|
588
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PostToolUse Hook: Detect build/sf-deploy completion
|
|
6
|
+
*
|
|
7
|
+
* Runs after Bash tool use. Detects SF CLI deploy/build commands
|
|
8
|
+
* and logs completion notice for awareness.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_STDIN = 1024 * 1024;
|
|
12
|
+
let raw = '';
|
|
13
|
+
|
|
14
|
+
process.stdin.setEncoding('utf8');
|
|
15
|
+
process.stdin.on('data', chunk => {
|
|
16
|
+
if (raw.length < MAX_STDIN) {
|
|
17
|
+
const remaining = MAX_STDIN - raw.length;
|
|
18
|
+
raw += chunk.substring(0, remaining);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
try {
|
|
24
|
+
const input = JSON.parse(raw);
|
|
25
|
+
const cmd = String(input.tool_input?.command || '');
|
|
26
|
+
if (/(sf\s+project\s+deploy|sf\s+deploy|sfdx\s+force:source:deploy|npm run build|pnpm build|yarn build)/.test(cmd)) {
|
|
27
|
+
console.error('[Hook] Build/deploy completed - review results above');
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore parse errors and pass through
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.stdout.write(raw);
|
|
34
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PostToolUse Hook: Detect PR creation and log review command
|
|
6
|
+
*
|
|
7
|
+
* Runs after Bash tool use. Detects `gh pr create` and extracts
|
|
8
|
+
* the PR URL to provide a quick review command.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MAX_STDIN = 1024 * 1024;
|
|
12
|
+
let raw = '';
|
|
13
|
+
|
|
14
|
+
process.stdin.setEncoding('utf8');
|
|
15
|
+
process.stdin.on('data', chunk => {
|
|
16
|
+
if (raw.length < MAX_STDIN) {
|
|
17
|
+
const remaining = MAX_STDIN - raw.length;
|
|
18
|
+
raw += chunk.substring(0, remaining);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
try {
|
|
24
|
+
const input = JSON.parse(raw);
|
|
25
|
+
const cmd = String(input.tool_input?.command || '');
|
|
26
|
+
|
|
27
|
+
if (/\bgh\s+pr\s+create\b/.test(cmd)) {
|
|
28
|
+
const out = String(input.tool_output?.output || '');
|
|
29
|
+
const match = out.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/);
|
|
30
|
+
if (match) {
|
|
31
|
+
const prUrl = match[0];
|
|
32
|
+
const repo = prUrl.replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1');
|
|
33
|
+
const prNum = prUrl.replace(/.+\/pull\/(\d+)/, '$1');
|
|
34
|
+
console.error(`[Hook] PR created: ${prUrl}`);
|
|
35
|
+
console.error(`[Hook] To review: gh pr review ${prNum} --repo ${repo}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore parse errors and pass through
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdout.write(raw);
|
|
43
|
+
});
|