aidevops 2.52.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/.agent/AGENTS.md +614 -0
- package/.agent/accounts.md +65 -0
- package/.agent/aidevops/add-new-mcp-to-aidevops.md +456 -0
- package/.agent/aidevops/api-integrations.md +335 -0
- package/.agent/aidevops/architecture.md +510 -0
- package/.agent/aidevops/configs.md +274 -0
- package/.agent/aidevops/docs.md +244 -0
- package/.agent/aidevops/extension.md +311 -0
- package/.agent/aidevops/mcp-integrations.md +340 -0
- package/.agent/aidevops/mcp-troubleshooting.md +162 -0
- package/.agent/aidevops/memory-patterns.md +172 -0
- package/.agent/aidevops/providers.md +217 -0
- package/.agent/aidevops/recommendations.md +321 -0
- package/.agent/aidevops/requirements.md +301 -0
- package/.agent/aidevops/resources.md +214 -0
- package/.agent/aidevops/security-requirements.md +174 -0
- package/.agent/aidevops/security.md +350 -0
- package/.agent/aidevops/service-links.md +400 -0
- package/.agent/aidevops/services.md +357 -0
- package/.agent/aidevops/setup.md +153 -0
- package/.agent/aidevops/troubleshooting.md +389 -0
- package/.agent/aidevops.md +124 -0
- package/.agent/build-plus.md +244 -0
- package/.agent/content/guidelines.md +109 -0
- package/.agent/content.md +87 -0
- package/.agent/health.md +59 -0
- package/.agent/legal.md +59 -0
- package/.agent/loop-state/full-loop.local.md +16 -0
- package/.agent/loop-state/ralph-loop.local.md +10 -0
- package/.agent/marketing.md +440 -0
- package/.agent/memory/README.md +260 -0
- package/.agent/onboarding.md +796 -0
- package/.agent/plan-plus.md +245 -0
- package/.agent/research.md +100 -0
- package/.agent/sales.md +333 -0
- package/.agent/scripts/101domains-helper.sh +701 -0
- package/.agent/scripts/add-missing-returns.sh +140 -0
- package/.agent/scripts/agent-browser-helper.sh +311 -0
- package/.agent/scripts/agno-setup.sh +712 -0
- package/.agent/scripts/ahrefs-mcp-wrapper.js +168 -0
- package/.agent/scripts/aidevops-update-check.sh +71 -0
- package/.agent/scripts/ampcode-cli.sh +522 -0
- package/.agent/scripts/auto-version-bump.sh +156 -0
- package/.agent/scripts/autogen-helper.sh +512 -0
- package/.agent/scripts/beads-sync-helper.sh +596 -0
- package/.agent/scripts/closte-helper.sh +5 -0
- package/.agent/scripts/cloudron-helper.sh +321 -0
- package/.agent/scripts/codacy-cli-chunked.sh +581 -0
- package/.agent/scripts/codacy-cli.sh +442 -0
- package/.agent/scripts/code-audit-helper.sh +5 -0
- package/.agent/scripts/coderabbit-cli.sh +417 -0
- package/.agent/scripts/coderabbit-pro-analysis.sh +238 -0
- package/.agent/scripts/commands/code-simplifier.md +86 -0
- package/.agent/scripts/commands/full-loop.md +246 -0
- package/.agent/scripts/commands/postflight-loop.md +103 -0
- package/.agent/scripts/commands/recall.md +182 -0
- package/.agent/scripts/commands/remember.md +132 -0
- package/.agent/scripts/commands/save-todo.md +175 -0
- package/.agent/scripts/commands/session-review.md +154 -0
- package/.agent/scripts/comprehensive-quality-fix.sh +106 -0
- package/.agent/scripts/context-builder-helper.sh +522 -0
- package/.agent/scripts/coolify-cli-helper.sh +674 -0
- package/.agent/scripts/coolify-helper.sh +380 -0
- package/.agent/scripts/crawl4ai-examples.sh +401 -0
- package/.agent/scripts/crawl4ai-helper.sh +1078 -0
- package/.agent/scripts/crewai-helper.sh +681 -0
- package/.agent/scripts/dev-browser-helper.sh +513 -0
- package/.agent/scripts/dns-helper.sh +396 -0
- package/.agent/scripts/domain-research-helper.sh +917 -0
- package/.agent/scripts/dspy-helper.sh +285 -0
- package/.agent/scripts/dspyground-helper.sh +291 -0
- package/.agent/scripts/eeat-score-helper.sh +1242 -0
- package/.agent/scripts/efficient-return-fix.sh +92 -0
- package/.agent/scripts/extract-opencode-prompts.sh +128 -0
- package/.agent/scripts/find-missing-returns.sh +113 -0
- package/.agent/scripts/fix-auth-headers.sh +104 -0
- package/.agent/scripts/fix-common-strings.sh +254 -0
- package/.agent/scripts/fix-content-type.sh +100 -0
- package/.agent/scripts/fix-error-messages.sh +130 -0
- package/.agent/scripts/fix-misplaced-returns.sh +74 -0
- package/.agent/scripts/fix-remaining-literals.sh +152 -0
- package/.agent/scripts/fix-return-statements.sh +41 -0
- package/.agent/scripts/fix-s131-default-cases.sh +249 -0
- package/.agent/scripts/fix-sc2155-simple.sh +102 -0
- package/.agent/scripts/fix-shellcheck-critical.sh +187 -0
- package/.agent/scripts/fix-string-literals.sh +273 -0
- package/.agent/scripts/full-loop-helper.sh +773 -0
- package/.agent/scripts/generate-opencode-agents.sh +497 -0
- package/.agent/scripts/generate-opencode-commands.sh +1629 -0
- package/.agent/scripts/generate-skills.sh +366 -0
- package/.agent/scripts/git-platforms-helper.sh +640 -0
- package/.agent/scripts/gitea-cli-helper.sh +743 -0
- package/.agent/scripts/github-cli-helper.sh +702 -0
- package/.agent/scripts/gitlab-cli-helper.sh +682 -0
- package/.agent/scripts/gsc-add-user-helper.sh +325 -0
- package/.agent/scripts/gsc-sitemap-helper.sh +678 -0
- package/.agent/scripts/hetzner-helper.sh +485 -0
- package/.agent/scripts/hostinger-helper.sh +229 -0
- package/.agent/scripts/keyword-research-helper.sh +1815 -0
- package/.agent/scripts/langflow-helper.sh +544 -0
- package/.agent/scripts/linkedin-automation.py +241 -0
- package/.agent/scripts/linter-manager.sh +599 -0
- package/.agent/scripts/linters-local.sh +434 -0
- package/.agent/scripts/list-keys-helper.sh +488 -0
- package/.agent/scripts/local-browser-automation.py +339 -0
- package/.agent/scripts/localhost-helper.sh +744 -0
- package/.agent/scripts/loop-common.sh +806 -0
- package/.agent/scripts/mainwp-helper.sh +728 -0
- package/.agent/scripts/markdown-formatter.sh +338 -0
- package/.agent/scripts/markdown-lint-fix.sh +311 -0
- package/.agent/scripts/mass-fix-returns.sh +58 -0
- package/.agent/scripts/mcp-diagnose.sh +167 -0
- package/.agent/scripts/mcp-inspector-helper.sh +449 -0
- package/.agent/scripts/memory-helper.sh +650 -0
- package/.agent/scripts/monitor-code-review.sh +255 -0
- package/.agent/scripts/onboarding-helper.sh +706 -0
- package/.agent/scripts/opencode-github-setup-helper.sh +797 -0
- package/.agent/scripts/opencode-test-helper.sh +213 -0
- package/.agent/scripts/pagespeed-helper.sh +464 -0
- package/.agent/scripts/pandoc-helper.sh +362 -0
- package/.agent/scripts/postflight-check.sh +555 -0
- package/.agent/scripts/pre-commit-hook.sh +259 -0
- package/.agent/scripts/pre-edit-check.sh +169 -0
- package/.agent/scripts/qlty-cli.sh +356 -0
- package/.agent/scripts/quality-cli-manager.sh +525 -0
- package/.agent/scripts/quality-feedback-helper.sh +462 -0
- package/.agent/scripts/quality-fix.sh +263 -0
- package/.agent/scripts/quality-loop-helper.sh +1108 -0
- package/.agent/scripts/ralph-loop-helper.sh +836 -0
- package/.agent/scripts/ralph-upstream-check.sh +341 -0
- package/.agent/scripts/secretlint-helper.sh +847 -0
- package/.agent/scripts/servers-helper.sh +241 -0
- package/.agent/scripts/ses-helper.sh +619 -0
- package/.agent/scripts/session-review-helper.sh +404 -0
- package/.agent/scripts/setup-linters-wizard.sh +379 -0
- package/.agent/scripts/setup-local-api-keys.sh +330 -0
- package/.agent/scripts/setup-mcp-integrations.sh +472 -0
- package/.agent/scripts/shared-constants.sh +246 -0
- package/.agent/scripts/site-crawler-helper.sh +1487 -0
- package/.agent/scripts/snyk-helper.sh +940 -0
- package/.agent/scripts/sonarcloud-autofix.sh +193 -0
- package/.agent/scripts/sonarcloud-cli.sh +191 -0
- package/.agent/scripts/sonarscanner-cli.sh +455 -0
- package/.agent/scripts/spaceship-helper.sh +747 -0
- package/.agent/scripts/stagehand-helper.sh +321 -0
- package/.agent/scripts/stagehand-python-helper.sh +321 -0
- package/.agent/scripts/stagehand-python-setup.sh +441 -0
- package/.agent/scripts/stagehand-setup.sh +439 -0
- package/.agent/scripts/system-cleanup.sh +340 -0
- package/.agent/scripts/terminal-title-helper.sh +388 -0
- package/.agent/scripts/terminal-title-setup.sh +549 -0
- package/.agent/scripts/test-stagehand-both-integration.sh +317 -0
- package/.agent/scripts/test-stagehand-integration.sh +309 -0
- package/.agent/scripts/test-stagehand-python-integration.sh +341 -0
- package/.agent/scripts/todo-ready.sh +263 -0
- package/.agent/scripts/tool-version-check.sh +362 -0
- package/.agent/scripts/toon-helper.sh +469 -0
- package/.agent/scripts/twilio-helper.sh +917 -0
- package/.agent/scripts/updown-helper.sh +279 -0
- package/.agent/scripts/validate-mcp-integrations.sh +250 -0
- package/.agent/scripts/validate-version-consistency.sh +131 -0
- package/.agent/scripts/vaultwarden-helper.sh +597 -0
- package/.agent/scripts/vercel-cli-helper.sh +816 -0
- package/.agent/scripts/verify-mirrors.sh +169 -0
- package/.agent/scripts/version-manager.sh +831 -0
- package/.agent/scripts/webhosting-helper.sh +471 -0
- package/.agent/scripts/webhosting-verify.sh +238 -0
- package/.agent/scripts/wordpress-mcp-helper.sh +508 -0
- package/.agent/scripts/worktree-helper.sh +595 -0
- package/.agent/scripts/worktree-sessions.sh +577 -0
- package/.agent/seo/dataforseo.md +215 -0
- package/.agent/seo/domain-research.md +532 -0
- package/.agent/seo/eeat-score.md +659 -0
- package/.agent/seo/google-search-console.md +366 -0
- package/.agent/seo/gsc-sitemaps.md +282 -0
- package/.agent/seo/keyword-research.md +521 -0
- package/.agent/seo/serper.md +278 -0
- package/.agent/seo/site-crawler.md +387 -0
- package/.agent/seo.md +236 -0
- package/.agent/services/accounting/quickfile.md +159 -0
- package/.agent/services/communications/telfon.md +470 -0
- package/.agent/services/communications/twilio.md +569 -0
- package/.agent/services/crm/fluentcrm.md +449 -0
- package/.agent/services/email/ses.md +399 -0
- package/.agent/services/hosting/101domains.md +378 -0
- package/.agent/services/hosting/closte.md +177 -0
- package/.agent/services/hosting/cloudflare.md +251 -0
- package/.agent/services/hosting/cloudron.md +478 -0
- package/.agent/services/hosting/dns-providers.md +335 -0
- package/.agent/services/hosting/domain-purchasing.md +344 -0
- package/.agent/services/hosting/hetzner.md +327 -0
- package/.agent/services/hosting/hostinger.md +287 -0
- package/.agent/services/hosting/localhost.md +419 -0
- package/.agent/services/hosting/spaceship.md +353 -0
- package/.agent/services/hosting/webhosting.md +330 -0
- package/.agent/social-media.md +69 -0
- package/.agent/templates/plans-template.md +114 -0
- package/.agent/templates/prd-template.md +129 -0
- package/.agent/templates/tasks-template.md +108 -0
- package/.agent/templates/todo-template.md +89 -0
- package/.agent/tools/ai-assistants/agno.md +471 -0
- package/.agent/tools/ai-assistants/capsolver.md +326 -0
- package/.agent/tools/ai-assistants/configuration.md +221 -0
- package/.agent/tools/ai-assistants/overview.md +209 -0
- package/.agent/tools/ai-assistants/status.md +171 -0
- package/.agent/tools/ai-assistants/windsurf.md +193 -0
- package/.agent/tools/ai-orchestration/autogen.md +406 -0
- package/.agent/tools/ai-orchestration/crewai.md +445 -0
- package/.agent/tools/ai-orchestration/langflow.md +405 -0
- package/.agent/tools/ai-orchestration/openprose.md +487 -0
- package/.agent/tools/ai-orchestration/overview.md +362 -0
- package/.agent/tools/ai-orchestration/packaging.md +647 -0
- package/.agent/tools/browser/agent-browser.md +464 -0
- package/.agent/tools/browser/browser-automation.md +400 -0
- package/.agent/tools/browser/chrome-devtools.md +282 -0
- package/.agent/tools/browser/crawl4ai-integration.md +422 -0
- package/.agent/tools/browser/crawl4ai-resources.md +277 -0
- package/.agent/tools/browser/crawl4ai-usage.md +416 -0
- package/.agent/tools/browser/crawl4ai.md +585 -0
- package/.agent/tools/browser/dev-browser.md +341 -0
- package/.agent/tools/browser/pagespeed.md +260 -0
- package/.agent/tools/browser/playwright.md +266 -0
- package/.agent/tools/browser/playwriter.md +310 -0
- package/.agent/tools/browser/stagehand-examples.md +456 -0
- package/.agent/tools/browser/stagehand-python.md +483 -0
- package/.agent/tools/browser/stagehand.md +421 -0
- package/.agent/tools/build-agent/agent-review.md +224 -0
- package/.agent/tools/build-agent/build-agent.md +784 -0
- package/.agent/tools/build-mcp/aidevops-plugin.md +476 -0
- package/.agent/tools/build-mcp/api-wrapper.md +445 -0
- package/.agent/tools/build-mcp/build-mcp.md +240 -0
- package/.agent/tools/build-mcp/deployment.md +401 -0
- package/.agent/tools/build-mcp/server-patterns.md +632 -0
- package/.agent/tools/build-mcp/transports.md +366 -0
- package/.agent/tools/code-review/auditing.md +383 -0
- package/.agent/tools/code-review/automation.md +219 -0
- package/.agent/tools/code-review/best-practices.md +203 -0
- package/.agent/tools/code-review/codacy.md +151 -0
- package/.agent/tools/code-review/code-simplifier.md +174 -0
- package/.agent/tools/code-review/code-standards.md +309 -0
- package/.agent/tools/code-review/coderabbit.md +101 -0
- package/.agent/tools/code-review/management.md +155 -0
- package/.agent/tools/code-review/qlty.md +248 -0
- package/.agent/tools/code-review/secretlint.md +565 -0
- package/.agent/tools/code-review/setup.md +250 -0
- package/.agent/tools/code-review/snyk.md +563 -0
- package/.agent/tools/code-review/tools.md +230 -0
- package/.agent/tools/content/summarize.md +353 -0
- package/.agent/tools/context/augment-context-engine.md +468 -0
- package/.agent/tools/context/context-builder-agent.md +76 -0
- package/.agent/tools/context/context-builder.md +375 -0
- package/.agent/tools/context/context7.md +371 -0
- package/.agent/tools/context/dspy.md +302 -0
- package/.agent/tools/context/dspyground.md +374 -0
- package/.agent/tools/context/llm-tldr.md +219 -0
- package/.agent/tools/context/osgrep.md +488 -0
- package/.agent/tools/context/prompt-optimization.md +338 -0
- package/.agent/tools/context/toon.md +292 -0
- package/.agent/tools/conversion/pandoc.md +304 -0
- package/.agent/tools/credentials/api-key-management.md +154 -0
- package/.agent/tools/credentials/api-key-setup.md +224 -0
- package/.agent/tools/credentials/environment-variables.md +180 -0
- package/.agent/tools/credentials/vaultwarden.md +382 -0
- package/.agent/tools/data-extraction/outscraper.md +974 -0
- package/.agent/tools/deployment/coolify-cli.md +388 -0
- package/.agent/tools/deployment/coolify-setup.md +353 -0
- package/.agent/tools/deployment/coolify.md +345 -0
- package/.agent/tools/deployment/vercel.md +390 -0
- package/.agent/tools/git/authentication.md +132 -0
- package/.agent/tools/git/gitea-cli.md +193 -0
- package/.agent/tools/git/github-actions.md +207 -0
- package/.agent/tools/git/github-cli.md +223 -0
- package/.agent/tools/git/gitlab-cli.md +190 -0
- package/.agent/tools/git/opencode-github-security.md +350 -0
- package/.agent/tools/git/opencode-github.md +328 -0
- package/.agent/tools/git/opencode-gitlab.md +252 -0
- package/.agent/tools/git/security.md +196 -0
- package/.agent/tools/git.md +207 -0
- package/.agent/tools/opencode/oh-my-opencode.md +375 -0
- package/.agent/tools/opencode/opencode-anthropic-auth.md +446 -0
- package/.agent/tools/opencode/opencode.md +651 -0
- package/.agent/tools/social-media/bird.md +437 -0
- package/.agent/tools/task-management/beads.md +336 -0
- package/.agent/tools/terminal/terminal-title.md +251 -0
- package/.agent/tools/ui/shadcn.md +196 -0
- package/.agent/tools/ui/ui-skills.md +115 -0
- package/.agent/tools/wordpress/localwp.md +311 -0
- package/.agent/tools/wordpress/mainwp.md +391 -0
- package/.agent/tools/wordpress/scf.md +527 -0
- package/.agent/tools/wordpress/wp-admin.md +729 -0
- package/.agent/tools/wordpress/wp-dev.md +940 -0
- package/.agent/tools/wordpress/wp-preferred.md +398 -0
- package/.agent/tools/wordpress.md +95 -0
- package/.agent/workflows/branch/bugfix.md +63 -0
- package/.agent/workflows/branch/chore.md +95 -0
- package/.agent/workflows/branch/experiment.md +115 -0
- package/.agent/workflows/branch/feature.md +59 -0
- package/.agent/workflows/branch/hotfix.md +98 -0
- package/.agent/workflows/branch/refactor.md +92 -0
- package/.agent/workflows/branch/release.md +96 -0
- package/.agent/workflows/branch.md +347 -0
- package/.agent/workflows/bug-fixing.md +267 -0
- package/.agent/workflows/changelog.md +129 -0
- package/.agent/workflows/code-audit-remote.md +279 -0
- package/.agent/workflows/conversation-starter.md +69 -0
- package/.agent/workflows/error-feedback.md +578 -0
- package/.agent/workflows/feature-development.md +355 -0
- package/.agent/workflows/git-workflow.md +702 -0
- package/.agent/workflows/multi-repo-workspace.md +268 -0
- package/.agent/workflows/plans.md +709 -0
- package/.agent/workflows/postflight.md +604 -0
- package/.agent/workflows/pr.md +571 -0
- package/.agent/workflows/preflight.md +278 -0
- package/.agent/workflows/ralph-loop.md +773 -0
- package/.agent/workflows/release.md +498 -0
- package/.agent/workflows/session-manager.md +254 -0
- package/.agent/workflows/session-review.md +311 -0
- package/.agent/workflows/sql-migrations.md +631 -0
- package/.agent/workflows/version-bump.md +283 -0
- package/.agent/workflows/wiki-update.md +333 -0
- package/.agent/workflows/worktree.md +477 -0
- package/LICENSE +21 -0
- package/README.md +1446 -0
- package/VERSION +1 -0
- package/aidevops.sh +1746 -0
- package/bin/aidevops +21 -0
- package/package.json +75 -0
- package/scripts/npm-postinstall.js +60 -0
- package/setup.sh +2366 -0
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# shellcheck disable=SC2034,SC2155,SC2317,SC2329,SC2016,SC2181,SC1091,SC2154,SC2015,SC2086,SC2129,SC2030,SC2031,SC2119,SC2120,SC2001,SC2162,SC2088,SC2089,SC2090,SC2029,SC2006,SC2153
|
|
3
|
+
|
|
4
|
+
# E-E-A-T Score Helper Script
|
|
5
|
+
# Content quality scoring using Google's E-E-A-T framework
|
|
6
|
+
#
|
|
7
|
+
# Usage: ./eeat-score-helper.sh [command] [input] [options]
|
|
8
|
+
# Commands:
|
|
9
|
+
# analyze - Analyze crawled pages from site-crawler output
|
|
10
|
+
# score - Score a single URL
|
|
11
|
+
# batch - Batch analyze URLs from a file
|
|
12
|
+
# report - Generate spreadsheet from existing scores
|
|
13
|
+
# status - Check dependencies and configuration
|
|
14
|
+
# help - Show this help message
|
|
15
|
+
#
|
|
16
|
+
# Author: AI DevOps Framework
|
|
17
|
+
# Version: 1.0.0
|
|
18
|
+
# License: MIT
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
# Colors for output
|
|
23
|
+
readonly GREEN='\033[0;32m'
|
|
24
|
+
readonly BLUE='\033[0;34m'
|
|
25
|
+
readonly YELLOW='\033[1;33m'
|
|
26
|
+
readonly RED='\033[0;31m'
|
|
27
|
+
readonly PURPLE='\033[0;35m'
|
|
28
|
+
readonly NC='\033[0m'
|
|
29
|
+
|
|
30
|
+
# Constants
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit
|
|
32
|
+
readonly SCRIPT_DIR
|
|
33
|
+
readonly CONFIG_DIR="${HOME}/.config/aidevops"
|
|
34
|
+
readonly CONFIG_FILE="${CONFIG_DIR}/eeat-score.json"
|
|
35
|
+
readonly DEFAULT_OUTPUT_DIR="${HOME}/Downloads"
|
|
36
|
+
|
|
37
|
+
# Default configuration
|
|
38
|
+
LLM_PROVIDER="openai"
|
|
39
|
+
LLM_MODEL="gpt-4o"
|
|
40
|
+
TEMPERATURE="0.3"
|
|
41
|
+
MAX_TOKENS="500"
|
|
42
|
+
CONCURRENT_REQUESTS=3
|
|
43
|
+
OUTPUT_FORMAT="xlsx"
|
|
44
|
+
INCLUDE_REASONING=true
|
|
45
|
+
|
|
46
|
+
# Weights for overall score calculation
|
|
47
|
+
WEIGHT_AUTHORSHIP=0.15
|
|
48
|
+
WEIGHT_CITATION=0.15
|
|
49
|
+
WEIGHT_EFFORT=0.15
|
|
50
|
+
WEIGHT_ORIGINALITY=0.15
|
|
51
|
+
WEIGHT_INTENT=0.15
|
|
52
|
+
WEIGHT_SUBJECTIVE=0.15
|
|
53
|
+
WEIGHT_WRITING=0.10
|
|
54
|
+
|
|
55
|
+
# Print functions
|
|
56
|
+
print_success() {
|
|
57
|
+
local message="$1"
|
|
58
|
+
echo -e "${GREEN}[OK] $message${NC}"
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
print_info() {
|
|
63
|
+
local message="$1"
|
|
64
|
+
echo -e "${BLUE}[INFO] $message${NC}"
|
|
65
|
+
return 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
print_warning() {
|
|
69
|
+
local message="$1"
|
|
70
|
+
echo -e "${YELLOW}[WARN] $message${NC}"
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
print_error() {
|
|
75
|
+
local message="$1"
|
|
76
|
+
echo -e "${RED}[ERROR] $message${NC}"
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
print_header() {
|
|
81
|
+
local message="$1"
|
|
82
|
+
echo -e "${PURPLE}=== $message ===${NC}"
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Load configuration
|
|
87
|
+
load_config() {
|
|
88
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
89
|
+
if command -v jq &> /dev/null; then
|
|
90
|
+
LLM_PROVIDER=$(jq -r '.llm_provider // "openai"' "$CONFIG_FILE")
|
|
91
|
+
LLM_MODEL=$(jq -r '.llm_model // "gpt-4o"' "$CONFIG_FILE")
|
|
92
|
+
TEMPERATURE=$(jq -r '.temperature // 0.3' "$CONFIG_FILE")
|
|
93
|
+
MAX_TOKENS=$(jq -r '.max_tokens // 500' "$CONFIG_FILE")
|
|
94
|
+
CONCURRENT_REQUESTS=$(jq -r '.concurrent_requests // 3' "$CONFIG_FILE")
|
|
95
|
+
OUTPUT_FORMAT=$(jq -r '.output_format // "xlsx"' "$CONFIG_FILE")
|
|
96
|
+
INCLUDE_REASONING=$(jq -r '.include_reasoning // true' "$CONFIG_FILE")
|
|
97
|
+
|
|
98
|
+
# Load weights
|
|
99
|
+
WEIGHT_AUTHORSHIP=$(jq -r '.weights.authorship // 0.15' "$CONFIG_FILE")
|
|
100
|
+
WEIGHT_CITATION=$(jq -r '.weights.citation // 0.15' "$CONFIG_FILE")
|
|
101
|
+
WEIGHT_EFFORT=$(jq -r '.weights.effort // 0.15' "$CONFIG_FILE")
|
|
102
|
+
WEIGHT_ORIGINALITY=$(jq -r '.weights.originality // 0.15' "$CONFIG_FILE")
|
|
103
|
+
WEIGHT_INTENT=$(jq -r '.weights.intent // 0.15' "$CONFIG_FILE")
|
|
104
|
+
WEIGHT_SUBJECTIVE=$(jq -r '.weights.subjective // 0.15' "$CONFIG_FILE")
|
|
105
|
+
WEIGHT_WRITING=$(jq -r '.weights.writing // 0.10' "$CONFIG_FILE")
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
return 0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Check dependencies
|
|
112
|
+
check_dependencies() {
|
|
113
|
+
local missing=()
|
|
114
|
+
|
|
115
|
+
if ! command -v curl &> /dev/null; then
|
|
116
|
+
missing+=("curl")
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
if ! command -v jq &> /dev/null; then
|
|
120
|
+
missing+=("jq")
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if ! command -v python3 &> /dev/null; then
|
|
124
|
+
missing+=("python3")
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
128
|
+
print_error "Missing dependencies: ${missing[*]}"
|
|
129
|
+
print_info "Install with: brew install ${missing[*]}"
|
|
130
|
+
return 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
return 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Check API key
|
|
137
|
+
check_api_key() {
|
|
138
|
+
if [[ "$LLM_PROVIDER" == "openai" ]]; then
|
|
139
|
+
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
|
|
140
|
+
print_error "OPENAI_API_KEY environment variable not set"
|
|
141
|
+
print_info "Set with: export OPENAI_API_KEY='sk-...'"
|
|
142
|
+
return 1
|
|
143
|
+
fi
|
|
144
|
+
elif [[ "$LLM_PROVIDER" == "anthropic" ]]; then
|
|
145
|
+
if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
146
|
+
print_error "ANTHROPIC_API_KEY environment variable not set"
|
|
147
|
+
return 1
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
return 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Extract domain from URL
|
|
154
|
+
get_domain() {
|
|
155
|
+
local url="$1"
|
|
156
|
+
echo "$url" | sed -E 's|^https?://||' | sed -E 's|/.*||' | sed -E 's|:.*||'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Create output directory structure
|
|
160
|
+
create_output_dir() {
|
|
161
|
+
local domain="$1"
|
|
162
|
+
local output_base="${2:-$DEFAULT_OUTPUT_DIR}"
|
|
163
|
+
local timestamp
|
|
164
|
+
timestamp=$(date +%Y-%m-%d_%H%M%S)
|
|
165
|
+
|
|
166
|
+
local output_dir="${output_base}/${domain}/${timestamp}"
|
|
167
|
+
mkdir -p "$output_dir"
|
|
168
|
+
|
|
169
|
+
# Update _latest symlink
|
|
170
|
+
local latest_link="${output_base}/${domain}/_latest"
|
|
171
|
+
rm -f "$latest_link"
|
|
172
|
+
ln -sf "$timestamp" "$latest_link"
|
|
173
|
+
|
|
174
|
+
echo "$output_dir"
|
|
175
|
+
return 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Generate Python E-E-A-T analyzer script
|
|
179
|
+
generate_analyzer_script() {
|
|
180
|
+
cat << 'PYTHON_SCRIPT'
|
|
181
|
+
#!/usr/bin/env python3
|
|
182
|
+
"""
|
|
183
|
+
E-E-A-T Score Analyzer
|
|
184
|
+
Evaluates content quality using Google's E-E-A-T framework
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
import asyncio
|
|
188
|
+
import json
|
|
189
|
+
import csv
|
|
190
|
+
import sys
|
|
191
|
+
import os
|
|
192
|
+
from datetime import datetime
|
|
193
|
+
from pathlib import Path
|
|
194
|
+
from dataclasses import dataclass, field, asdict
|
|
195
|
+
from typing import Optional, List, Dict
|
|
196
|
+
import aiohttp
|
|
197
|
+
from bs4 import BeautifulSoup
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
import openpyxl
|
|
201
|
+
from openpyxl.styles import Font, PatternFill, Alignment
|
|
202
|
+
HAS_OPENPYXL = True
|
|
203
|
+
except ImportError:
|
|
204
|
+
HAS_OPENPYXL = False
|
|
205
|
+
|
|
206
|
+
# E-E-A-T Prompts
|
|
207
|
+
PROMPTS = {
|
|
208
|
+
"authorship_reasoning": """You are evaluating Authorship & Expertise for this page. Analyze and explain in 3-4 sentences:
|
|
209
|
+
- Is there a clear AUTHOR? If yes, who and what credentials?
|
|
210
|
+
- Can you identify the PUBLISHER (who owns/operates the site)?
|
|
211
|
+
- Is this a "Disconnected Entity" (anonymous, untraceable) or "Connected Entity" (verifiable)?
|
|
212
|
+
- Do they demonstrate RELEVANT EXPERTISE for this topic?
|
|
213
|
+
Be specific with names, credentials, evidence from the page.""",
|
|
214
|
+
|
|
215
|
+
"authorship_score": """You are evaluating Authorship & Expertise (isAuthor criterion).
|
|
216
|
+
CRITICAL: A "Disconnected Entity" is one where you CANNOT find "who owns and operates" the site.
|
|
217
|
+
Evaluate:
|
|
218
|
+
- Is there a clear author byline linking to a detailed biography?
|
|
219
|
+
- Does the About page clearly identify the company or person responsible?
|
|
220
|
+
- Is this entity VERIFIABLE and ACCOUNTABLE?
|
|
221
|
+
- Do they demonstrate RELEVANT EXPERTISE for this topic?
|
|
222
|
+
Score 1-10:
|
|
223
|
+
1-3 = DISCONNECTED ENTITY: No clear author, anonymous, untraceable
|
|
224
|
+
4-6 = Partial attribution, but weak verifiability or unclear credentials
|
|
225
|
+
7-10 = CONNECTED ENTITY: Clear author with detailed bio, verifiable expertise
|
|
226
|
+
Return ONLY the number.""",
|
|
227
|
+
|
|
228
|
+
"citation_reasoning": """You are evaluating Citation Quality for this page. Analyze and explain in 3-4 sentences:
|
|
229
|
+
- Does the page make SPECIFIC FACTUAL CLAIMS?
|
|
230
|
+
- Are those claims SUBSTANTIATED with citations?
|
|
231
|
+
- QUALITY assessment: Primary sources (studies, official docs) or secondary/low-quality?
|
|
232
|
+
- Or are claims unsupported?
|
|
233
|
+
Be specific with examples of claims and their (lack of) citations.""",
|
|
234
|
+
|
|
235
|
+
"citation_score": """You are evaluating Citation Quality & Substantiation.
|
|
236
|
+
Does this content BACK UP its claims with high-quality sources?
|
|
237
|
+
Analyze:
|
|
238
|
+
- Does the page make SPECIFIC FACTUAL CLAIMS?
|
|
239
|
+
- Are those claims SUBSTANTIATED with citations/links?
|
|
240
|
+
- QUALITY of sources: Primary sources (studies, legal docs, official data)?
|
|
241
|
+
Score 1-10:
|
|
242
|
+
1-3 = LOW: Bold claims with NO citations, or only low-quality links
|
|
243
|
+
4-6 = MODERATE: Some citations but mediocre quality
|
|
244
|
+
7-10 = HIGH: Core claims substantiated with primary sources
|
|
245
|
+
Return ONLY the number.""",
|
|
246
|
+
|
|
247
|
+
"effort_reasoning": """You are evaluating Content Effort for this page. Analyze and explain in 3-4 sentences:
|
|
248
|
+
- How DIFFICULT would it be to REPLICATE this content? (time, cost, expertise)
|
|
249
|
+
- Does the page "SHOW ITS WORK"? Is the creation process transparent?
|
|
250
|
+
- What evidence of high/low effort? (original research, data, multimedia, depth)
|
|
251
|
+
- Any unique elements that required significant resources?
|
|
252
|
+
Be specific with examples from the page.""",
|
|
253
|
+
|
|
254
|
+
"effort_score": """You are evaluating Content Effort.
|
|
255
|
+
Assess the DEMONSTRABLE effort, expertise, and resources invested.
|
|
256
|
+
Key questions:
|
|
257
|
+
1. REPLICABILITY: How difficult would it be for a competitor to create equal content?
|
|
258
|
+
2. CREATION PROCESS: Does the page "show its work"?
|
|
259
|
+
Look for: In-depth analysis, original data, unique multimedia, transparent methodology
|
|
260
|
+
Score 1-10:
|
|
261
|
+
1-3 = LOW EFFORT: Generic, formulaic, easily replicated in hours
|
|
262
|
+
7-8 = HIGH EFFORT: Significant investment, hard to replicate
|
|
263
|
+
9-10 = EXCEPTIONAL: Original research, proprietary data, unique tools
|
|
264
|
+
Return ONLY the number.""",
|
|
265
|
+
|
|
266
|
+
"originality_reasoning": """You are evaluating Content Originality for this page. Analyze and explain in 3-4 sentences:
|
|
267
|
+
- Does this page introduce NEW INFORMATION or a UNIQUE PERSPECTIVE?
|
|
268
|
+
- Or does it just REPHRASE existing knowledge from other sources?
|
|
269
|
+
- Is it substantively unique in phrasing, data, angle, or presentation?
|
|
270
|
+
- What makes it original or generic?
|
|
271
|
+
Be specific with examples.""",
|
|
272
|
+
|
|
273
|
+
"originality_score": """You are evaluating Content Originality.
|
|
274
|
+
Does this content ADD NEW INFORMATION to the web, or just rephrase what exists?
|
|
275
|
+
Evaluate:
|
|
276
|
+
- Is the content SUBSTANTIVELY UNIQUE in phrasing, perspective, data?
|
|
277
|
+
- Does it introduce NEW INFORMATION or a UNIQUE ANGLE?
|
|
278
|
+
Red flags: Templated content, spun/paraphrased, generic information
|
|
279
|
+
Score 1-10:
|
|
280
|
+
1-3 = LOW ORIGINALITY: Templated, duplicated, rehashes existing knowledge
|
|
281
|
+
4-6 = MODERATE: Mix of original and generic elements
|
|
282
|
+
7-10 = HIGH ORIGINALITY: Substantively unique, adds new information
|
|
283
|
+
Return ONLY the number.""",
|
|
284
|
+
|
|
285
|
+
"intent_reasoning": """You are evaluating Page Intent for this page. Analyze and explain in 3-4 sentences:
|
|
286
|
+
- What is this page's PRIMARY PURPOSE (the "WHY" it exists)?
|
|
287
|
+
- Is it HELPFUL-FIRST (created to help users) or SEARCH-FIRST (created to rank)?
|
|
288
|
+
- Is the intent TRANSPARENT and honest, or DECEPTIVE?
|
|
289
|
+
- What evidence supports your assessment?
|
|
290
|
+
Be specific with examples from the content.""",
|
|
291
|
+
|
|
292
|
+
"intent_score": """You are evaluating Page Intent.
|
|
293
|
+
WHY was this page created? What is its PRIMARY PURPOSE?
|
|
294
|
+
Determine if this is:
|
|
295
|
+
- HELPFUL-FIRST: Created primarily to help users/solve problems
|
|
296
|
+
- Or SEARCH-FIRST: Created primarily to rank in search
|
|
297
|
+
Red flags: Thin content for keywords, disguised affiliate, keyword stuffing
|
|
298
|
+
Green flags: Clear user problem solved, transparent purpose, genuine value
|
|
299
|
+
Score 1-10:
|
|
300
|
+
1-3 = DECEPTIVE/SEARCH-FIRST: Created for search traffic, deceptive intent
|
|
301
|
+
4-6 = UNCLEAR: Mixed signals
|
|
302
|
+
7-10 = TRANSPARENT/HELPFUL-FIRST: Created to help people, honest purpose
|
|
303
|
+
Return ONLY the number.""",
|
|
304
|
+
|
|
305
|
+
"subjective_reasoning": """You are a brutally honest content critic. Be direct, not nice. Evaluate this content for:
|
|
306
|
+
boring sections, confusing parts, unbelievable claims, unclear audience pain point,
|
|
307
|
+
missing culprit identification, sections that could be condensed, lack of proprietary insights.
|
|
308
|
+
CRITICAL: Provide EXACTLY 2-3 sentences summarizing the main weaknesses.
|
|
309
|
+
NO bullet points. NO lists. NO section headers. NO more than 3 sentences.""",
|
|
310
|
+
|
|
311
|
+
"subjective_score": """You are a brutally honest content critic evaluating subjective quality.
|
|
312
|
+
CRITICAL: Put on your most critical hat. Don't be nice. High standards only.
|
|
313
|
+
Evaluate: ENGAGEMENT (boring or compelling?), CLARITY (confusing?), CREDIBILITY (believable?),
|
|
314
|
+
AUDIENCE TARGETING (pain point addressed?), VALUE DENSITY (fluff or substance?)
|
|
315
|
+
Score 1-10:
|
|
316
|
+
1-3 = LOW QUALITY: Boring, confusing, unbelievable, generic advice
|
|
317
|
+
4-6 = MEDIOCRE: Some good parts but significant issues
|
|
318
|
+
7-10 = HIGH QUALITY: Compelling, clear, credible, dense value
|
|
319
|
+
Return ONLY the number.""",
|
|
320
|
+
|
|
321
|
+
"writing_reasoning": """You are a writing quality analyst. Evaluate this text's linguistic quality.
|
|
322
|
+
Analyze: lexical diversity (vocabulary richness/repetition), readability (sentence length 15-20 words optimal),
|
|
323
|
+
modal verbs balance, passive voice usage, and heavy adverbs.
|
|
324
|
+
CRITICAL: Provide EXACTLY 2-3 sentences summarizing the main writing issues.
|
|
325
|
+
NO bullet points. NO lists. Maximum 150 words total.""",
|
|
326
|
+
|
|
327
|
+
"writing_score": """You are a writing quality analyst evaluating objective linguistic metrics.
|
|
328
|
+
Analyze:
|
|
329
|
+
1. LEXICAL DIVERSITY: Rich vocabulary or repetitive?
|
|
330
|
+
2. READABILITY: Sentence length 15-20 words optimal, mix of easy/medium sentences
|
|
331
|
+
3. LINGUISTIC QUALITY: Modal verbs balanced, minimal passive voice, limited heavy adverbs
|
|
332
|
+
Score 1-10:
|
|
333
|
+
1-3 = POOR: Repetitive vocabulary, long complex sentences, excessive passive/adverbs
|
|
334
|
+
4-6 = AVERAGE: Some issues with readability or linguistic quality
|
|
335
|
+
7-10 = EXCELLENT: Rich vocabulary, optimal sentence length, active voice, concise
|
|
336
|
+
Return ONLY the number."""
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@dataclass
|
|
340
|
+
class EEATScore:
|
|
341
|
+
url: str
|
|
342
|
+
authorship_score: int = 0
|
|
343
|
+
authorship_reasoning: str = ""
|
|
344
|
+
citation_score: int = 0
|
|
345
|
+
citation_reasoning: str = ""
|
|
346
|
+
effort_score: int = 0
|
|
347
|
+
effort_reasoning: str = ""
|
|
348
|
+
originality_score: int = 0
|
|
349
|
+
originality_reasoning: str = ""
|
|
350
|
+
intent_score: int = 0
|
|
351
|
+
intent_reasoning: str = ""
|
|
352
|
+
subjective_score: int = 0
|
|
353
|
+
subjective_reasoning: str = ""
|
|
354
|
+
writing_score: int = 0
|
|
355
|
+
writing_reasoning: str = ""
|
|
356
|
+
overall_score: float = 0.0
|
|
357
|
+
grade: str = ""
|
|
358
|
+
analyzed_at: str = ""
|
|
359
|
+
|
|
360
|
+
class EEATAnalyzer:
|
|
361
|
+
def __init__(self, output_dir: str, provider: str = "openai",
|
|
362
|
+
model: str = "gpt-4o", temperature: float = 0.3,
|
|
363
|
+
weights: Dict[str, float] = None):
|
|
364
|
+
self.output_dir = Path(output_dir)
|
|
365
|
+
self.provider = provider
|
|
366
|
+
self.model = model
|
|
367
|
+
self.temperature = temperature
|
|
368
|
+
self.weights = weights or {
|
|
369
|
+
"authorship": 0.15,
|
|
370
|
+
"citation": 0.15,
|
|
371
|
+
"effort": 0.15,
|
|
372
|
+
"originality": 0.15,
|
|
373
|
+
"intent": 0.15,
|
|
374
|
+
"subjective": 0.15,
|
|
375
|
+
"writing": 0.10
|
|
376
|
+
}
|
|
377
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
378
|
+
self.scores: List[EEATScore] = []
|
|
379
|
+
|
|
380
|
+
async def fetch_page_content(self, url: str) -> str:
|
|
381
|
+
"""Fetch page content for analysis"""
|
|
382
|
+
try:
|
|
383
|
+
async with self.session.get(url, timeout=30) as response:
|
|
384
|
+
if response.status == 200:
|
|
385
|
+
html = await response.text()
|
|
386
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
387
|
+
|
|
388
|
+
# Remove script and style elements
|
|
389
|
+
for element in soup(['script', 'style', 'nav', 'footer', 'header']):
|
|
390
|
+
element.decompose()
|
|
391
|
+
|
|
392
|
+
# Get text content
|
|
393
|
+
text = soup.get_text(separator='\n', strip=True)
|
|
394
|
+
|
|
395
|
+
# Truncate to reasonable length for LLM
|
|
396
|
+
if len(text) > 15000:
|
|
397
|
+
text = text[:15000] + "\n[Content truncated...]"
|
|
398
|
+
|
|
399
|
+
return text
|
|
400
|
+
except Exception as e:
|
|
401
|
+
print(f"Error fetching {url}: {e}")
|
|
402
|
+
return ""
|
|
403
|
+
|
|
404
|
+
async def call_llm(self, prompt: str, content: str) -> str:
|
|
405
|
+
"""Call LLM API for analysis"""
|
|
406
|
+
if self.provider == "openai":
|
|
407
|
+
return await self._call_openai(prompt, content)
|
|
408
|
+
elif self.provider == "anthropic":
|
|
409
|
+
return await self._call_anthropic(prompt, content)
|
|
410
|
+
return ""
|
|
411
|
+
|
|
412
|
+
async def _call_openai(self, prompt: str, content: str) -> str:
|
|
413
|
+
"""Call OpenAI API"""
|
|
414
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
415
|
+
if not api_key:
|
|
416
|
+
raise ValueError("OPENAI_API_KEY not set")
|
|
417
|
+
|
|
418
|
+
headers = {
|
|
419
|
+
"Authorization": f"Bearer {api_key}",
|
|
420
|
+
"Content-Type": "application/json"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
payload = {
|
|
424
|
+
"model": self.model,
|
|
425
|
+
"messages": [
|
|
426
|
+
{"role": "system", "content": prompt},
|
|
427
|
+
{"role": "user", "content": f"Analyze this content:\n\n{content}"}
|
|
428
|
+
],
|
|
429
|
+
"temperature": self.temperature,
|
|
430
|
+
"max_tokens": 500
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
async with self.session.post(
|
|
435
|
+
"https://api.openai.com/v1/chat/completions",
|
|
436
|
+
headers=headers,
|
|
437
|
+
json=payload,
|
|
438
|
+
timeout=60
|
|
439
|
+
) as response:
|
|
440
|
+
if response.status == 200:
|
|
441
|
+
data = await response.json()
|
|
442
|
+
return data["choices"][0]["message"]["content"].strip()
|
|
443
|
+
else:
|
|
444
|
+
error = await response.text()
|
|
445
|
+
print(f"OpenAI API error: {error}")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
print(f"OpenAI API call failed: {e}")
|
|
448
|
+
return ""
|
|
449
|
+
|
|
450
|
+
async def _call_anthropic(self, prompt: str, content: str) -> str:
|
|
451
|
+
"""Call Anthropic API"""
|
|
452
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
453
|
+
if not api_key:
|
|
454
|
+
raise ValueError("ANTHROPIC_API_KEY not set")
|
|
455
|
+
|
|
456
|
+
headers = {
|
|
457
|
+
"x-api-key": api_key,
|
|
458
|
+
"Content-Type": "application/json",
|
|
459
|
+
"anthropic-version": "2023-06-01"
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
payload = {
|
|
463
|
+
"model": self.model if "claude" in self.model else "claude-3-sonnet-20240229",
|
|
464
|
+
"max_tokens": 500,
|
|
465
|
+
"messages": [
|
|
466
|
+
{"role": "user", "content": f"{prompt}\n\nAnalyze this content:\n\n{content}"}
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
async with self.session.post(
|
|
472
|
+
"https://api.anthropic.com/v1/messages",
|
|
473
|
+
headers=headers,
|
|
474
|
+
json=payload,
|
|
475
|
+
timeout=60
|
|
476
|
+
) as response:
|
|
477
|
+
if response.status == 200:
|
|
478
|
+
data = await response.json()
|
|
479
|
+
return data["content"][0]["text"].strip()
|
|
480
|
+
except Exception as e:
|
|
481
|
+
print(f"Anthropic API call failed: {e}")
|
|
482
|
+
return ""
|
|
483
|
+
|
|
484
|
+
def parse_score(self, response: str) -> int:
|
|
485
|
+
"""Extract numeric score from LLM response"""
|
|
486
|
+
# Try to find a number 1-10
|
|
487
|
+
import re
|
|
488
|
+
numbers = re.findall(r'\b([1-9]|10)\b', response)
|
|
489
|
+
if numbers:
|
|
490
|
+
return int(numbers[0])
|
|
491
|
+
return 5 # Default to middle score
|
|
492
|
+
|
|
493
|
+
def calculate_overall_score(self, score: EEATScore) -> float:
|
|
494
|
+
"""Calculate weighted overall score"""
|
|
495
|
+
total = (
|
|
496
|
+
score.authorship_score * self.weights["authorship"] +
|
|
497
|
+
score.citation_score * self.weights["citation"] +
|
|
498
|
+
score.effort_score * self.weights["effort"] +
|
|
499
|
+
score.originality_score * self.weights["originality"] +
|
|
500
|
+
score.intent_score * self.weights["intent"] +
|
|
501
|
+
score.subjective_score * self.weights["subjective"] +
|
|
502
|
+
score.writing_score * self.weights["writing"]
|
|
503
|
+
)
|
|
504
|
+
return round(total, 2)
|
|
505
|
+
|
|
506
|
+
def calculate_grade(self, overall_score: float) -> str:
|
|
507
|
+
"""Convert score to letter grade"""
|
|
508
|
+
if overall_score >= 8.0:
|
|
509
|
+
return "A"
|
|
510
|
+
elif overall_score >= 6.5:
|
|
511
|
+
return "B"
|
|
512
|
+
elif overall_score >= 5.0:
|
|
513
|
+
return "C"
|
|
514
|
+
elif overall_score >= 3.5:
|
|
515
|
+
return "D"
|
|
516
|
+
else:
|
|
517
|
+
return "F"
|
|
518
|
+
|
|
519
|
+
async def analyze_url(self, url: str) -> EEATScore:
|
|
520
|
+
"""Analyze a single URL for E-E-A-T"""
|
|
521
|
+
print(f"Analyzing: {url}")
|
|
522
|
+
|
|
523
|
+
score = EEATScore(url=url, analyzed_at=datetime.now().isoformat())
|
|
524
|
+
|
|
525
|
+
# Fetch content
|
|
526
|
+
content = await self.fetch_page_content(url)
|
|
527
|
+
if not content:
|
|
528
|
+
print(f" Could not fetch content for {url}")
|
|
529
|
+
return score
|
|
530
|
+
|
|
531
|
+
# Analyze each criterion
|
|
532
|
+
criteria = [
|
|
533
|
+
("authorship", "authorship_score", "authorship_reasoning"),
|
|
534
|
+
("citation", "citation_score", "citation_reasoning"),
|
|
535
|
+
("effort", "effort_score", "effort_reasoning"),
|
|
536
|
+
("originality", "originality_score", "originality_reasoning"),
|
|
537
|
+
("intent", "intent_score", "intent_reasoning"),
|
|
538
|
+
("subjective", "subjective_score", "subjective_reasoning"),
|
|
539
|
+
("writing", "writing_score", "writing_reasoning"),
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
for criterion, score_attr, reasoning_attr in criteria:
|
|
543
|
+
print(f" Evaluating {criterion}...")
|
|
544
|
+
|
|
545
|
+
# Get reasoning
|
|
546
|
+
reasoning_prompt = PROMPTS[f"{criterion}_reasoning"]
|
|
547
|
+
reasoning = await self.call_llm(reasoning_prompt, content)
|
|
548
|
+
setattr(score, reasoning_attr, reasoning)
|
|
549
|
+
|
|
550
|
+
# Get score
|
|
551
|
+
score_prompt = PROMPTS[f"{criterion}_score"]
|
|
552
|
+
score_response = await self.call_llm(score_prompt, content)
|
|
553
|
+
numeric_score = self.parse_score(score_response)
|
|
554
|
+
setattr(score, score_attr, numeric_score)
|
|
555
|
+
|
|
556
|
+
print(f" Score: {numeric_score}/10")
|
|
557
|
+
|
|
558
|
+
# Small delay to avoid rate limits
|
|
559
|
+
await asyncio.sleep(0.5)
|
|
560
|
+
|
|
561
|
+
# Calculate overall score and grade
|
|
562
|
+
score.overall_score = self.calculate_overall_score(score)
|
|
563
|
+
score.grade = self.calculate_grade(score.overall_score)
|
|
564
|
+
|
|
565
|
+
print(f" Overall: {score.overall_score}/10 (Grade: {score.grade})")
|
|
566
|
+
|
|
567
|
+
return score
|
|
568
|
+
|
|
569
|
+
async def analyze_urls(self, urls: List[str]):
|
|
570
|
+
"""Analyze multiple URLs"""
|
|
571
|
+
headers = {
|
|
572
|
+
'User-Agent': 'AIDevOps-EEATAnalyzer/1.0'
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
connector = aiohttp.TCPConnector(limit=5)
|
|
576
|
+
timeout = aiohttp.ClientTimeout(total=120)
|
|
577
|
+
|
|
578
|
+
async with aiohttp.ClientSession(
|
|
579
|
+
headers=headers,
|
|
580
|
+
connector=connector,
|
|
581
|
+
timeout=timeout
|
|
582
|
+
) as session:
|
|
583
|
+
self.session = session
|
|
584
|
+
|
|
585
|
+
for url in urls:
|
|
586
|
+
score = await self.analyze_url(url)
|
|
587
|
+
self.scores.append(score)
|
|
588
|
+
|
|
589
|
+
return self.scores
|
|
590
|
+
|
|
591
|
+
def export_csv(self, filename: str):
|
|
592
|
+
"""Export scores to CSV"""
|
|
593
|
+
filepath = self.output_dir / filename
|
|
594
|
+
|
|
595
|
+
fieldnames = [
|
|
596
|
+
'url', 'overall_score', 'grade',
|
|
597
|
+
'authorship_score', 'authorship_reasoning',
|
|
598
|
+
'citation_score', 'citation_reasoning',
|
|
599
|
+
'effort_score', 'effort_reasoning',
|
|
600
|
+
'originality_score', 'originality_reasoning',
|
|
601
|
+
'intent_score', 'intent_reasoning',
|
|
602
|
+
'subjective_score', 'subjective_reasoning',
|
|
603
|
+
'writing_score', 'writing_reasoning',
|
|
604
|
+
'analyzed_at'
|
|
605
|
+
]
|
|
606
|
+
|
|
607
|
+
with open(filepath, 'w', newline='', encoding='utf-8') as f:
|
|
608
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
609
|
+
writer.writeheader()
|
|
610
|
+
for score in self.scores:
|
|
611
|
+
writer.writerow(asdict(score))
|
|
612
|
+
|
|
613
|
+
print(f"Exported: {filepath}")
|
|
614
|
+
|
|
615
|
+
def export_xlsx(self, filename: str):
|
|
616
|
+
"""Export scores to Excel with formatting"""
|
|
617
|
+
if not HAS_OPENPYXL:
|
|
618
|
+
print("openpyxl not installed, skipping XLSX export")
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
filepath = self.output_dir / filename
|
|
622
|
+
wb = openpyxl.Workbook()
|
|
623
|
+
ws = wb.active
|
|
624
|
+
ws.title = "E-E-A-T Scores"
|
|
625
|
+
|
|
626
|
+
# Headers
|
|
627
|
+
headers = [
|
|
628
|
+
'URL', 'Overall Score', 'Grade',
|
|
629
|
+
'Authorship', 'Authorship Notes',
|
|
630
|
+
'Citation', 'Citation Notes',
|
|
631
|
+
'Effort', 'Effort Notes',
|
|
632
|
+
'Originality', 'Originality Notes',
|
|
633
|
+
'Intent', 'Intent Notes',
|
|
634
|
+
'Subjective', 'Subjective Notes',
|
|
635
|
+
'Writing', 'Writing Notes',
|
|
636
|
+
'Analyzed At'
|
|
637
|
+
]
|
|
638
|
+
|
|
639
|
+
# Header styling
|
|
640
|
+
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
|
641
|
+
header_font = Font(color="FFFFFF", bold=True)
|
|
642
|
+
|
|
643
|
+
for col, header in enumerate(headers, 1):
|
|
644
|
+
cell = ws.cell(row=1, column=col, value=header)
|
|
645
|
+
cell.fill = header_fill
|
|
646
|
+
cell.font = header_font
|
|
647
|
+
cell.alignment = Alignment(horizontal='center')
|
|
648
|
+
|
|
649
|
+
# Grade colors
|
|
650
|
+
grade_colors = {
|
|
651
|
+
'A': '00B050', # Green
|
|
652
|
+
'B': '92D050', # Light green
|
|
653
|
+
'C': 'FFEB9C', # Yellow
|
|
654
|
+
'D': 'FFC7CE', # Light red
|
|
655
|
+
'F': 'FF0000', # Red
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# Data rows
|
|
659
|
+
for row_num, score in enumerate(self.scores, 2):
|
|
660
|
+
ws.cell(row=row_num, column=1, value=score.url)
|
|
661
|
+
ws.cell(row=row_num, column=2, value=score.overall_score)
|
|
662
|
+
|
|
663
|
+
grade_cell = ws.cell(row=row_num, column=3, value=score.grade)
|
|
664
|
+
if score.grade in grade_colors:
|
|
665
|
+
grade_cell.fill = PatternFill(
|
|
666
|
+
start_color=grade_colors[score.grade],
|
|
667
|
+
end_color=grade_colors[score.grade],
|
|
668
|
+
fill_type="solid"
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
ws.cell(row=row_num, column=4, value=score.authorship_score)
|
|
672
|
+
ws.cell(row=row_num, column=5, value=score.authorship_reasoning)
|
|
673
|
+
ws.cell(row=row_num, column=6, value=score.citation_score)
|
|
674
|
+
ws.cell(row=row_num, column=7, value=score.citation_reasoning)
|
|
675
|
+
ws.cell(row=row_num, column=8, value=score.effort_score)
|
|
676
|
+
ws.cell(row=row_num, column=9, value=score.effort_reasoning)
|
|
677
|
+
ws.cell(row=row_num, column=10, value=score.originality_score)
|
|
678
|
+
ws.cell(row=row_num, column=11, value=score.originality_reasoning)
|
|
679
|
+
ws.cell(row=row_num, column=12, value=score.intent_score)
|
|
680
|
+
ws.cell(row=row_num, column=13, value=score.intent_reasoning)
|
|
681
|
+
ws.cell(row=row_num, column=14, value=score.subjective_score)
|
|
682
|
+
ws.cell(row=row_num, column=15, value=score.subjective_reasoning)
|
|
683
|
+
ws.cell(row=row_num, column=16, value=score.writing_score)
|
|
684
|
+
ws.cell(row=row_num, column=17, value=score.writing_reasoning)
|
|
685
|
+
ws.cell(row=row_num, column=18, value=score.analyzed_at)
|
|
686
|
+
|
|
687
|
+
# Adjust column widths
|
|
688
|
+
ws.column_dimensions['A'].width = 50
|
|
689
|
+
ws.column_dimensions['B'].width = 12
|
|
690
|
+
ws.column_dimensions['C'].width = 8
|
|
691
|
+
for col in ['D', 'F', 'H', 'J', 'L', 'N', 'P']:
|
|
692
|
+
ws.column_dimensions[col].width = 10
|
|
693
|
+
for col in ['E', 'G', 'I', 'K', 'M', 'O', 'Q']:
|
|
694
|
+
ws.column_dimensions[col].width = 40
|
|
695
|
+
ws.column_dimensions['R'].width = 20
|
|
696
|
+
|
|
697
|
+
# Freeze header row
|
|
698
|
+
ws.freeze_panes = 'A2'
|
|
699
|
+
|
|
700
|
+
wb.save(filepath)
|
|
701
|
+
print(f"Exported: {filepath}")
|
|
702
|
+
|
|
703
|
+
def export_summary(self, filename: str = "eeat-summary.json"):
|
|
704
|
+
"""Export summary statistics"""
|
|
705
|
+
if not self.scores:
|
|
706
|
+
return
|
|
707
|
+
|
|
708
|
+
summary = {
|
|
709
|
+
"analyzed_at": datetime.now().isoformat(),
|
|
710
|
+
"total_pages": len(self.scores),
|
|
711
|
+
"average_scores": {
|
|
712
|
+
"overall": round(sum(s.overall_score for s in self.scores) / len(self.scores), 2),
|
|
713
|
+
"authorship": round(sum(s.authorship_score for s in self.scores) / len(self.scores), 2),
|
|
714
|
+
"citation": round(sum(s.citation_score for s in self.scores) / len(self.scores), 2),
|
|
715
|
+
"effort": round(sum(s.effort_score for s in self.scores) / len(self.scores), 2),
|
|
716
|
+
"originality": round(sum(s.originality_score for s in self.scores) / len(self.scores), 2),
|
|
717
|
+
"intent": round(sum(s.intent_score for s in self.scores) / len(self.scores), 2),
|
|
718
|
+
"subjective": round(sum(s.subjective_score for s in self.scores) / len(self.scores), 2),
|
|
719
|
+
"writing": round(sum(s.writing_score for s in self.scores) / len(self.scores), 2),
|
|
720
|
+
},
|
|
721
|
+
"grade_distribution": {
|
|
722
|
+
"A": sum(1 for s in self.scores if s.grade == "A"),
|
|
723
|
+
"B": sum(1 for s in self.scores if s.grade == "B"),
|
|
724
|
+
"C": sum(1 for s in self.scores if s.grade == "C"),
|
|
725
|
+
"D": sum(1 for s in self.scores if s.grade == "D"),
|
|
726
|
+
"F": sum(1 for s in self.scores if s.grade == "F"),
|
|
727
|
+
},
|
|
728
|
+
"weakest_areas": [],
|
|
729
|
+
"strongest_areas": []
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# Find weakest and strongest areas
|
|
733
|
+
avg_scores = summary["average_scores"]
|
|
734
|
+
sorted_areas = sorted(
|
|
735
|
+
[(k, v) for k, v in avg_scores.items() if k != "overall"],
|
|
736
|
+
key=lambda x: x[1]
|
|
737
|
+
)
|
|
738
|
+
summary["weakest_areas"] = [a[0] for a in sorted_areas[:2]]
|
|
739
|
+
summary["strongest_areas"] = [a[0] for a in sorted_areas[-2:]]
|
|
740
|
+
|
|
741
|
+
filepath = self.output_dir / filename
|
|
742
|
+
with open(filepath, 'w') as f:
|
|
743
|
+
json.dump(summary, f, indent=2)
|
|
744
|
+
|
|
745
|
+
print(f"Exported: {filepath}")
|
|
746
|
+
return summary
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
async def main():
|
|
750
|
+
import argparse
|
|
751
|
+
|
|
752
|
+
parser = argparse.ArgumentParser(description='E-E-A-T Score Analyzer')
|
|
753
|
+
parser.add_argument('urls', nargs='+', help='URLs to analyze')
|
|
754
|
+
parser.add_argument('--output', '-o', required=True, help='Output directory')
|
|
755
|
+
parser.add_argument('--provider', default='openai', choices=['openai', 'anthropic'])
|
|
756
|
+
parser.add_argument('--model', default='gpt-4o', help='LLM model to use')
|
|
757
|
+
parser.add_argument('--format', '-f', choices=['csv', 'xlsx', 'all'], default='xlsx')
|
|
758
|
+
parser.add_argument('--domain', help='Domain name for output files')
|
|
759
|
+
|
|
760
|
+
args = parser.parse_args()
|
|
761
|
+
|
|
762
|
+
# Create output directory
|
|
763
|
+
output_dir = Path(args.output)
|
|
764
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
765
|
+
|
|
766
|
+
analyzer = EEATAnalyzer(
|
|
767
|
+
output_dir=str(output_dir),
|
|
768
|
+
provider=args.provider,
|
|
769
|
+
model=args.model
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
await analyzer.analyze_urls(args.urls)
|
|
773
|
+
|
|
774
|
+
# Generate filename
|
|
775
|
+
domain = args.domain or "eeat-analysis"
|
|
776
|
+
timestamp = datetime.now().strftime("%Y-%m-%d")
|
|
777
|
+
|
|
778
|
+
if args.format in ("xlsx", "all"):
|
|
779
|
+
analyzer.export_xlsx(f"{domain}-eeat-score-{timestamp}.xlsx")
|
|
780
|
+
if args.format in ("csv", "all"):
|
|
781
|
+
analyzer.export_csv(f"{domain}-eeat-score-{timestamp}.csv")
|
|
782
|
+
|
|
783
|
+
summary = analyzer.export_summary()
|
|
784
|
+
|
|
785
|
+
print(f"\n=== E-E-A-T Analysis Summary ===")
|
|
786
|
+
print(f"Pages analyzed: {summary['total_pages']}")
|
|
787
|
+
print(f"Average overall score: {summary['average_scores']['overall']}/10")
|
|
788
|
+
print(f"Grade distribution: A={summary['grade_distribution']['A']}, "
|
|
789
|
+
f"B={summary['grade_distribution']['B']}, C={summary['grade_distribution']['C']}, "
|
|
790
|
+
f"D={summary['grade_distribution']['D']}, F={summary['grade_distribution']['F']}")
|
|
791
|
+
print(f"Weakest areas: {', '.join(summary['weakest_areas'])}")
|
|
792
|
+
print(f"Strongest areas: {', '.join(summary['strongest_areas'])}")
|
|
793
|
+
print(f"\nResults saved to: {output_dir}")
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
if __name__ == "__main__":
|
|
797
|
+
asyncio.run(main())
|
|
798
|
+
PYTHON_SCRIPT
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
# Analyze crawled pages
|
|
802
|
+
do_analyze() {
|
|
803
|
+
local input_file="$1"
|
|
804
|
+
shift
|
|
805
|
+
|
|
806
|
+
if [[ ! -f "$input_file" ]]; then
|
|
807
|
+
print_error "Input file not found: $input_file"
|
|
808
|
+
return 1
|
|
809
|
+
fi
|
|
810
|
+
|
|
811
|
+
# Parse options
|
|
812
|
+
local output_base="$DEFAULT_OUTPUT_DIR"
|
|
813
|
+
local format="$OUTPUT_FORMAT"
|
|
814
|
+
local provider="$LLM_PROVIDER"
|
|
815
|
+
local model="$LLM_MODEL"
|
|
816
|
+
|
|
817
|
+
while [[ $# -gt 0 ]]; do
|
|
818
|
+
case "$1" in
|
|
819
|
+
--output)
|
|
820
|
+
output_base="$2"
|
|
821
|
+
shift 2
|
|
822
|
+
;;
|
|
823
|
+
--format)
|
|
824
|
+
format="$2"
|
|
825
|
+
shift 2
|
|
826
|
+
;;
|
|
827
|
+
--provider)
|
|
828
|
+
provider="$2"
|
|
829
|
+
shift 2
|
|
830
|
+
;;
|
|
831
|
+
--model)
|
|
832
|
+
model="$2"
|
|
833
|
+
shift 2
|
|
834
|
+
;;
|
|
835
|
+
*)
|
|
836
|
+
shift
|
|
837
|
+
;;
|
|
838
|
+
esac
|
|
839
|
+
done
|
|
840
|
+
|
|
841
|
+
# Extract URLs from crawl data
|
|
842
|
+
local urls=()
|
|
843
|
+
if [[ "$input_file" == *.json ]]; then
|
|
844
|
+
# JSON format - extract URLs with status 200
|
|
845
|
+
mapfile -t urls < <(jq -r '.[] | select(.status_code == 200) | .url' "$input_file" 2>/dev/null || echo "")
|
|
846
|
+
elif [[ "$input_file" == *.csv ]]; then
|
|
847
|
+
# CSV format - extract URLs from first column where status is 200
|
|
848
|
+
mapfile -t urls < <(tail -n +2 "$input_file" | awk -F',' '$2 == "200" || $2 == 200 {gsub(/"/, "", $1); print $1}')
|
|
849
|
+
fi
|
|
850
|
+
|
|
851
|
+
if [[ ${#urls[@]} -eq 0 ]]; then
|
|
852
|
+
print_error "No valid URLs found in input file"
|
|
853
|
+
return 1
|
|
854
|
+
fi
|
|
855
|
+
|
|
856
|
+
print_header "E-E-A-T Score Analysis"
|
|
857
|
+
print_info "Input: $input_file"
|
|
858
|
+
print_info "URLs to analyze: ${#urls[@]}"
|
|
859
|
+
|
|
860
|
+
# Determine domain from first URL
|
|
861
|
+
local domain
|
|
862
|
+
domain=$(get_domain "${urls[0]}")
|
|
863
|
+
|
|
864
|
+
# Get output directory (use same as crawl data if in domain folder)
|
|
865
|
+
local input_dir
|
|
866
|
+
input_dir=$(dirname "$input_file")
|
|
867
|
+
local output_dir
|
|
868
|
+
|
|
869
|
+
if [[ "$input_dir" == *"$domain"* ]]; then
|
|
870
|
+
output_dir="$input_dir"
|
|
871
|
+
else
|
|
872
|
+
output_dir=$(create_output_dir "$domain" "$output_base")
|
|
873
|
+
fi
|
|
874
|
+
|
|
875
|
+
print_info "Output: $output_dir"
|
|
876
|
+
|
|
877
|
+
# Check Python dependencies
|
|
878
|
+
if ! python3 -c "import aiohttp, bs4" 2>/dev/null; then
|
|
879
|
+
print_warning "Installing Python dependencies..."
|
|
880
|
+
pip3 install aiohttp beautifulsoup4 openpyxl --quiet
|
|
881
|
+
fi
|
|
882
|
+
|
|
883
|
+
# Generate and run analyzer
|
|
884
|
+
local analyzer_script="/tmp/eeat_analyzer_$$.py"
|
|
885
|
+
generate_analyzer_script > "$analyzer_script"
|
|
886
|
+
|
|
887
|
+
# Limit to reasonable number for API costs
|
|
888
|
+
local max_urls=50
|
|
889
|
+
if [[ ${#urls[@]} -gt $max_urls ]]; then
|
|
890
|
+
print_warning "Limiting analysis to first $max_urls URLs (of ${#urls[@]})"
|
|
891
|
+
urls=("${urls[@]:0:$max_urls}")
|
|
892
|
+
fi
|
|
893
|
+
|
|
894
|
+
python3 "$analyzer_script" "${urls[@]}" \
|
|
895
|
+
--output "$output_dir" \
|
|
896
|
+
--provider "$provider" \
|
|
897
|
+
--model "$model" \
|
|
898
|
+
--format "$format" \
|
|
899
|
+
--domain "$domain"
|
|
900
|
+
|
|
901
|
+
rm -f "$analyzer_script"
|
|
902
|
+
|
|
903
|
+
print_success "E-E-A-T analysis complete!"
|
|
904
|
+
print_info "Results: $output_dir"
|
|
905
|
+
|
|
906
|
+
return 0
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
# Score single URL
|
|
910
|
+
do_score() {
|
|
911
|
+
local url="$1"
|
|
912
|
+
shift
|
|
913
|
+
|
|
914
|
+
local verbose=false
|
|
915
|
+
local output_base="$DEFAULT_OUTPUT_DIR"
|
|
916
|
+
|
|
917
|
+
while [[ $# -gt 0 ]]; do
|
|
918
|
+
case "$1" in
|
|
919
|
+
--verbose|-v)
|
|
920
|
+
verbose=true
|
|
921
|
+
shift
|
|
922
|
+
;;
|
|
923
|
+
--output)
|
|
924
|
+
output_base="$2"
|
|
925
|
+
shift 2
|
|
926
|
+
;;
|
|
927
|
+
*)
|
|
928
|
+
shift
|
|
929
|
+
;;
|
|
930
|
+
esac
|
|
931
|
+
done
|
|
932
|
+
|
|
933
|
+
local domain
|
|
934
|
+
domain=$(get_domain "$url")
|
|
935
|
+
local output_dir
|
|
936
|
+
output_dir=$(create_output_dir "$domain" "$output_base")
|
|
937
|
+
|
|
938
|
+
print_header "E-E-A-T Score Analysis"
|
|
939
|
+
print_info "URL: $url"
|
|
940
|
+
|
|
941
|
+
# Check Python dependencies
|
|
942
|
+
if ! python3 -c "import aiohttp, bs4" 2>/dev/null; then
|
|
943
|
+
print_warning "Installing Python dependencies..."
|
|
944
|
+
pip3 install aiohttp beautifulsoup4 openpyxl --quiet
|
|
945
|
+
fi
|
|
946
|
+
|
|
947
|
+
local analyzer_script="/tmp/eeat_analyzer_$$.py"
|
|
948
|
+
generate_analyzer_script > "$analyzer_script"
|
|
949
|
+
|
|
950
|
+
python3 "$analyzer_script" "$url" \
|
|
951
|
+
--output "$output_dir" \
|
|
952
|
+
--provider "$LLM_PROVIDER" \
|
|
953
|
+
--model "$LLM_MODEL" \
|
|
954
|
+
--format "all" \
|
|
955
|
+
--domain "$domain"
|
|
956
|
+
|
|
957
|
+
rm -f "$analyzer_script"
|
|
958
|
+
|
|
959
|
+
print_success "Analysis complete!"
|
|
960
|
+
print_info "Results: $output_dir"
|
|
961
|
+
|
|
962
|
+
return 0
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
# Batch analyze URLs from file
|
|
966
|
+
do_batch() {
|
|
967
|
+
local urls_file="$1"
|
|
968
|
+
shift
|
|
969
|
+
|
|
970
|
+
if [[ ! -f "$urls_file" ]]; then
|
|
971
|
+
print_error "URLs file not found: $urls_file"
|
|
972
|
+
return 1
|
|
973
|
+
fi
|
|
974
|
+
|
|
975
|
+
local urls=()
|
|
976
|
+
while IFS= read -r url; do
|
|
977
|
+
[[ -n "$url" && ! "$url" =~ ^# ]] && urls+=("$url")
|
|
978
|
+
done < "$urls_file"
|
|
979
|
+
|
|
980
|
+
if [[ ${#urls[@]} -eq 0 ]]; then
|
|
981
|
+
print_error "No URLs found in file"
|
|
982
|
+
return 1
|
|
983
|
+
fi
|
|
984
|
+
|
|
985
|
+
local output_base="$DEFAULT_OUTPUT_DIR"
|
|
986
|
+
local format="$OUTPUT_FORMAT"
|
|
987
|
+
|
|
988
|
+
while [[ $# -gt 0 ]]; do
|
|
989
|
+
case "$1" in
|
|
990
|
+
--output)
|
|
991
|
+
output_base="$2"
|
|
992
|
+
shift 2
|
|
993
|
+
;;
|
|
994
|
+
--format)
|
|
995
|
+
format="$2"
|
|
996
|
+
shift 2
|
|
997
|
+
;;
|
|
998
|
+
*)
|
|
999
|
+
shift
|
|
1000
|
+
;;
|
|
1001
|
+
esac
|
|
1002
|
+
done
|
|
1003
|
+
|
|
1004
|
+
local domain
|
|
1005
|
+
domain=$(get_domain "${urls[0]}")
|
|
1006
|
+
local output_dir
|
|
1007
|
+
output_dir=$(create_output_dir "$domain" "$output_base")
|
|
1008
|
+
|
|
1009
|
+
print_header "E-E-A-T Batch Analysis"
|
|
1010
|
+
print_info "URLs: ${#urls[@]}"
|
|
1011
|
+
print_info "Output: $output_dir"
|
|
1012
|
+
|
|
1013
|
+
# Check Python dependencies
|
|
1014
|
+
if ! python3 -c "import aiohttp, bs4" 2>/dev/null; then
|
|
1015
|
+
print_warning "Installing Python dependencies..."
|
|
1016
|
+
pip3 install aiohttp beautifulsoup4 openpyxl --quiet
|
|
1017
|
+
fi
|
|
1018
|
+
|
|
1019
|
+
local analyzer_script="/tmp/eeat_analyzer_$$.py"
|
|
1020
|
+
generate_analyzer_script > "$analyzer_script"
|
|
1021
|
+
|
|
1022
|
+
python3 "$analyzer_script" "${urls[@]}" \
|
|
1023
|
+
--output "$output_dir" \
|
|
1024
|
+
--provider "$LLM_PROVIDER" \
|
|
1025
|
+
--model "$LLM_MODEL" \
|
|
1026
|
+
--format "$format" \
|
|
1027
|
+
--domain "$domain"
|
|
1028
|
+
|
|
1029
|
+
rm -f "$analyzer_script"
|
|
1030
|
+
|
|
1031
|
+
print_success "Batch analysis complete!"
|
|
1032
|
+
print_info "Results: $output_dir"
|
|
1033
|
+
|
|
1034
|
+
return 0
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
# Generate report from existing scores
|
|
1038
|
+
do_report() {
|
|
1039
|
+
local scores_file="$1"
|
|
1040
|
+
shift
|
|
1041
|
+
|
|
1042
|
+
if [[ ! -f "$scores_file" ]]; then
|
|
1043
|
+
print_error "Scores file not found: $scores_file"
|
|
1044
|
+
return 1
|
|
1045
|
+
fi
|
|
1046
|
+
|
|
1047
|
+
print_header "Generating E-E-A-T Report"
|
|
1048
|
+
print_info "Input: $scores_file"
|
|
1049
|
+
|
|
1050
|
+
# For now, just display summary from JSON
|
|
1051
|
+
if [[ "$scores_file" == *.json ]]; then
|
|
1052
|
+
if command -v jq &> /dev/null; then
|
|
1053
|
+
jq '.' "$scores_file"
|
|
1054
|
+
else
|
|
1055
|
+
cat "$scores_file"
|
|
1056
|
+
fi
|
|
1057
|
+
fi
|
|
1058
|
+
|
|
1059
|
+
return 0
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
# Check status
|
|
1063
|
+
check_status() {
|
|
1064
|
+
print_header "E-E-A-T Score Helper Status"
|
|
1065
|
+
|
|
1066
|
+
# Check dependencies
|
|
1067
|
+
print_info "Checking dependencies..."
|
|
1068
|
+
|
|
1069
|
+
if command -v curl &> /dev/null; then
|
|
1070
|
+
print_success "curl: installed"
|
|
1071
|
+
else
|
|
1072
|
+
print_error "curl: not installed"
|
|
1073
|
+
fi
|
|
1074
|
+
|
|
1075
|
+
if command -v jq &> /dev/null; then
|
|
1076
|
+
print_success "jq: installed"
|
|
1077
|
+
else
|
|
1078
|
+
print_error "jq: not installed"
|
|
1079
|
+
fi
|
|
1080
|
+
|
|
1081
|
+
if command -v python3 &> /dev/null; then
|
|
1082
|
+
print_success "python3: installed"
|
|
1083
|
+
|
|
1084
|
+
if python3 -c "import aiohttp" 2>/dev/null; then
|
|
1085
|
+
print_success " aiohttp: installed"
|
|
1086
|
+
else
|
|
1087
|
+
print_warning " aiohttp: not installed (pip3 install aiohttp)"
|
|
1088
|
+
fi
|
|
1089
|
+
|
|
1090
|
+
if python3 -c "import bs4" 2>/dev/null; then
|
|
1091
|
+
print_success " beautifulsoup4: installed"
|
|
1092
|
+
else
|
|
1093
|
+
print_warning " beautifulsoup4: not installed"
|
|
1094
|
+
fi
|
|
1095
|
+
|
|
1096
|
+
if python3 -c "import openpyxl" 2>/dev/null; then
|
|
1097
|
+
print_success " openpyxl: installed"
|
|
1098
|
+
else
|
|
1099
|
+
print_warning " openpyxl: not installed"
|
|
1100
|
+
fi
|
|
1101
|
+
else
|
|
1102
|
+
print_error "python3: not installed"
|
|
1103
|
+
fi
|
|
1104
|
+
|
|
1105
|
+
# Check API keys
|
|
1106
|
+
print_info "Checking API keys..."
|
|
1107
|
+
|
|
1108
|
+
if [[ -n "${OPENAI_API_KEY:-}" ]]; then
|
|
1109
|
+
print_success "OPENAI_API_KEY: set"
|
|
1110
|
+
else
|
|
1111
|
+
print_warning "OPENAI_API_KEY: not set"
|
|
1112
|
+
fi
|
|
1113
|
+
|
|
1114
|
+
if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
1115
|
+
print_success "ANTHROPIC_API_KEY: set"
|
|
1116
|
+
else
|
|
1117
|
+
print_info "ANTHROPIC_API_KEY: not set (optional)"
|
|
1118
|
+
fi
|
|
1119
|
+
|
|
1120
|
+
# Check config
|
|
1121
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
1122
|
+
print_success "Config: $CONFIG_FILE"
|
|
1123
|
+
else
|
|
1124
|
+
print_info "Config: using defaults"
|
|
1125
|
+
fi
|
|
1126
|
+
|
|
1127
|
+
return 0
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
# Show help
|
|
1131
|
+
show_help() {
|
|
1132
|
+
cat << 'EOF'
|
|
1133
|
+
E-E-A-T Score Helper - Content Quality Analysis
|
|
1134
|
+
|
|
1135
|
+
Usage: eeat-score-helper.sh [command] [input] [options]
|
|
1136
|
+
|
|
1137
|
+
Commands:
|
|
1138
|
+
analyze <crawl-data> Analyze pages from site-crawler output
|
|
1139
|
+
score <url> Score a single URL
|
|
1140
|
+
batch <urls-file> Batch analyze URLs from a file
|
|
1141
|
+
report <scores-file> Generate report from existing scores
|
|
1142
|
+
status Check dependencies and configuration
|
|
1143
|
+
help Show this help message
|
|
1144
|
+
|
|
1145
|
+
Options:
|
|
1146
|
+
--output <dir> Output directory (default: ~/Downloads)
|
|
1147
|
+
--format <fmt> Output format: csv, xlsx, all (default: xlsx)
|
|
1148
|
+
--provider <name> LLM provider: openai, anthropic (default: openai)
|
|
1149
|
+
--model <name> LLM model (default: gpt-4o)
|
|
1150
|
+
--verbose Show detailed output
|
|
1151
|
+
|
|
1152
|
+
Examples:
|
|
1153
|
+
# Analyze crawled pages
|
|
1154
|
+
eeat-score-helper.sh analyze ~/Downloads/example.com/_latest/crawl-data.json
|
|
1155
|
+
|
|
1156
|
+
# Score single URL
|
|
1157
|
+
eeat-score-helper.sh score https://example.com/blog/article
|
|
1158
|
+
|
|
1159
|
+
# Batch analyze
|
|
1160
|
+
eeat-score-helper.sh batch urls.txt --format xlsx
|
|
1161
|
+
|
|
1162
|
+
# Check status
|
|
1163
|
+
eeat-score-helper.sh status
|
|
1164
|
+
|
|
1165
|
+
Output Structure:
|
|
1166
|
+
~/Downloads/{domain}/{timestamp}/
|
|
1167
|
+
- {domain}-eeat-score-{date}.xlsx E-E-A-T scores with reasoning
|
|
1168
|
+
- {domain}-eeat-score-{date}.csv Same data in CSV format
|
|
1169
|
+
- eeat-summary.json Summary statistics
|
|
1170
|
+
|
|
1171
|
+
~/Downloads/{domain}/_latest -> symlink to latest analysis
|
|
1172
|
+
|
|
1173
|
+
Scoring Criteria (1-10 scale):
|
|
1174
|
+
- Authorship & Expertise (15%): Author credentials, verifiable entity
|
|
1175
|
+
- Citation Quality (15%): Source quality, substantiation
|
|
1176
|
+
- Content Effort (15%): Replicability, depth, original research
|
|
1177
|
+
- Original Content (15%): Unique perspective, new information
|
|
1178
|
+
- Page Intent (15%): Helpful-first vs search-first
|
|
1179
|
+
- Subjective Quality (15%): Engagement, clarity, credibility
|
|
1180
|
+
- Writing Quality (10%): Lexical diversity, readability
|
|
1181
|
+
|
|
1182
|
+
Grades:
|
|
1183
|
+
A (8.0-10.0): Excellent E-E-A-T
|
|
1184
|
+
B (6.5-7.9): Good E-E-A-T
|
|
1185
|
+
C (5.0-6.4): Average E-E-A-T
|
|
1186
|
+
D (3.5-4.9): Poor E-E-A-T
|
|
1187
|
+
F (1.0-3.4): Very poor E-E-A-T
|
|
1188
|
+
|
|
1189
|
+
Environment Variables:
|
|
1190
|
+
OPENAI_API_KEY Required for OpenAI provider
|
|
1191
|
+
ANTHROPIC_API_KEY Required for Anthropic provider
|
|
1192
|
+
|
|
1193
|
+
Related:
|
|
1194
|
+
- Site crawler: site-crawler-helper.sh
|
|
1195
|
+
- Crawl4AI: crawl4ai-helper.sh
|
|
1196
|
+
EOF
|
|
1197
|
+
return 0
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
# Main function
|
|
1201
|
+
main() {
|
|
1202
|
+
load_config
|
|
1203
|
+
|
|
1204
|
+
local command="${1:-help}"
|
|
1205
|
+
shift || true
|
|
1206
|
+
|
|
1207
|
+
case "$command" in
|
|
1208
|
+
analyze)
|
|
1209
|
+
check_dependencies || exit 1
|
|
1210
|
+
check_api_key || exit 1
|
|
1211
|
+
do_analyze "$@"
|
|
1212
|
+
;;
|
|
1213
|
+
score)
|
|
1214
|
+
check_dependencies || exit 1
|
|
1215
|
+
check_api_key || exit 1
|
|
1216
|
+
do_score "$@"
|
|
1217
|
+
;;
|
|
1218
|
+
batch)
|
|
1219
|
+
check_dependencies || exit 1
|
|
1220
|
+
check_api_key || exit 1
|
|
1221
|
+
do_batch "$@"
|
|
1222
|
+
;;
|
|
1223
|
+
report)
|
|
1224
|
+
do_report "$@"
|
|
1225
|
+
;;
|
|
1226
|
+
status)
|
|
1227
|
+
check_status
|
|
1228
|
+
;;
|
|
1229
|
+
help|-h|--help|"")
|
|
1230
|
+
show_help
|
|
1231
|
+
;;
|
|
1232
|
+
*)
|
|
1233
|
+
print_error "Unknown command: $command"
|
|
1234
|
+
show_help
|
|
1235
|
+
exit 1
|
|
1236
|
+
;;
|
|
1237
|
+
esac
|
|
1238
|
+
|
|
1239
|
+
return 0
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
main "$@"
|