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.
Files changed (329) hide show
  1. package/.agent/AGENTS.md +614 -0
  2. package/.agent/accounts.md +65 -0
  3. package/.agent/aidevops/add-new-mcp-to-aidevops.md +456 -0
  4. package/.agent/aidevops/api-integrations.md +335 -0
  5. package/.agent/aidevops/architecture.md +510 -0
  6. package/.agent/aidevops/configs.md +274 -0
  7. package/.agent/aidevops/docs.md +244 -0
  8. package/.agent/aidevops/extension.md +311 -0
  9. package/.agent/aidevops/mcp-integrations.md +340 -0
  10. package/.agent/aidevops/mcp-troubleshooting.md +162 -0
  11. package/.agent/aidevops/memory-patterns.md +172 -0
  12. package/.agent/aidevops/providers.md +217 -0
  13. package/.agent/aidevops/recommendations.md +321 -0
  14. package/.agent/aidevops/requirements.md +301 -0
  15. package/.agent/aidevops/resources.md +214 -0
  16. package/.agent/aidevops/security-requirements.md +174 -0
  17. package/.agent/aidevops/security.md +350 -0
  18. package/.agent/aidevops/service-links.md +400 -0
  19. package/.agent/aidevops/services.md +357 -0
  20. package/.agent/aidevops/setup.md +153 -0
  21. package/.agent/aidevops/troubleshooting.md +389 -0
  22. package/.agent/aidevops.md +124 -0
  23. package/.agent/build-plus.md +244 -0
  24. package/.agent/content/guidelines.md +109 -0
  25. package/.agent/content.md +87 -0
  26. package/.agent/health.md +59 -0
  27. package/.agent/legal.md +59 -0
  28. package/.agent/loop-state/full-loop.local.md +16 -0
  29. package/.agent/loop-state/ralph-loop.local.md +10 -0
  30. package/.agent/marketing.md +440 -0
  31. package/.agent/memory/README.md +260 -0
  32. package/.agent/onboarding.md +796 -0
  33. package/.agent/plan-plus.md +245 -0
  34. package/.agent/research.md +100 -0
  35. package/.agent/sales.md +333 -0
  36. package/.agent/scripts/101domains-helper.sh +701 -0
  37. package/.agent/scripts/add-missing-returns.sh +140 -0
  38. package/.agent/scripts/agent-browser-helper.sh +311 -0
  39. package/.agent/scripts/agno-setup.sh +712 -0
  40. package/.agent/scripts/ahrefs-mcp-wrapper.js +168 -0
  41. package/.agent/scripts/aidevops-update-check.sh +71 -0
  42. package/.agent/scripts/ampcode-cli.sh +522 -0
  43. package/.agent/scripts/auto-version-bump.sh +156 -0
  44. package/.agent/scripts/autogen-helper.sh +512 -0
  45. package/.agent/scripts/beads-sync-helper.sh +596 -0
  46. package/.agent/scripts/closte-helper.sh +5 -0
  47. package/.agent/scripts/cloudron-helper.sh +321 -0
  48. package/.agent/scripts/codacy-cli-chunked.sh +581 -0
  49. package/.agent/scripts/codacy-cli.sh +442 -0
  50. package/.agent/scripts/code-audit-helper.sh +5 -0
  51. package/.agent/scripts/coderabbit-cli.sh +417 -0
  52. package/.agent/scripts/coderabbit-pro-analysis.sh +238 -0
  53. package/.agent/scripts/commands/code-simplifier.md +86 -0
  54. package/.agent/scripts/commands/full-loop.md +246 -0
  55. package/.agent/scripts/commands/postflight-loop.md +103 -0
  56. package/.agent/scripts/commands/recall.md +182 -0
  57. package/.agent/scripts/commands/remember.md +132 -0
  58. package/.agent/scripts/commands/save-todo.md +175 -0
  59. package/.agent/scripts/commands/session-review.md +154 -0
  60. package/.agent/scripts/comprehensive-quality-fix.sh +106 -0
  61. package/.agent/scripts/context-builder-helper.sh +522 -0
  62. package/.agent/scripts/coolify-cli-helper.sh +674 -0
  63. package/.agent/scripts/coolify-helper.sh +380 -0
  64. package/.agent/scripts/crawl4ai-examples.sh +401 -0
  65. package/.agent/scripts/crawl4ai-helper.sh +1078 -0
  66. package/.agent/scripts/crewai-helper.sh +681 -0
  67. package/.agent/scripts/dev-browser-helper.sh +513 -0
  68. package/.agent/scripts/dns-helper.sh +396 -0
  69. package/.agent/scripts/domain-research-helper.sh +917 -0
  70. package/.agent/scripts/dspy-helper.sh +285 -0
  71. package/.agent/scripts/dspyground-helper.sh +291 -0
  72. package/.agent/scripts/eeat-score-helper.sh +1242 -0
  73. package/.agent/scripts/efficient-return-fix.sh +92 -0
  74. package/.agent/scripts/extract-opencode-prompts.sh +128 -0
  75. package/.agent/scripts/find-missing-returns.sh +113 -0
  76. package/.agent/scripts/fix-auth-headers.sh +104 -0
  77. package/.agent/scripts/fix-common-strings.sh +254 -0
  78. package/.agent/scripts/fix-content-type.sh +100 -0
  79. package/.agent/scripts/fix-error-messages.sh +130 -0
  80. package/.agent/scripts/fix-misplaced-returns.sh +74 -0
  81. package/.agent/scripts/fix-remaining-literals.sh +152 -0
  82. package/.agent/scripts/fix-return-statements.sh +41 -0
  83. package/.agent/scripts/fix-s131-default-cases.sh +249 -0
  84. package/.agent/scripts/fix-sc2155-simple.sh +102 -0
  85. package/.agent/scripts/fix-shellcheck-critical.sh +187 -0
  86. package/.agent/scripts/fix-string-literals.sh +273 -0
  87. package/.agent/scripts/full-loop-helper.sh +773 -0
  88. package/.agent/scripts/generate-opencode-agents.sh +497 -0
  89. package/.agent/scripts/generate-opencode-commands.sh +1629 -0
  90. package/.agent/scripts/generate-skills.sh +366 -0
  91. package/.agent/scripts/git-platforms-helper.sh +640 -0
  92. package/.agent/scripts/gitea-cli-helper.sh +743 -0
  93. package/.agent/scripts/github-cli-helper.sh +702 -0
  94. package/.agent/scripts/gitlab-cli-helper.sh +682 -0
  95. package/.agent/scripts/gsc-add-user-helper.sh +325 -0
  96. package/.agent/scripts/gsc-sitemap-helper.sh +678 -0
  97. package/.agent/scripts/hetzner-helper.sh +485 -0
  98. package/.agent/scripts/hostinger-helper.sh +229 -0
  99. package/.agent/scripts/keyword-research-helper.sh +1815 -0
  100. package/.agent/scripts/langflow-helper.sh +544 -0
  101. package/.agent/scripts/linkedin-automation.py +241 -0
  102. package/.agent/scripts/linter-manager.sh +599 -0
  103. package/.agent/scripts/linters-local.sh +434 -0
  104. package/.agent/scripts/list-keys-helper.sh +488 -0
  105. package/.agent/scripts/local-browser-automation.py +339 -0
  106. package/.agent/scripts/localhost-helper.sh +744 -0
  107. package/.agent/scripts/loop-common.sh +806 -0
  108. package/.agent/scripts/mainwp-helper.sh +728 -0
  109. package/.agent/scripts/markdown-formatter.sh +338 -0
  110. package/.agent/scripts/markdown-lint-fix.sh +311 -0
  111. package/.agent/scripts/mass-fix-returns.sh +58 -0
  112. package/.agent/scripts/mcp-diagnose.sh +167 -0
  113. package/.agent/scripts/mcp-inspector-helper.sh +449 -0
  114. package/.agent/scripts/memory-helper.sh +650 -0
  115. package/.agent/scripts/monitor-code-review.sh +255 -0
  116. package/.agent/scripts/onboarding-helper.sh +706 -0
  117. package/.agent/scripts/opencode-github-setup-helper.sh +797 -0
  118. package/.agent/scripts/opencode-test-helper.sh +213 -0
  119. package/.agent/scripts/pagespeed-helper.sh +464 -0
  120. package/.agent/scripts/pandoc-helper.sh +362 -0
  121. package/.agent/scripts/postflight-check.sh +555 -0
  122. package/.agent/scripts/pre-commit-hook.sh +259 -0
  123. package/.agent/scripts/pre-edit-check.sh +169 -0
  124. package/.agent/scripts/qlty-cli.sh +356 -0
  125. package/.agent/scripts/quality-cli-manager.sh +525 -0
  126. package/.agent/scripts/quality-feedback-helper.sh +462 -0
  127. package/.agent/scripts/quality-fix.sh +263 -0
  128. package/.agent/scripts/quality-loop-helper.sh +1108 -0
  129. package/.agent/scripts/ralph-loop-helper.sh +836 -0
  130. package/.agent/scripts/ralph-upstream-check.sh +341 -0
  131. package/.agent/scripts/secretlint-helper.sh +847 -0
  132. package/.agent/scripts/servers-helper.sh +241 -0
  133. package/.agent/scripts/ses-helper.sh +619 -0
  134. package/.agent/scripts/session-review-helper.sh +404 -0
  135. package/.agent/scripts/setup-linters-wizard.sh +379 -0
  136. package/.agent/scripts/setup-local-api-keys.sh +330 -0
  137. package/.agent/scripts/setup-mcp-integrations.sh +472 -0
  138. package/.agent/scripts/shared-constants.sh +246 -0
  139. package/.agent/scripts/site-crawler-helper.sh +1487 -0
  140. package/.agent/scripts/snyk-helper.sh +940 -0
  141. package/.agent/scripts/sonarcloud-autofix.sh +193 -0
  142. package/.agent/scripts/sonarcloud-cli.sh +191 -0
  143. package/.agent/scripts/sonarscanner-cli.sh +455 -0
  144. package/.agent/scripts/spaceship-helper.sh +747 -0
  145. package/.agent/scripts/stagehand-helper.sh +321 -0
  146. package/.agent/scripts/stagehand-python-helper.sh +321 -0
  147. package/.agent/scripts/stagehand-python-setup.sh +441 -0
  148. package/.agent/scripts/stagehand-setup.sh +439 -0
  149. package/.agent/scripts/system-cleanup.sh +340 -0
  150. package/.agent/scripts/terminal-title-helper.sh +388 -0
  151. package/.agent/scripts/terminal-title-setup.sh +549 -0
  152. package/.agent/scripts/test-stagehand-both-integration.sh +317 -0
  153. package/.agent/scripts/test-stagehand-integration.sh +309 -0
  154. package/.agent/scripts/test-stagehand-python-integration.sh +341 -0
  155. package/.agent/scripts/todo-ready.sh +263 -0
  156. package/.agent/scripts/tool-version-check.sh +362 -0
  157. package/.agent/scripts/toon-helper.sh +469 -0
  158. package/.agent/scripts/twilio-helper.sh +917 -0
  159. package/.agent/scripts/updown-helper.sh +279 -0
  160. package/.agent/scripts/validate-mcp-integrations.sh +250 -0
  161. package/.agent/scripts/validate-version-consistency.sh +131 -0
  162. package/.agent/scripts/vaultwarden-helper.sh +597 -0
  163. package/.agent/scripts/vercel-cli-helper.sh +816 -0
  164. package/.agent/scripts/verify-mirrors.sh +169 -0
  165. package/.agent/scripts/version-manager.sh +831 -0
  166. package/.agent/scripts/webhosting-helper.sh +471 -0
  167. package/.agent/scripts/webhosting-verify.sh +238 -0
  168. package/.agent/scripts/wordpress-mcp-helper.sh +508 -0
  169. package/.agent/scripts/worktree-helper.sh +595 -0
  170. package/.agent/scripts/worktree-sessions.sh +577 -0
  171. package/.agent/seo/dataforseo.md +215 -0
  172. package/.agent/seo/domain-research.md +532 -0
  173. package/.agent/seo/eeat-score.md +659 -0
  174. package/.agent/seo/google-search-console.md +366 -0
  175. package/.agent/seo/gsc-sitemaps.md +282 -0
  176. package/.agent/seo/keyword-research.md +521 -0
  177. package/.agent/seo/serper.md +278 -0
  178. package/.agent/seo/site-crawler.md +387 -0
  179. package/.agent/seo.md +236 -0
  180. package/.agent/services/accounting/quickfile.md +159 -0
  181. package/.agent/services/communications/telfon.md +470 -0
  182. package/.agent/services/communications/twilio.md +569 -0
  183. package/.agent/services/crm/fluentcrm.md +449 -0
  184. package/.agent/services/email/ses.md +399 -0
  185. package/.agent/services/hosting/101domains.md +378 -0
  186. package/.agent/services/hosting/closte.md +177 -0
  187. package/.agent/services/hosting/cloudflare.md +251 -0
  188. package/.agent/services/hosting/cloudron.md +478 -0
  189. package/.agent/services/hosting/dns-providers.md +335 -0
  190. package/.agent/services/hosting/domain-purchasing.md +344 -0
  191. package/.agent/services/hosting/hetzner.md +327 -0
  192. package/.agent/services/hosting/hostinger.md +287 -0
  193. package/.agent/services/hosting/localhost.md +419 -0
  194. package/.agent/services/hosting/spaceship.md +353 -0
  195. package/.agent/services/hosting/webhosting.md +330 -0
  196. package/.agent/social-media.md +69 -0
  197. package/.agent/templates/plans-template.md +114 -0
  198. package/.agent/templates/prd-template.md +129 -0
  199. package/.agent/templates/tasks-template.md +108 -0
  200. package/.agent/templates/todo-template.md +89 -0
  201. package/.agent/tools/ai-assistants/agno.md +471 -0
  202. package/.agent/tools/ai-assistants/capsolver.md +326 -0
  203. package/.agent/tools/ai-assistants/configuration.md +221 -0
  204. package/.agent/tools/ai-assistants/overview.md +209 -0
  205. package/.agent/tools/ai-assistants/status.md +171 -0
  206. package/.agent/tools/ai-assistants/windsurf.md +193 -0
  207. package/.agent/tools/ai-orchestration/autogen.md +406 -0
  208. package/.agent/tools/ai-orchestration/crewai.md +445 -0
  209. package/.agent/tools/ai-orchestration/langflow.md +405 -0
  210. package/.agent/tools/ai-orchestration/openprose.md +487 -0
  211. package/.agent/tools/ai-orchestration/overview.md +362 -0
  212. package/.agent/tools/ai-orchestration/packaging.md +647 -0
  213. package/.agent/tools/browser/agent-browser.md +464 -0
  214. package/.agent/tools/browser/browser-automation.md +400 -0
  215. package/.agent/tools/browser/chrome-devtools.md +282 -0
  216. package/.agent/tools/browser/crawl4ai-integration.md +422 -0
  217. package/.agent/tools/browser/crawl4ai-resources.md +277 -0
  218. package/.agent/tools/browser/crawl4ai-usage.md +416 -0
  219. package/.agent/tools/browser/crawl4ai.md +585 -0
  220. package/.agent/tools/browser/dev-browser.md +341 -0
  221. package/.agent/tools/browser/pagespeed.md +260 -0
  222. package/.agent/tools/browser/playwright.md +266 -0
  223. package/.agent/tools/browser/playwriter.md +310 -0
  224. package/.agent/tools/browser/stagehand-examples.md +456 -0
  225. package/.agent/tools/browser/stagehand-python.md +483 -0
  226. package/.agent/tools/browser/stagehand.md +421 -0
  227. package/.agent/tools/build-agent/agent-review.md +224 -0
  228. package/.agent/tools/build-agent/build-agent.md +784 -0
  229. package/.agent/tools/build-mcp/aidevops-plugin.md +476 -0
  230. package/.agent/tools/build-mcp/api-wrapper.md +445 -0
  231. package/.agent/tools/build-mcp/build-mcp.md +240 -0
  232. package/.agent/tools/build-mcp/deployment.md +401 -0
  233. package/.agent/tools/build-mcp/server-patterns.md +632 -0
  234. package/.agent/tools/build-mcp/transports.md +366 -0
  235. package/.agent/tools/code-review/auditing.md +383 -0
  236. package/.agent/tools/code-review/automation.md +219 -0
  237. package/.agent/tools/code-review/best-practices.md +203 -0
  238. package/.agent/tools/code-review/codacy.md +151 -0
  239. package/.agent/tools/code-review/code-simplifier.md +174 -0
  240. package/.agent/tools/code-review/code-standards.md +309 -0
  241. package/.agent/tools/code-review/coderabbit.md +101 -0
  242. package/.agent/tools/code-review/management.md +155 -0
  243. package/.agent/tools/code-review/qlty.md +248 -0
  244. package/.agent/tools/code-review/secretlint.md +565 -0
  245. package/.agent/tools/code-review/setup.md +250 -0
  246. package/.agent/tools/code-review/snyk.md +563 -0
  247. package/.agent/tools/code-review/tools.md +230 -0
  248. package/.agent/tools/content/summarize.md +353 -0
  249. package/.agent/tools/context/augment-context-engine.md +468 -0
  250. package/.agent/tools/context/context-builder-agent.md +76 -0
  251. package/.agent/tools/context/context-builder.md +375 -0
  252. package/.agent/tools/context/context7.md +371 -0
  253. package/.agent/tools/context/dspy.md +302 -0
  254. package/.agent/tools/context/dspyground.md +374 -0
  255. package/.agent/tools/context/llm-tldr.md +219 -0
  256. package/.agent/tools/context/osgrep.md +488 -0
  257. package/.agent/tools/context/prompt-optimization.md +338 -0
  258. package/.agent/tools/context/toon.md +292 -0
  259. package/.agent/tools/conversion/pandoc.md +304 -0
  260. package/.agent/tools/credentials/api-key-management.md +154 -0
  261. package/.agent/tools/credentials/api-key-setup.md +224 -0
  262. package/.agent/tools/credentials/environment-variables.md +180 -0
  263. package/.agent/tools/credentials/vaultwarden.md +382 -0
  264. package/.agent/tools/data-extraction/outscraper.md +974 -0
  265. package/.agent/tools/deployment/coolify-cli.md +388 -0
  266. package/.agent/tools/deployment/coolify-setup.md +353 -0
  267. package/.agent/tools/deployment/coolify.md +345 -0
  268. package/.agent/tools/deployment/vercel.md +390 -0
  269. package/.agent/tools/git/authentication.md +132 -0
  270. package/.agent/tools/git/gitea-cli.md +193 -0
  271. package/.agent/tools/git/github-actions.md +207 -0
  272. package/.agent/tools/git/github-cli.md +223 -0
  273. package/.agent/tools/git/gitlab-cli.md +190 -0
  274. package/.agent/tools/git/opencode-github-security.md +350 -0
  275. package/.agent/tools/git/opencode-github.md +328 -0
  276. package/.agent/tools/git/opencode-gitlab.md +252 -0
  277. package/.agent/tools/git/security.md +196 -0
  278. package/.agent/tools/git.md +207 -0
  279. package/.agent/tools/opencode/oh-my-opencode.md +375 -0
  280. package/.agent/tools/opencode/opencode-anthropic-auth.md +446 -0
  281. package/.agent/tools/opencode/opencode.md +651 -0
  282. package/.agent/tools/social-media/bird.md +437 -0
  283. package/.agent/tools/task-management/beads.md +336 -0
  284. package/.agent/tools/terminal/terminal-title.md +251 -0
  285. package/.agent/tools/ui/shadcn.md +196 -0
  286. package/.agent/tools/ui/ui-skills.md +115 -0
  287. package/.agent/tools/wordpress/localwp.md +311 -0
  288. package/.agent/tools/wordpress/mainwp.md +391 -0
  289. package/.agent/tools/wordpress/scf.md +527 -0
  290. package/.agent/tools/wordpress/wp-admin.md +729 -0
  291. package/.agent/tools/wordpress/wp-dev.md +940 -0
  292. package/.agent/tools/wordpress/wp-preferred.md +398 -0
  293. package/.agent/tools/wordpress.md +95 -0
  294. package/.agent/workflows/branch/bugfix.md +63 -0
  295. package/.agent/workflows/branch/chore.md +95 -0
  296. package/.agent/workflows/branch/experiment.md +115 -0
  297. package/.agent/workflows/branch/feature.md +59 -0
  298. package/.agent/workflows/branch/hotfix.md +98 -0
  299. package/.agent/workflows/branch/refactor.md +92 -0
  300. package/.agent/workflows/branch/release.md +96 -0
  301. package/.agent/workflows/branch.md +347 -0
  302. package/.agent/workflows/bug-fixing.md +267 -0
  303. package/.agent/workflows/changelog.md +129 -0
  304. package/.agent/workflows/code-audit-remote.md +279 -0
  305. package/.agent/workflows/conversation-starter.md +69 -0
  306. package/.agent/workflows/error-feedback.md +578 -0
  307. package/.agent/workflows/feature-development.md +355 -0
  308. package/.agent/workflows/git-workflow.md +702 -0
  309. package/.agent/workflows/multi-repo-workspace.md +268 -0
  310. package/.agent/workflows/plans.md +709 -0
  311. package/.agent/workflows/postflight.md +604 -0
  312. package/.agent/workflows/pr.md +571 -0
  313. package/.agent/workflows/preflight.md +278 -0
  314. package/.agent/workflows/ralph-loop.md +773 -0
  315. package/.agent/workflows/release.md +498 -0
  316. package/.agent/workflows/session-manager.md +254 -0
  317. package/.agent/workflows/session-review.md +311 -0
  318. package/.agent/workflows/sql-migrations.md +631 -0
  319. package/.agent/workflows/version-bump.md +283 -0
  320. package/.agent/workflows/wiki-update.md +333 -0
  321. package/.agent/workflows/worktree.md +477 -0
  322. package/LICENSE +21 -0
  323. package/README.md +1446 -0
  324. package/VERSION +1 -0
  325. package/aidevops.sh +1746 -0
  326. package/bin/aidevops +21 -0
  327. package/package.json +75 -0
  328. package/scripts/npm-postinstall.js +60 -0
  329. 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 "$@"