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,1815 @@
|
|
|
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
|
+
# Keyword Research Helper Script
|
|
5
|
+
# Comprehensive keyword research with SERP weakness detection and opportunity scoring
|
|
6
|
+
# Providers: DataForSEO (primary), Serper (alternative), Ahrefs (optional)
|
|
7
|
+
# Webmaster Tools: Google Search Console, Bing Webmaster Tools (for owned sites)
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# Source shared constants
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit
|
|
13
|
+
if [[ -f "$SCRIPT_DIR/shared-constants.sh" ]]; then
|
|
14
|
+
source "$SCRIPT_DIR/shared-constants.sh"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Configuration
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
readonly CONFIG_FILE="$HOME/.config/aidevops/keyword-research.json"
|
|
22
|
+
readonly CONFIG_DIR="$HOME/.config/aidevops"
|
|
23
|
+
readonly DOWNLOADS_DIR="$HOME/Downloads"
|
|
24
|
+
readonly CACHE_DIR="$HOME/.cache/aidevops/keyword-research"
|
|
25
|
+
|
|
26
|
+
# Default settings
|
|
27
|
+
DEFAULT_LOCALE="us-en"
|
|
28
|
+
DEFAULT_PROVIDER="dataforseo"
|
|
29
|
+
DEFAULT_LIMIT=100
|
|
30
|
+
MAX_LIMIT=10000
|
|
31
|
+
|
|
32
|
+
# Location codes for DataForSEO (bash 3.2 compatible - no associative arrays)
|
|
33
|
+
get_location_code() {
|
|
34
|
+
local locale="$1"
|
|
35
|
+
case "$locale" in
|
|
36
|
+
"us-en") echo "2840" ;;
|
|
37
|
+
"uk-en") echo "2826" ;;
|
|
38
|
+
"ca-en") echo "2124" ;;
|
|
39
|
+
"au-en") echo "2036" ;;
|
|
40
|
+
"de-de") echo "2276" ;;
|
|
41
|
+
"fr-fr") echo "2250" ;;
|
|
42
|
+
"es-es") echo "2724" ;;
|
|
43
|
+
custom-*) echo "${locale#custom-}" ;;
|
|
44
|
+
*) echo "2840" ;; # Default to US
|
|
45
|
+
esac
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get_language_code() {
|
|
50
|
+
local locale="$1"
|
|
51
|
+
case "$locale" in
|
|
52
|
+
"us-en"|"uk-en"|"ca-en"|"au-en") echo "en" ;;
|
|
53
|
+
"de-de") echo "de" ;;
|
|
54
|
+
"fr-fr") echo "fr" ;;
|
|
55
|
+
"es-es") echo "es" ;;
|
|
56
|
+
custom-*) echo "en" ;; # Default to English for custom
|
|
57
|
+
*) echo "en" ;;
|
|
58
|
+
esac
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# SERP Weakness thresholds
|
|
63
|
+
readonly THRESHOLD_LOW_DS=10
|
|
64
|
+
readonly THRESHOLD_LOW_PS=0
|
|
65
|
+
readonly THRESHOLD_SLOW_PAGE=3000
|
|
66
|
+
readonly THRESHOLD_HIGH_SPAM=50
|
|
67
|
+
readonly THRESHOLD_OLD_CONTENT_YEARS=2
|
|
68
|
+
readonly THRESHOLD_UGC_HEAVY=3
|
|
69
|
+
|
|
70
|
+
# Colors
|
|
71
|
+
readonly RED='\033[0;31m'
|
|
72
|
+
readonly GREEN='\033[0;32m'
|
|
73
|
+
readonly BLUE='\033[0;34m'
|
|
74
|
+
readonly YELLOW='\033[1;33m'
|
|
75
|
+
readonly PURPLE='\033[0;35m'
|
|
76
|
+
readonly CYAN='\033[0;36m'
|
|
77
|
+
readonly NC='\033[0m'
|
|
78
|
+
|
|
79
|
+
# =============================================================================
|
|
80
|
+
# Utility Functions
|
|
81
|
+
# =============================================================================
|
|
82
|
+
|
|
83
|
+
print_header() { local msg="$1"; echo -e "${PURPLE}═══ $msg ═══${NC}"; return 0; }
|
|
84
|
+
print_info() { local msg="$1"; echo -e "${BLUE}[INFO]${NC} $msg"; return 0; }
|
|
85
|
+
print_success() { local msg="$1"; echo -e "${GREEN}[SUCCESS]${NC} $msg"; return 0; }
|
|
86
|
+
print_warning() { local msg="$1"; echo -e "${YELLOW}[WARNING]${NC} $msg"; return 0; }
|
|
87
|
+
print_error() { local msg="$1"; echo -e "${RED}[ERROR]${NC} $msg" >&2; return 0; }
|
|
88
|
+
|
|
89
|
+
# Ensure directories exist
|
|
90
|
+
ensure_directories() {
|
|
91
|
+
mkdir -p "$CONFIG_DIR"
|
|
92
|
+
mkdir -p "$CACHE_DIR"
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# =============================================================================
|
|
97
|
+
# Configuration Management
|
|
98
|
+
# =============================================================================
|
|
99
|
+
|
|
100
|
+
load_config() {
|
|
101
|
+
ensure_directories
|
|
102
|
+
|
|
103
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
104
|
+
# Load existing config
|
|
105
|
+
DEFAULT_LOCALE=$(jq -r '.default_locale // "us-en"' "$CONFIG_FILE" 2>/dev/null || echo "us-en")
|
|
106
|
+
DEFAULT_PROVIDER=$(jq -r '.default_provider // "dataforseo"' "$CONFIG_FILE" 2>/dev/null || echo "dataforseo")
|
|
107
|
+
DEFAULT_LIMIT=$(jq -r '.default_limit // 100' "$CONFIG_FILE" 2>/dev/null || echo "100")
|
|
108
|
+
fi
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
save_config() {
|
|
113
|
+
local locale="$1"
|
|
114
|
+
local provider="$2"
|
|
115
|
+
local limit="$3"
|
|
116
|
+
|
|
117
|
+
ensure_directories
|
|
118
|
+
|
|
119
|
+
cat > "$CONFIG_FILE" << EOF
|
|
120
|
+
{
|
|
121
|
+
"default_locale": "$locale",
|
|
122
|
+
"default_provider": "$provider",
|
|
123
|
+
"default_limit": $limit,
|
|
124
|
+
"include_ahrefs": false,
|
|
125
|
+
"csv_directory": "$DOWNLOADS_DIR"
|
|
126
|
+
}
|
|
127
|
+
EOF
|
|
128
|
+
print_success "Configuration saved to $CONFIG_FILE"
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
show_config() {
|
|
133
|
+
print_header "Current Configuration"
|
|
134
|
+
|
|
135
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
136
|
+
cat "$CONFIG_FILE" | jq .
|
|
137
|
+
else
|
|
138
|
+
print_info "No configuration file found. Using defaults."
|
|
139
|
+
echo " Locale: $DEFAULT_LOCALE"
|
|
140
|
+
echo " Provider: $DEFAULT_PROVIDER"
|
|
141
|
+
echo " Limit: $DEFAULT_LIMIT"
|
|
142
|
+
fi
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# Credential Checking
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
check_credentials() {
|
|
151
|
+
local provider="$1"
|
|
152
|
+
local has_creds=false
|
|
153
|
+
|
|
154
|
+
# Source credentials
|
|
155
|
+
if [[ -f "$HOME/.config/aidevops/mcp-env.sh" ]]; then
|
|
156
|
+
source "$HOME/.config/aidevops/mcp-env.sh"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
case "$provider" in
|
|
160
|
+
"dataforseo")
|
|
161
|
+
if [[ -n "${DATAFORSEO_USERNAME:-}" ]] && [[ -n "${DATAFORSEO_PASSWORD:-}" ]]; then
|
|
162
|
+
has_creds=true
|
|
163
|
+
fi
|
|
164
|
+
;;
|
|
165
|
+
"serper")
|
|
166
|
+
if [[ -n "${SERPER_API_KEY:-}" ]]; then
|
|
167
|
+
has_creds=true
|
|
168
|
+
fi
|
|
169
|
+
;;
|
|
170
|
+
"ahrefs")
|
|
171
|
+
if [[ -n "${AHREFS_API_KEY:-}" ]]; then
|
|
172
|
+
has_creds=true
|
|
173
|
+
fi
|
|
174
|
+
;;
|
|
175
|
+
"both")
|
|
176
|
+
if [[ -n "${DATAFORSEO_USERNAME:-}" ]] && [[ -n "${SERPER_API_KEY:-}" ]]; then
|
|
177
|
+
has_creds=true
|
|
178
|
+
fi
|
|
179
|
+
;;
|
|
180
|
+
*)
|
|
181
|
+
print_error "Unknown provider: $provider"
|
|
182
|
+
return 1
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
|
|
186
|
+
if [[ "$has_creds" == "false" ]]; then
|
|
187
|
+
print_error "Missing credentials for provider: $provider"
|
|
188
|
+
print_info "Run '/list-keys' to check your API keys"
|
|
189
|
+
print_info "Configure in ~/.config/aidevops/mcp-env.sh"
|
|
190
|
+
return 1
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# =============================================================================
|
|
197
|
+
# Locale Selection
|
|
198
|
+
# =============================================================================
|
|
199
|
+
|
|
200
|
+
prompt_locale() {
|
|
201
|
+
print_header "Select Location/Language"
|
|
202
|
+
echo ""
|
|
203
|
+
echo " 1) US/English (default)"
|
|
204
|
+
echo " 2) UK/English"
|
|
205
|
+
echo " 3) Canada/English"
|
|
206
|
+
echo " 4) Australia/English"
|
|
207
|
+
echo " 5) Germany/German"
|
|
208
|
+
echo " 6) France/French"
|
|
209
|
+
echo " 7) Spain/Spanish"
|
|
210
|
+
echo " 8) Custom (enter location code)"
|
|
211
|
+
echo ""
|
|
212
|
+
read -p "Select option [1]: " choice
|
|
213
|
+
|
|
214
|
+
case "${choice:-1}" in
|
|
215
|
+
1) echo "us-en" ;;
|
|
216
|
+
2) echo "uk-en" ;;
|
|
217
|
+
3) echo "ca-en" ;;
|
|
218
|
+
4) echo "au-en" ;;
|
|
219
|
+
5) echo "de-de" ;;
|
|
220
|
+
6) echo "fr-fr" ;;
|
|
221
|
+
7) echo "es-es" ;;
|
|
222
|
+
8)
|
|
223
|
+
read -p "Enter DataForSEO location code: " custom_code
|
|
224
|
+
echo "custom-$custom_code"
|
|
225
|
+
;;
|
|
226
|
+
*) echo "us-en" ;;
|
|
227
|
+
esac
|
|
228
|
+
return 0
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# =============================================================================
|
|
232
|
+
# DataForSEO API Functions
|
|
233
|
+
# =============================================================================
|
|
234
|
+
|
|
235
|
+
dataforseo_request() {
|
|
236
|
+
local endpoint="$1"
|
|
237
|
+
local data="$2"
|
|
238
|
+
|
|
239
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
240
|
+
|
|
241
|
+
local auth
|
|
242
|
+
auth=$(echo -n "${DATAFORSEO_USERNAME}:${DATAFORSEO_PASSWORD}" | base64)
|
|
243
|
+
|
|
244
|
+
curl -s -X POST \
|
|
245
|
+
"https://api.dataforseo.com/v3/$endpoint" \
|
|
246
|
+
-H "Authorization: Basic $auth" \
|
|
247
|
+
-H "Content-Type: application/json" \
|
|
248
|
+
-d "$data"
|
|
249
|
+
return 0
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Keyword suggestions (seed keyword expansion)
|
|
253
|
+
dataforseo_keyword_suggestions() {
|
|
254
|
+
local keyword="$1"
|
|
255
|
+
local location_code="$2"
|
|
256
|
+
local language_code="$3"
|
|
257
|
+
local limit="$4"
|
|
258
|
+
|
|
259
|
+
local data
|
|
260
|
+
data=$(cat << EOF
|
|
261
|
+
[{
|
|
262
|
+
"keyword": "$keyword",
|
|
263
|
+
"location_code": $location_code,
|
|
264
|
+
"language_code": "$language_code",
|
|
265
|
+
"limit": $limit,
|
|
266
|
+
"include_seed_keyword": true,
|
|
267
|
+
"include_serp_info": true
|
|
268
|
+
}]
|
|
269
|
+
EOF
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
dataforseo_request "dataforseo_labs/google/keyword_suggestions/live" "$data"
|
|
273
|
+
return 0
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Google autocomplete (uses keyword_suggestions for richer data)
|
|
277
|
+
dataforseo_autocomplete() {
|
|
278
|
+
local keyword="$1"
|
|
279
|
+
local location_code="$2"
|
|
280
|
+
local language_code="$3"
|
|
281
|
+
|
|
282
|
+
local data
|
|
283
|
+
data=$(cat << EOF
|
|
284
|
+
[{
|
|
285
|
+
"keyword": "$keyword",
|
|
286
|
+
"location_code": $location_code,
|
|
287
|
+
"language_code": "$language_code",
|
|
288
|
+
"limit": 50,
|
|
289
|
+
"include_seed_keyword": true
|
|
290
|
+
}]
|
|
291
|
+
EOF
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
dataforseo_request "dataforseo_labs/google/keyword_suggestions/live" "$data"
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Ranked keywords (competitor research)
|
|
299
|
+
dataforseo_ranked_keywords() {
|
|
300
|
+
local domain="$1"
|
|
301
|
+
local location_code="$2"
|
|
302
|
+
local language_code="$3"
|
|
303
|
+
local limit="$4"
|
|
304
|
+
|
|
305
|
+
local data
|
|
306
|
+
data=$(cat << EOF
|
|
307
|
+
[{
|
|
308
|
+
"target": "$domain",
|
|
309
|
+
"location_code": $location_code,
|
|
310
|
+
"language_code": "$language_code",
|
|
311
|
+
"limit": $limit,
|
|
312
|
+
"order_by": ["keyword_data.keyword_info.search_volume,desc"]
|
|
313
|
+
}]
|
|
314
|
+
EOF
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
dataforseo_request "dataforseo_labs/google/ranked_keywords/live" "$data"
|
|
318
|
+
return 0
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Domain intersection (keyword gap)
|
|
322
|
+
dataforseo_keyword_gap() {
|
|
323
|
+
local your_domain="$1"
|
|
324
|
+
local competitor_domain="$2"
|
|
325
|
+
local location_code="$3"
|
|
326
|
+
local language_code="$4"
|
|
327
|
+
local limit="$5"
|
|
328
|
+
|
|
329
|
+
local data
|
|
330
|
+
data=$(cat << EOF
|
|
331
|
+
[{
|
|
332
|
+
"target1": "$competitor_domain",
|
|
333
|
+
"target2": "$your_domain",
|
|
334
|
+
"location_code": $location_code,
|
|
335
|
+
"language_code": "$language_code",
|
|
336
|
+
"limit": $limit,
|
|
337
|
+
"intersections": false,
|
|
338
|
+
"order_by": ["first_domain_serp_element.etv,desc"]
|
|
339
|
+
}]
|
|
340
|
+
EOF
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
dataforseo_request "dataforseo_labs/google/domain_intersection/live" "$data"
|
|
344
|
+
return 0
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Backlinks summary (domain/page scores)
|
|
348
|
+
dataforseo_backlinks_summary() {
|
|
349
|
+
local target="$1"
|
|
350
|
+
|
|
351
|
+
local data
|
|
352
|
+
data=$(cat << EOF
|
|
353
|
+
[{
|
|
354
|
+
"target": "$target",
|
|
355
|
+
"include_subdomains": true
|
|
356
|
+
}]
|
|
357
|
+
EOF
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
dataforseo_request "backlinks/summary/live" "$data"
|
|
361
|
+
return 0
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# SERP organic results
|
|
365
|
+
dataforseo_serp_organic() {
|
|
366
|
+
local keyword="$1"
|
|
367
|
+
local location_code="$2"
|
|
368
|
+
local language_code="$3"
|
|
369
|
+
|
|
370
|
+
local data
|
|
371
|
+
data=$(cat << EOF
|
|
372
|
+
[{
|
|
373
|
+
"keyword": "$keyword",
|
|
374
|
+
"location_code": $location_code,
|
|
375
|
+
"language_code": "$language_code",
|
|
376
|
+
"device": "desktop",
|
|
377
|
+
"os": "windows",
|
|
378
|
+
"depth": 10
|
|
379
|
+
}]
|
|
380
|
+
EOF
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
dataforseo_request "serp/google/organic/live/regular" "$data"
|
|
384
|
+
return 0
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# On-page instant (page speed, technical analysis)
|
|
388
|
+
dataforseo_onpage_instant() {
|
|
389
|
+
local url="$1"
|
|
390
|
+
|
|
391
|
+
local data
|
|
392
|
+
data=$(cat << EOF
|
|
393
|
+
[{
|
|
394
|
+
"url": "$url",
|
|
395
|
+
"enable_javascript": true,
|
|
396
|
+
"load_resources": true
|
|
397
|
+
}]
|
|
398
|
+
EOF
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
dataforseo_request "on_page/instant_pages" "$data"
|
|
402
|
+
return 0
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# =============================================================================
|
|
406
|
+
# Serper API Functions
|
|
407
|
+
# =============================================================================
|
|
408
|
+
|
|
409
|
+
serper_request() {
|
|
410
|
+
local endpoint="$1"
|
|
411
|
+
local data="$2"
|
|
412
|
+
|
|
413
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
414
|
+
|
|
415
|
+
curl -s -X POST \
|
|
416
|
+
"https://google.serper.dev/$endpoint" \
|
|
417
|
+
-H "X-API-KEY: ${SERPER_API_KEY}" \
|
|
418
|
+
-H "Content-Type: application/json" \
|
|
419
|
+
-d "$data"
|
|
420
|
+
return 0
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
serper_search() {
|
|
424
|
+
local query="$1"
|
|
425
|
+
local location="$2"
|
|
426
|
+
local num="$3"
|
|
427
|
+
|
|
428
|
+
local data
|
|
429
|
+
data=$(cat << EOF
|
|
430
|
+
{
|
|
431
|
+
"q": "$query",
|
|
432
|
+
"gl": "$location",
|
|
433
|
+
"num": $num
|
|
434
|
+
}
|
|
435
|
+
EOF
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
serper_request "search" "$data"
|
|
439
|
+
return 0
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
serper_autocomplete() {
|
|
443
|
+
local query="$1"
|
|
444
|
+
local location="$2"
|
|
445
|
+
|
|
446
|
+
local data
|
|
447
|
+
data=$(cat << EOF
|
|
448
|
+
{
|
|
449
|
+
"q": "$query",
|
|
450
|
+
"gl": "$location"
|
|
451
|
+
}
|
|
452
|
+
EOF
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
serper_request "autocomplete" "$data"
|
|
456
|
+
return 0
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# =============================================================================
|
|
460
|
+
# Ahrefs API Functions
|
|
461
|
+
# =============================================================================
|
|
462
|
+
|
|
463
|
+
ahrefs_request() {
|
|
464
|
+
local endpoint="$1"
|
|
465
|
+
local params="$2"
|
|
466
|
+
|
|
467
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
468
|
+
|
|
469
|
+
curl -s -X GET \
|
|
470
|
+
"https://api.ahrefs.com/v3/$endpoint?$params" \
|
|
471
|
+
-H "Authorization: Bearer ${AHREFS_API_KEY}"
|
|
472
|
+
return 0
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
ahrefs_domain_rating() {
|
|
476
|
+
local domain="$1"
|
|
477
|
+
local today
|
|
478
|
+
today=$(date +%Y-%m-%d)
|
|
479
|
+
|
|
480
|
+
ahrefs_request "site-explorer/domain-rating" "target=$domain&date=$today"
|
|
481
|
+
return 0
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
ahrefs_url_rating() {
|
|
485
|
+
local url="$1"
|
|
486
|
+
local today
|
|
487
|
+
today=$(date +%Y-%m-%d)
|
|
488
|
+
|
|
489
|
+
ahrefs_request "site-explorer/url-rating" "target=$url&date=$today"
|
|
490
|
+
return 0
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# =============================================================================
|
|
494
|
+
# Google Search Console API Functions
|
|
495
|
+
# =============================================================================
|
|
496
|
+
|
|
497
|
+
gsc_request() {
|
|
498
|
+
local endpoint="$1"
|
|
499
|
+
local data="$2"
|
|
500
|
+
|
|
501
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
502
|
+
|
|
503
|
+
# Check for service account credentials
|
|
504
|
+
if [[ -z "${GSC_ACCESS_TOKEN:-}" ]]; then
|
|
505
|
+
# Try to get access token from service account
|
|
506
|
+
if [[ -n "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]] && [[ -f "$GOOGLE_APPLICATION_CREDENTIALS" ]]; then
|
|
507
|
+
# Use gcloud or manual JWT flow
|
|
508
|
+
local token
|
|
509
|
+
token=$(gcloud auth application-default print-access-token 2>/dev/null || echo "")
|
|
510
|
+
if [[ -z "$token" ]]; then
|
|
511
|
+
print_error "Failed to get GSC access token. Run: gcloud auth application-default login"
|
|
512
|
+
return 1
|
|
513
|
+
fi
|
|
514
|
+
GSC_ACCESS_TOKEN="$token"
|
|
515
|
+
else
|
|
516
|
+
print_error "GSC credentials not configured. Set GOOGLE_APPLICATION_CREDENTIALS or GSC_ACCESS_TOKEN"
|
|
517
|
+
return 1
|
|
518
|
+
fi
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
curl -s -X POST \
|
|
522
|
+
"https://searchconsole.googleapis.com/webmasters/v3/$endpoint" \
|
|
523
|
+
-H "Authorization: Bearer $GSC_ACCESS_TOKEN" \
|
|
524
|
+
-H "Content-Type: application/json" \
|
|
525
|
+
-d "$data"
|
|
526
|
+
return 0
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Get search analytics (queries, pages, clicks, impressions, CTR, position)
|
|
530
|
+
gsc_search_analytics() {
|
|
531
|
+
local site_url="$1"
|
|
532
|
+
local start_date="$2"
|
|
533
|
+
local end_date="$3"
|
|
534
|
+
local limit="${4:-1000}"
|
|
535
|
+
local dimensions="${5:-query}" # query, page, country, device, searchAppearance
|
|
536
|
+
|
|
537
|
+
# URL encode the site URL
|
|
538
|
+
local encoded_url
|
|
539
|
+
encoded_url=$(echo -n "$site_url" | jq -sRr @uri)
|
|
540
|
+
|
|
541
|
+
local data
|
|
542
|
+
data=$(cat << EOF
|
|
543
|
+
{
|
|
544
|
+
"startDate": "$start_date",
|
|
545
|
+
"endDate": "$end_date",
|
|
546
|
+
"dimensions": ["$dimensions"],
|
|
547
|
+
"rowLimit": $limit,
|
|
548
|
+
"startRow": 0
|
|
549
|
+
}
|
|
550
|
+
EOF
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
gsc_request "sites/$encoded_url/searchAnalytics/query" "$data"
|
|
554
|
+
return 0
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
# Get top queries for a site
|
|
558
|
+
gsc_top_queries() {
|
|
559
|
+
local site_url="$1"
|
|
560
|
+
local days="${2:-30}"
|
|
561
|
+
local limit="${3:-100}"
|
|
562
|
+
|
|
563
|
+
local end_date
|
|
564
|
+
local start_date
|
|
565
|
+
end_date=$(date +%Y-%m-%d)
|
|
566
|
+
start_date=$(date -v-${days}d +%Y-%m-%d 2>/dev/null || date -d "$days days ago" +%Y-%m-%d)
|
|
567
|
+
|
|
568
|
+
gsc_search_analytics "$site_url" "$start_date" "$end_date" "$limit" "query"
|
|
569
|
+
return 0
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# Get queries for a specific page
|
|
573
|
+
gsc_page_queries() {
|
|
574
|
+
local site_url="$1"
|
|
575
|
+
local page_url="$2"
|
|
576
|
+
local days="${3:-30}"
|
|
577
|
+
local limit="${4:-100}"
|
|
578
|
+
|
|
579
|
+
local end_date
|
|
580
|
+
local start_date
|
|
581
|
+
end_date=$(date +%Y-%m-%d)
|
|
582
|
+
start_date=$(date -v-${days}d +%Y-%m-%d 2>/dev/null || date -d "$days days ago" +%Y-%m-%d)
|
|
583
|
+
|
|
584
|
+
local encoded_site
|
|
585
|
+
encoded_site=$(echo -n "$site_url" | jq -sRr @uri)
|
|
586
|
+
|
|
587
|
+
local data
|
|
588
|
+
data=$(cat << EOF
|
|
589
|
+
{
|
|
590
|
+
"startDate": "$start_date",
|
|
591
|
+
"endDate": "$end_date",
|
|
592
|
+
"dimensions": ["query"],
|
|
593
|
+
"dimensionFilterGroups": [{
|
|
594
|
+
"filters": [{
|
|
595
|
+
"dimension": "page",
|
|
596
|
+
"operator": "equals",
|
|
597
|
+
"expression": "$page_url"
|
|
598
|
+
}]
|
|
599
|
+
}],
|
|
600
|
+
"rowLimit": $limit
|
|
601
|
+
}
|
|
602
|
+
EOF
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
gsc_request "sites/$encoded_site/searchAnalytics/query" "$data"
|
|
606
|
+
return 0
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
# List verified sites
|
|
610
|
+
gsc_list_sites() {
|
|
611
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
612
|
+
|
|
613
|
+
if [[ -z "${GSC_ACCESS_TOKEN:-}" ]]; then
|
|
614
|
+
if [[ -n "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]] && [[ -f "$GOOGLE_APPLICATION_CREDENTIALS" ]]; then
|
|
615
|
+
local token
|
|
616
|
+
token=$(gcloud auth application-default print-access-token 2>/dev/null || echo "")
|
|
617
|
+
if [[ -z "$token" ]]; then
|
|
618
|
+
print_error "Failed to get GSC access token"
|
|
619
|
+
return 1
|
|
620
|
+
fi
|
|
621
|
+
GSC_ACCESS_TOKEN="$token"
|
|
622
|
+
else
|
|
623
|
+
print_error "GSC credentials not configured"
|
|
624
|
+
return 1
|
|
625
|
+
fi
|
|
626
|
+
fi
|
|
627
|
+
|
|
628
|
+
curl -s -X GET \
|
|
629
|
+
"https://searchconsole.googleapis.com/webmasters/v3/sites" \
|
|
630
|
+
-H "Authorization: Bearer $GSC_ACCESS_TOKEN"
|
|
631
|
+
return 0
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# =============================================================================
|
|
635
|
+
# Bing Webmaster Tools API Functions
|
|
636
|
+
# =============================================================================
|
|
637
|
+
|
|
638
|
+
bing_request() {
|
|
639
|
+
local endpoint="$1"
|
|
640
|
+
local site_url="$2"
|
|
641
|
+
|
|
642
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
643
|
+
|
|
644
|
+
if [[ -z "${BING_WEBMASTER_API_KEY:-}" ]]; then
|
|
645
|
+
print_error "BING_WEBMASTER_API_KEY not configured in ~/.config/aidevops/mcp-env.sh"
|
|
646
|
+
return 1
|
|
647
|
+
fi
|
|
648
|
+
|
|
649
|
+
# URL encode the site URL
|
|
650
|
+
local encoded_url
|
|
651
|
+
encoded_url=$(echo -n "$site_url" | jq -sRr @uri)
|
|
652
|
+
|
|
653
|
+
curl -s -X GET \
|
|
654
|
+
"https://ssl.bing.com/webmaster/api.svc/json/$endpoint?siteUrl=$encoded_url&apikey=$BING_WEBMASTER_API_KEY"
|
|
655
|
+
return 0
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# Get query statistics (top queries with impressions/clicks)
|
|
659
|
+
bing_query_stats() {
|
|
660
|
+
local site_url="$1"
|
|
661
|
+
|
|
662
|
+
bing_request "GetQueryStats" "$site_url"
|
|
663
|
+
return 0
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
# Get keyword details for a specific query
|
|
667
|
+
bing_keyword() {
|
|
668
|
+
local site_url="$1"
|
|
669
|
+
local query="$2"
|
|
670
|
+
local start_date="$3"
|
|
671
|
+
local end_date="$4"
|
|
672
|
+
|
|
673
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
674
|
+
|
|
675
|
+
if [[ -z "${BING_WEBMASTER_API_KEY:-}" ]]; then
|
|
676
|
+
print_error "BING_WEBMASTER_API_KEY not configured"
|
|
677
|
+
return 1
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
local encoded_url
|
|
681
|
+
encoded_url=$(echo -n "$site_url" | jq -sRr @uri)
|
|
682
|
+
local encoded_query
|
|
683
|
+
encoded_query=$(echo -n "$query" | jq -sRr @uri)
|
|
684
|
+
|
|
685
|
+
curl -s -X GET \
|
|
686
|
+
"https://ssl.bing.com/webmaster/api.svc/json/GetKeyword?siteUrl=$encoded_url&query=$encoded_query&startDate=$start_date&endDate=$end_date&apikey=$BING_WEBMASTER_API_KEY"
|
|
687
|
+
return 0
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# Get related keywords
|
|
691
|
+
bing_related_keywords() {
|
|
692
|
+
local site_url="$1"
|
|
693
|
+
local query="$2"
|
|
694
|
+
local start_date="$3"
|
|
695
|
+
local end_date="$4"
|
|
696
|
+
|
|
697
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
698
|
+
|
|
699
|
+
if [[ -z "${BING_WEBMASTER_API_KEY:-}" ]]; then
|
|
700
|
+
print_error "BING_WEBMASTER_API_KEY not configured"
|
|
701
|
+
return 1
|
|
702
|
+
fi
|
|
703
|
+
|
|
704
|
+
local encoded_url
|
|
705
|
+
encoded_url=$(echo -n "$site_url" | jq -sRr @uri)
|
|
706
|
+
local encoded_query
|
|
707
|
+
encoded_query=$(echo -n "$query" | jq -sRr @uri)
|
|
708
|
+
|
|
709
|
+
curl -s -X GET \
|
|
710
|
+
"https://ssl.bing.com/webmaster/api.svc/json/GetRelatedKeywords?siteUrl=$encoded_url&query=$encoded_query&startDate=$start_date&endDate=$end_date&apikey=$BING_WEBMASTER_API_KEY"
|
|
711
|
+
return 0
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
# Get page query stats (queries for a specific page)
|
|
715
|
+
bing_page_query_stats() {
|
|
716
|
+
local site_url="$1"
|
|
717
|
+
local page_url="$2"
|
|
718
|
+
|
|
719
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
720
|
+
|
|
721
|
+
if [[ -z "${BING_WEBMASTER_API_KEY:-}" ]]; then
|
|
722
|
+
print_error "BING_WEBMASTER_API_KEY not configured"
|
|
723
|
+
return 1
|
|
724
|
+
fi
|
|
725
|
+
|
|
726
|
+
local encoded_site
|
|
727
|
+
encoded_site=$(echo -n "$site_url" | jq -sRr @uri)
|
|
728
|
+
local encoded_page
|
|
729
|
+
encoded_page=$(echo -n "$page_url" | jq -sRr @uri)
|
|
730
|
+
|
|
731
|
+
curl -s -X GET \
|
|
732
|
+
"https://ssl.bing.com/webmaster/api.svc/json/GetPageQueryStats?siteUrl=$encoded_site&page=$encoded_page&apikey=$BING_WEBMASTER_API_KEY"
|
|
733
|
+
return 0
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
# Get rank and traffic stats
|
|
737
|
+
bing_rank_traffic() {
|
|
738
|
+
local site_url="$1"
|
|
739
|
+
|
|
740
|
+
bing_request "GetRankAndTrafficStats" "$site_url"
|
|
741
|
+
return 0
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
# List user sites
|
|
745
|
+
bing_list_sites() {
|
|
746
|
+
source "$HOME/.config/aidevops/mcp-env.sh" 2>/dev/null || true
|
|
747
|
+
|
|
748
|
+
if [[ -z "${BING_WEBMASTER_API_KEY:-}" ]]; then
|
|
749
|
+
print_error "BING_WEBMASTER_API_KEY not configured"
|
|
750
|
+
return 1
|
|
751
|
+
fi
|
|
752
|
+
|
|
753
|
+
curl -s -X GET \
|
|
754
|
+
"https://ssl.bing.com/webmaster/api.svc/json/GetUserSites?apikey=$BING_WEBMASTER_API_KEY"
|
|
755
|
+
return 0
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
# =============================================================================
|
|
759
|
+
# Webmaster Tools Research (GSC + Bing combined)
|
|
760
|
+
# =============================================================================
|
|
761
|
+
|
|
762
|
+
do_webmaster_research() {
|
|
763
|
+
local site_url="$1"
|
|
764
|
+
local days="${2:-30}"
|
|
765
|
+
local limit="${3:-100}"
|
|
766
|
+
local csv_export="${4:-false}"
|
|
767
|
+
local enrich="${5:-true}"
|
|
768
|
+
|
|
769
|
+
print_header "Webmaster Tools Keyword Research"
|
|
770
|
+
print_info "Site: $site_url"
|
|
771
|
+
print_info "Period: Last $days days"
|
|
772
|
+
print_info "Enrichment: $enrich"
|
|
773
|
+
|
|
774
|
+
local all_keywords=()
|
|
775
|
+
local gsc_data=""
|
|
776
|
+
local bing_data=""
|
|
777
|
+
|
|
778
|
+
# Fetch from Google Search Console
|
|
779
|
+
print_info "Fetching from Google Search Console..."
|
|
780
|
+
gsc_data=$(gsc_top_queries "$site_url" "$days" "$limit" 2>/dev/null || echo "")
|
|
781
|
+
|
|
782
|
+
if [[ -n "$gsc_data" ]] && echo "$gsc_data" | jq -e '.rows' >/dev/null 2>&1; then
|
|
783
|
+
local gsc_count
|
|
784
|
+
gsc_count=$(echo "$gsc_data" | jq '.rows | length')
|
|
785
|
+
print_success "GSC: Found $gsc_count queries"
|
|
786
|
+
else
|
|
787
|
+
print_warning "GSC: No data or not configured"
|
|
788
|
+
gsc_data=""
|
|
789
|
+
fi
|
|
790
|
+
|
|
791
|
+
# Fetch from Bing Webmaster Tools
|
|
792
|
+
print_info "Fetching from Bing Webmaster Tools..."
|
|
793
|
+
bing_data=$(bing_query_stats "$site_url" 2>/dev/null || echo "")
|
|
794
|
+
|
|
795
|
+
if [[ -n "$bing_data" ]] && echo "$bing_data" | jq -e '.d' >/dev/null 2>&1; then
|
|
796
|
+
local bing_count
|
|
797
|
+
bing_count=$(echo "$bing_data" | jq '.d | length')
|
|
798
|
+
print_success "Bing: Found $bing_count queries"
|
|
799
|
+
else
|
|
800
|
+
print_warning "Bing: No data or not configured"
|
|
801
|
+
bing_data=""
|
|
802
|
+
fi
|
|
803
|
+
|
|
804
|
+
# Combine and deduplicate keywords
|
|
805
|
+
local combined_keywords
|
|
806
|
+
combined_keywords=$(mktemp)
|
|
807
|
+
|
|
808
|
+
# Process GSC data
|
|
809
|
+
if [[ -n "$gsc_data" ]]; then
|
|
810
|
+
echo "$gsc_data" | jq -r '.rows[]? | [.keys[0], .clicks, .impressions, .ctr, .position, "gsc"] | @tsv' >> "$combined_keywords"
|
|
811
|
+
fi
|
|
812
|
+
|
|
813
|
+
# Process Bing data
|
|
814
|
+
if [[ -n "$bing_data" ]]; then
|
|
815
|
+
echo "$bing_data" | jq -r '.d[]? | [.Query, .Clicks, .Impressions, (.Clicks / (.Impressions + 0.001)), .AvgPosition, "bing"] | @tsv' >> "$combined_keywords"
|
|
816
|
+
fi
|
|
817
|
+
|
|
818
|
+
# Aggregate by keyword (combine GSC + Bing data)
|
|
819
|
+
local aggregated
|
|
820
|
+
aggregated=$(sort -t$'\t' -k1,1 "$combined_keywords" | awk -F'\t' '
|
|
821
|
+
{
|
|
822
|
+
kw = $1
|
|
823
|
+
clicks[kw] += $2
|
|
824
|
+
impressions[kw] += $3
|
|
825
|
+
ctr_sum[kw] += $4
|
|
826
|
+
pos_sum[kw] += $5
|
|
827
|
+
count[kw]++
|
|
828
|
+
if ($6 == "gsc") gsc[kw] = 1
|
|
829
|
+
if ($6 == "bing") bing[kw] = 1
|
|
830
|
+
}
|
|
831
|
+
END {
|
|
832
|
+
for (kw in clicks) {
|
|
833
|
+
sources = ""
|
|
834
|
+
if (gsc[kw]) sources = "GSC"
|
|
835
|
+
if (bing[kw]) sources = sources (sources ? "+" : "") "Bing"
|
|
836
|
+
printf "%s\t%d\t%d\t%.4f\t%.1f\t%s\n", kw, clicks[kw], impressions[kw], ctr_sum[kw]/count[kw], pos_sum[kw]/count[kw], sources
|
|
837
|
+
}
|
|
838
|
+
}' | sort -t$'\t' -k3 -rn | head -n "$limit")
|
|
839
|
+
|
|
840
|
+
rm -f "$combined_keywords"
|
|
841
|
+
|
|
842
|
+
if [[ -z "$aggregated" ]]; then
|
|
843
|
+
print_warning "No keyword data found from webmaster tools"
|
|
844
|
+
return 0
|
|
845
|
+
fi
|
|
846
|
+
|
|
847
|
+
# Enrich with DataForSEO volume/difficulty data if requested
|
|
848
|
+
local enriched_data="[]"
|
|
849
|
+
if [[ "$enrich" == "true" ]]; then
|
|
850
|
+
print_info "Enriching with search volume and difficulty data..."
|
|
851
|
+
|
|
852
|
+
# Get unique keywords for enrichment (top 50 to avoid API limits)
|
|
853
|
+
local keywords_to_enrich
|
|
854
|
+
keywords_to_enrich=$(echo "$aggregated" | head -50 | cut -f1 | tr '\n' ',' | sed 's/,$//')
|
|
855
|
+
|
|
856
|
+
if [[ -n "$keywords_to_enrich" ]]; then
|
|
857
|
+
local location_code
|
|
858
|
+
local language_code
|
|
859
|
+
location_code=$(get_location_code "$DEFAULT_LOCALE")
|
|
860
|
+
language_code=$(get_language_code "$DEFAULT_LOCALE")
|
|
861
|
+
|
|
862
|
+
# Fetch volume data from DataForSEO
|
|
863
|
+
local volume_data
|
|
864
|
+
volume_data=$(dataforseo_keyword_suggestions "${keywords_to_enrich%%,*}" "$location_code" "$language_code" 50 2>/dev/null || echo "")
|
|
865
|
+
|
|
866
|
+
# Build lookup table for volume/difficulty
|
|
867
|
+
local volume_lookup
|
|
868
|
+
volume_lookup=$(echo "$volume_data" | jq -r '
|
|
869
|
+
[.tasks[]?.result[]?.items[]? | {
|
|
870
|
+
keyword: .keyword,
|
|
871
|
+
volume: (.keyword_info.search_volume // 0),
|
|
872
|
+
difficulty: (.keyword_properties.keyword_difficulty // 0),
|
|
873
|
+
cpc: (.keyword_info.cpc // 0),
|
|
874
|
+
intent: (.search_intent_info.main_intent // "unknown")
|
|
875
|
+
}] | INDEX(.keyword)
|
|
876
|
+
' 2>/dev/null || echo "{}")
|
|
877
|
+
fi
|
|
878
|
+
fi
|
|
879
|
+
|
|
880
|
+
# Format output
|
|
881
|
+
echo ""
|
|
882
|
+
printf "| %-40s | %10s | %12s | %6s | %8s | %8s | %6s | %8s | %-10s |\n" \
|
|
883
|
+
"Keyword" "Clicks" "Impressions" "CTR" "Position" "Volume" "KD" "CPC" "Sources"
|
|
884
|
+
printf "|%-42s|%12s|%14s|%8s|%10s|%10s|%8s|%10s|%-12s|\n" \
|
|
885
|
+
"$(printf '%0.s-' {1..42})" "$(printf '%0.s-' {1..12})" "$(printf '%0.s-' {1..14})" \
|
|
886
|
+
"$(printf '%0.s-' {1..8})" "$(printf '%0.s-' {1..10})" "$(printf '%0.s-' {1..10})" \
|
|
887
|
+
"$(printf '%0.s-' {1..8})" "$(printf '%0.s-' {1..10})" "$(printf '%0.s-' {1..12})"
|
|
888
|
+
|
|
889
|
+
local count=0
|
|
890
|
+
while IFS=$'\t' read -r keyword clicks impressions ctr position sources; do
|
|
891
|
+
# Get enrichment data if available
|
|
892
|
+
local volume="-"
|
|
893
|
+
local kd="-"
|
|
894
|
+
local cpc="-"
|
|
895
|
+
|
|
896
|
+
if [[ -n "${volume_lookup:-}" ]]; then
|
|
897
|
+
local enriched
|
|
898
|
+
enriched=$(echo "$volume_lookup" | jq -r --arg kw "$keyword" '.[$kw] // empty')
|
|
899
|
+
if [[ -n "$enriched" ]]; then
|
|
900
|
+
volume=$(echo "$enriched" | jq -r '.volume // "-"')
|
|
901
|
+
kd=$(echo "$enriched" | jq -r '.difficulty // "-"')
|
|
902
|
+
cpc=$(echo "$enriched" | jq -r '.cpc // "-"')
|
|
903
|
+
fi
|
|
904
|
+
fi
|
|
905
|
+
|
|
906
|
+
# Format CTR as percentage
|
|
907
|
+
local ctr_pct
|
|
908
|
+
ctr_pct=$(echo "scale=2; $ctr * 100" | bc 2>/dev/null || echo "$ctr")
|
|
909
|
+
|
|
910
|
+
printf "| %-40s | %10s | %12s | %5s%% | %8.1f | %8s | %6s | %8s | %-10s |\n" \
|
|
911
|
+
"${keyword:0:40}" "$clicks" "$impressions" "$ctr_pct" "$position" "$volume" "$kd" "$cpc" "$sources"
|
|
912
|
+
|
|
913
|
+
count=$((count + 1))
|
|
914
|
+
done <<< "$aggregated"
|
|
915
|
+
|
|
916
|
+
echo ""
|
|
917
|
+
print_success "Found $count keywords from webmaster tools"
|
|
918
|
+
|
|
919
|
+
# CSV export
|
|
920
|
+
if [[ "$csv_export" == "true" ]]; then
|
|
921
|
+
local csv_file="$DOWNLOADS_DIR/webmaster-keywords-$(date +%Y%m%d-%H%M%S).csv"
|
|
922
|
+
echo "Keyword,Clicks,Impressions,CTR,Position,Volume,KD,CPC,Sources" > "$csv_file"
|
|
923
|
+
echo "$aggregated" | while IFS=$'\t' read -r keyword clicks impressions ctr position sources; do
|
|
924
|
+
echo "\"$keyword\",$clicks,$impressions,$ctr,$position,,,\"$sources\"" >> "$csv_file"
|
|
925
|
+
done
|
|
926
|
+
print_success "Exported to: $csv_file"
|
|
927
|
+
fi
|
|
928
|
+
|
|
929
|
+
return 0
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
# List all verified sites from both GSC and Bing
|
|
933
|
+
do_list_sites() {
|
|
934
|
+
print_header "Verified Webmaster Sites"
|
|
935
|
+
|
|
936
|
+
echo ""
|
|
937
|
+
echo "Google Search Console:"
|
|
938
|
+
echo "----------------------"
|
|
939
|
+
local gsc_sites
|
|
940
|
+
gsc_sites=$(gsc_list_sites 2>/dev/null || echo "")
|
|
941
|
+
if [[ -n "$gsc_sites" ]] && echo "$gsc_sites" | jq -e '.siteEntry' >/dev/null 2>&1; then
|
|
942
|
+
echo "$gsc_sites" | jq -r '.siteEntry[]? | " \(.siteUrl) [\(.permissionLevel)]"'
|
|
943
|
+
else
|
|
944
|
+
echo " (Not configured or no sites)"
|
|
945
|
+
fi
|
|
946
|
+
|
|
947
|
+
echo ""
|
|
948
|
+
echo "Bing Webmaster Tools:"
|
|
949
|
+
echo "---------------------"
|
|
950
|
+
local bing_sites
|
|
951
|
+
bing_sites=$(bing_list_sites 2>/dev/null || echo "")
|
|
952
|
+
if [[ -n "$bing_sites" ]] && echo "$bing_sites" | jq -e '.d' >/dev/null 2>&1; then
|
|
953
|
+
echo "$bing_sites" | jq -r '.d[]? | " \(.Url)"'
|
|
954
|
+
else
|
|
955
|
+
echo " (Not configured or no sites)"
|
|
956
|
+
fi
|
|
957
|
+
|
|
958
|
+
echo ""
|
|
959
|
+
return 0
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
# =============================================================================
|
|
963
|
+
# SERP Weakness Detection
|
|
964
|
+
# =============================================================================
|
|
965
|
+
|
|
966
|
+
detect_weaknesses() {
|
|
967
|
+
local serp_data="$1"
|
|
968
|
+
local weaknesses=()
|
|
969
|
+
local weakness_count=0
|
|
970
|
+
|
|
971
|
+
# Parse SERP results and detect weaknesses
|
|
972
|
+
# This is a simplified version - full implementation would analyze each result
|
|
973
|
+
|
|
974
|
+
# Check for low domain scores
|
|
975
|
+
local low_ds_count
|
|
976
|
+
low_ds_count=$(echo "$serp_data" | jq "[.items[]? | select(.main_domain_rank <= $THRESHOLD_LOW_DS)] | length" 2>/dev/null || echo "0")
|
|
977
|
+
if [[ "$low_ds_count" -gt 0 ]]; then
|
|
978
|
+
weaknesses+=("Low DS ($low_ds_count)")
|
|
979
|
+
weakness_count=$((weakness_count + low_ds_count))
|
|
980
|
+
fi
|
|
981
|
+
|
|
982
|
+
# Check for no backlinks
|
|
983
|
+
local no_backlinks_count
|
|
984
|
+
no_backlinks_count=$(echo "$serp_data" | jq '[.items[]? | select(.backlinks_count == 0)] | length' 2>/dev/null || echo "0")
|
|
985
|
+
if [[ "$no_backlinks_count" -gt 0 ]]; then
|
|
986
|
+
weaknesses+=("No Backlinks ($no_backlinks_count)")
|
|
987
|
+
weakness_count=$((weakness_count + no_backlinks_count))
|
|
988
|
+
fi
|
|
989
|
+
|
|
990
|
+
# Check for non-HTTPS
|
|
991
|
+
# SONAR: Detecting insecure URLs for security audit, not using them
|
|
992
|
+
local non_https_count
|
|
993
|
+
non_https_count=$(echo "$serp_data" | jq '[.items[]? | select(.url | startswith("http://"))] | length' 2>/dev/null || echo "0")
|
|
994
|
+
if [[ "$non_https_count" -gt 0 ]]; then
|
|
995
|
+
weaknesses+=("Non-HTTPS ($non_https_count)")
|
|
996
|
+
weakness_count=$((weakness_count + non_https_count))
|
|
997
|
+
fi
|
|
998
|
+
|
|
999
|
+
# Check for UGC-heavy results
|
|
1000
|
+
local ugc_count
|
|
1001
|
+
ugc_count=$(echo "$serp_data" | jq '[.items[]? | select(.domain | test("reddit|quora|stackoverflow|forum"; "i"))] | length' 2>/dev/null || echo "0")
|
|
1002
|
+
if [[ "$ugc_count" -ge "$THRESHOLD_UGC_HEAVY" ]]; then
|
|
1003
|
+
weaknesses+=("UGC-Heavy ($ugc_count)")
|
|
1004
|
+
weakness_count=$((weakness_count + 1))
|
|
1005
|
+
fi
|
|
1006
|
+
|
|
1007
|
+
# Output results
|
|
1008
|
+
echo "$weakness_count|${weaknesses[*]:-None}"
|
|
1009
|
+
return 0
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
calculate_keyword_score() {
|
|
1013
|
+
local weakness_count="$1"
|
|
1014
|
+
local volume="$2"
|
|
1015
|
+
local difficulty="$3"
|
|
1016
|
+
local serp_features="$4"
|
|
1017
|
+
|
|
1018
|
+
local score=0
|
|
1019
|
+
|
|
1020
|
+
# Base score from weaknesses (1 point each, max 13)
|
|
1021
|
+
score=$((score + weakness_count))
|
|
1022
|
+
|
|
1023
|
+
# Volume bonus
|
|
1024
|
+
if [[ "$volume" -gt 5000 ]]; then
|
|
1025
|
+
score=$((score + 3))
|
|
1026
|
+
elif [[ "$volume" -gt 1000 ]]; then
|
|
1027
|
+
score=$((score + 2))
|
|
1028
|
+
elif [[ "$volume" -gt 100 ]]; then
|
|
1029
|
+
score=$((score + 1))
|
|
1030
|
+
fi
|
|
1031
|
+
|
|
1032
|
+
# Difficulty bonus
|
|
1033
|
+
if [[ "$difficulty" -eq 0 ]]; then
|
|
1034
|
+
score=$((score + 3))
|
|
1035
|
+
elif [[ "$difficulty" -le 15 ]]; then
|
|
1036
|
+
score=$((score + 2))
|
|
1037
|
+
elif [[ "$difficulty" -le 30 ]]; then
|
|
1038
|
+
score=$((score + 1))
|
|
1039
|
+
fi
|
|
1040
|
+
|
|
1041
|
+
# SERP features penalty (max -3)
|
|
1042
|
+
local feature_penalty
|
|
1043
|
+
feature_penalty=$(echo "$serp_features" | jq 'length' 2>/dev/null || echo "0")
|
|
1044
|
+
if [[ "$feature_penalty" -gt 3 ]]; then
|
|
1045
|
+
feature_penalty=3
|
|
1046
|
+
fi
|
|
1047
|
+
score=$((score - feature_penalty))
|
|
1048
|
+
|
|
1049
|
+
# Normalize to 0-100 scale (exponential scaling)
|
|
1050
|
+
# Max raw score ~20, scale to 100
|
|
1051
|
+
local normalized
|
|
1052
|
+
normalized=$(echo "scale=0; ($score * 5)" | bc)
|
|
1053
|
+
if [[ "$normalized" -gt 100 ]]; then
|
|
1054
|
+
normalized=100
|
|
1055
|
+
fi
|
|
1056
|
+
if [[ "$normalized" -lt 0 ]]; then
|
|
1057
|
+
normalized=0
|
|
1058
|
+
fi
|
|
1059
|
+
|
|
1060
|
+
echo "$normalized"
|
|
1061
|
+
return 0
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
# =============================================================================
|
|
1065
|
+
# Output Formatting
|
|
1066
|
+
# =============================================================================
|
|
1067
|
+
|
|
1068
|
+
format_volume() {
|
|
1069
|
+
local volume="$1"
|
|
1070
|
+
|
|
1071
|
+
if [[ "$volume" -ge 1000000 ]]; then
|
|
1072
|
+
echo "$(echo "scale=1; $volume / 1000000" | bc)M"
|
|
1073
|
+
elif [[ "$volume" -ge 1000 ]]; then
|
|
1074
|
+
echo "$(echo "scale=1; $volume / 1000" | bc)K"
|
|
1075
|
+
else
|
|
1076
|
+
echo "$volume"
|
|
1077
|
+
fi
|
|
1078
|
+
return 0
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
format_cpc() {
|
|
1082
|
+
local cpc="$1"
|
|
1083
|
+
printf "\$%.2f" "$cpc"
|
|
1084
|
+
return 0
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
# Print markdown table with space-padded columns
|
|
1088
|
+
print_research_table() {
|
|
1089
|
+
local json_data="$1"
|
|
1090
|
+
local mode="$2"
|
|
1091
|
+
|
|
1092
|
+
case "$mode" in
|
|
1093
|
+
"basic")
|
|
1094
|
+
echo ""
|
|
1095
|
+
printf "| %-40s | %8s | %7s | %4s | %-14s |\n" "Keyword" "Volume" "CPC" "KD" "Intent"
|
|
1096
|
+
printf "|%-42s|%10s|%9s|%6s|%16s|\n" "$(printf '%0.s-' {1..42})" "$(printf '%0.s-' {1..10})" "$(printf '%0.s-' {1..9})" "$(printf '%0.s-' {1..6})" "$(printf '%0.s-' {1..16})"
|
|
1097
|
+
|
|
1098
|
+
echo "$json_data" | jq -r '.[] | "\(.keyword)|\(.volume)|\(.cpc)|\(.difficulty)|\(.intent)"' 2>/dev/null | while IFS='|' read -r kw vol cpc kd intent; do
|
|
1099
|
+
local vol_fmt
|
|
1100
|
+
vol_fmt=$(format_volume "$vol")
|
|
1101
|
+
local cpc_fmt
|
|
1102
|
+
cpc_fmt=$(format_cpc "$cpc")
|
|
1103
|
+
printf "| %-40s | %8s | %7s | %4s | %-14s |\n" "${kw:0:40}" "$vol_fmt" "$cpc_fmt" "$kd" "${intent:0:14}"
|
|
1104
|
+
done
|
|
1105
|
+
;;
|
|
1106
|
+
"extended")
|
|
1107
|
+
echo ""
|
|
1108
|
+
printf "| %-30s | %7s | %4s | %4s | %10s | %-30s | %4s | %4s |\n" "Keyword" "Vol" "KD" "KS" "Weaknesses" "Weakness Types" "DS" "PS"
|
|
1109
|
+
printf "|%-32s|%9s|%6s|%6s|%12s|%32s|%6s|%6s|\n" "$(printf '%0.s-' {1..32})" "$(printf '%0.s-' {1..9})" "$(printf '%0.s-' {1..6})" "$(printf '%0.s-' {1..6})" "$(printf '%0.s-' {1..12})" "$(printf '%0.s-' {1..32})" "$(printf '%0.s-' {1..6})" "$(printf '%0.s-' {1..6})"
|
|
1110
|
+
|
|
1111
|
+
echo "$json_data" | jq -r '.[] | "\(.keyword)|\(.volume)|\(.difficulty)|\(.keyword_score)|\(.weakness_count)|\(.weaknesses)|\(.domain_score)|\(.page_score)"' 2>/dev/null | while IFS='|' read -r kw vol kd ks wc wt ds ps; do
|
|
1112
|
+
local vol_fmt
|
|
1113
|
+
vol_fmt=$(format_volume "$vol")
|
|
1114
|
+
printf "| %-30s | %7s | %4s | %4s | %10s | %-30s | %4s | %4s |\n" "${kw:0:30}" "$vol_fmt" "$kd" "$ks" "$wc" "${wt:0:30}" "$ds" "$ps"
|
|
1115
|
+
done
|
|
1116
|
+
;;
|
|
1117
|
+
"competitor")
|
|
1118
|
+
echo ""
|
|
1119
|
+
printf "| %-30s | %7s | %4s | %8s | %11s | %-35s |\n" "Keyword" "Vol" "KD" "Position" "Est Traffic" "Ranking URL"
|
|
1120
|
+
printf "|%-32s|%9s|%6s|%10s|%13s|%37s|\n" "$(printf '%0.s-' {1..32})" "$(printf '%0.s-' {1..9})" "$(printf '%0.s-' {1..6})" "$(printf '%0.s-' {1..10})" "$(printf '%0.s-' {1..13})" "$(printf '%0.s-' {1..37})"
|
|
1121
|
+
|
|
1122
|
+
echo "$json_data" | jq -r '.[] | "\(.keyword)|\(.volume)|\(.difficulty)|\(.position)|\(.est_traffic)|\(.ranking_url)"' 2>/dev/null | while IFS='|' read -r kw vol kd pos traffic url; do
|
|
1123
|
+
local vol_fmt
|
|
1124
|
+
vol_fmt=$(format_volume "$vol")
|
|
1125
|
+
printf "| %-30s | %7s | %4s | %8s | %11s | %-35s |\n" "${kw:0:30}" "$vol_fmt" "$kd" "$pos" "$traffic" "${url:0:35}"
|
|
1126
|
+
done
|
|
1127
|
+
;;
|
|
1128
|
+
*)
|
|
1129
|
+
print_error "Unknown mode: $mode"
|
|
1130
|
+
;;
|
|
1131
|
+
esac
|
|
1132
|
+
echo ""
|
|
1133
|
+
return 0
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
# =============================================================================
|
|
1137
|
+
# CSV Export
|
|
1138
|
+
# =============================================================================
|
|
1139
|
+
|
|
1140
|
+
export_csv() {
|
|
1141
|
+
local json_data="$1"
|
|
1142
|
+
local mode="$2"
|
|
1143
|
+
local filename="$3"
|
|
1144
|
+
|
|
1145
|
+
local filepath="$DOWNLOADS_DIR/$filename"
|
|
1146
|
+
|
|
1147
|
+
case "$mode" in
|
|
1148
|
+
"basic")
|
|
1149
|
+
echo "Keyword,Volume,CPC,Difficulty,Intent" > "$filepath"
|
|
1150
|
+
echo "$json_data" | jq -r '.[] | "\"\(.keyword)\",\(.volume),\(.cpc),\(.difficulty),\"\(.intent)\""' >> "$filepath"
|
|
1151
|
+
;;
|
|
1152
|
+
"extended")
|
|
1153
|
+
echo "Keyword,Volume,CPC,Difficulty,Intent,KeywordScore,DomainScore,PageScore,WeaknessCount,Weaknesses" > "$filepath"
|
|
1154
|
+
echo "$json_data" | jq -r '.[] | "\"\(.keyword)\",\(.volume),\(.cpc),\(.difficulty),\"\(.intent)\",\(.keyword_score),\(.domain_score),\(.page_score),\(.weakness_count),\"\(.weaknesses)\""' >> "$filepath"
|
|
1155
|
+
;;
|
|
1156
|
+
"competitor")
|
|
1157
|
+
echo "Keyword,Volume,CPC,Difficulty,Intent,Position,EstTraffic,RankingURL" > "$filepath"
|
|
1158
|
+
echo "$json_data" | jq -r '.[] | "\"\(.keyword)\",\(.volume),\(.cpc),\(.difficulty),\"\(.intent)\",\(.position),\(.est_traffic),\"\(.ranking_url)\""' >> "$filepath"
|
|
1159
|
+
;;
|
|
1160
|
+
*)
|
|
1161
|
+
print_error "Unknown export mode: $mode"
|
|
1162
|
+
return 1
|
|
1163
|
+
;;
|
|
1164
|
+
esac
|
|
1165
|
+
|
|
1166
|
+
print_success "Exported to: $filepath"
|
|
1167
|
+
return 0
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
# =============================================================================
|
|
1171
|
+
# Main Research Functions
|
|
1172
|
+
# =============================================================================
|
|
1173
|
+
|
|
1174
|
+
do_keyword_research() {
|
|
1175
|
+
local keywords="$1"
|
|
1176
|
+
local provider="$2"
|
|
1177
|
+
local locale="$3"
|
|
1178
|
+
local limit="$4"
|
|
1179
|
+
local csv_export="$5"
|
|
1180
|
+
local filters="$6"
|
|
1181
|
+
|
|
1182
|
+
print_header "Keyword Research"
|
|
1183
|
+
print_info "Keywords: $keywords"
|
|
1184
|
+
print_info "Provider: $provider"
|
|
1185
|
+
print_info "Locale: $locale"
|
|
1186
|
+
print_info "Limit: $limit"
|
|
1187
|
+
|
|
1188
|
+
check_credentials "$provider" || return 1
|
|
1189
|
+
|
|
1190
|
+
local location_code
|
|
1191
|
+
location_code=$(get_location_code "$locale")
|
|
1192
|
+
local language_code
|
|
1193
|
+
language_code=$(get_language_code "$locale")
|
|
1194
|
+
|
|
1195
|
+
local results="[]"
|
|
1196
|
+
|
|
1197
|
+
# Split keywords by comma and process each
|
|
1198
|
+
IFS=',' read -ra keyword_array <<< "$keywords"
|
|
1199
|
+
|
|
1200
|
+
for keyword in "${keyword_array[@]}"; do
|
|
1201
|
+
keyword=$(echo "$keyword" | xargs) # Trim whitespace
|
|
1202
|
+
print_info "Researching: $keyword"
|
|
1203
|
+
|
|
1204
|
+
if [[ "$provider" == "dataforseo" ]] || [[ "$provider" == "both" ]]; then
|
|
1205
|
+
local response
|
|
1206
|
+
response=$(dataforseo_keyword_suggestions "$keyword" "$location_code" "$language_code" "$limit")
|
|
1207
|
+
|
|
1208
|
+
# Parse and add to results
|
|
1209
|
+
local parsed
|
|
1210
|
+
parsed=$(echo "$response" | jq '[.tasks[0].result[0].items[]? | {
|
|
1211
|
+
keyword: .keyword,
|
|
1212
|
+
volume: (.keyword_info.search_volume // 0),
|
|
1213
|
+
cpc: (.keyword_info.cpc // 0),
|
|
1214
|
+
difficulty: (.keyword_info.keyword_difficulty // 0),
|
|
1215
|
+
intent: (.search_intent_info.main_intent // "unknown")
|
|
1216
|
+
}]' 2>/dev/null || echo "[]")
|
|
1217
|
+
|
|
1218
|
+
results=$(echo "$results $parsed" | jq -s 'add')
|
|
1219
|
+
fi
|
|
1220
|
+
|
|
1221
|
+
if [[ "$provider" == "serper" ]] || [[ "$provider" == "both" ]]; then
|
|
1222
|
+
# Serper doesn't have keyword suggestions, use search instead
|
|
1223
|
+
print_warning "Serper doesn't support keyword suggestions. Use DataForSEO for this feature."
|
|
1224
|
+
fi
|
|
1225
|
+
done
|
|
1226
|
+
|
|
1227
|
+
# Apply filters if provided
|
|
1228
|
+
if [[ -n "$filters" ]]; then
|
|
1229
|
+
results=$(apply_filters "$results" "$filters")
|
|
1230
|
+
fi
|
|
1231
|
+
|
|
1232
|
+
# Count results
|
|
1233
|
+
local count
|
|
1234
|
+
count=$(echo "$results" | jq 'length')
|
|
1235
|
+
print_success "Found $count keywords"
|
|
1236
|
+
|
|
1237
|
+
# Print table
|
|
1238
|
+
print_research_table "$results" "basic"
|
|
1239
|
+
|
|
1240
|
+
# Export CSV if requested
|
|
1241
|
+
if [[ "$csv_export" == "true" ]]; then
|
|
1242
|
+
local timestamp
|
|
1243
|
+
timestamp=$(date +"%Y%m%d-%H%M%S")
|
|
1244
|
+
export_csv "$results" "basic" "keyword-research-$timestamp.csv"
|
|
1245
|
+
fi
|
|
1246
|
+
|
|
1247
|
+
# Prompt for more results
|
|
1248
|
+
if [[ "$count" -ge "$limit" ]]; then
|
|
1249
|
+
echo ""
|
|
1250
|
+
read -p "Retrieved $count keywords. Need more? Enter number (max $MAX_LIMIT) or press Enter to continue: " more_count
|
|
1251
|
+
if [[ -n "$more_count" ]] && [[ "$more_count" =~ ^[0-9]+$ ]]; then
|
|
1252
|
+
if [[ "$more_count" -le "$MAX_LIMIT" ]]; then
|
|
1253
|
+
do_keyword_research "$keywords" "$provider" "$locale" "$more_count" "$csv_export" "$filters"
|
|
1254
|
+
else
|
|
1255
|
+
print_warning "Maximum limit is $MAX_LIMIT"
|
|
1256
|
+
fi
|
|
1257
|
+
fi
|
|
1258
|
+
fi
|
|
1259
|
+
|
|
1260
|
+
return 0
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
do_autocomplete_research() {
|
|
1264
|
+
local keyword="$1"
|
|
1265
|
+
local provider="$2"
|
|
1266
|
+
local locale="$3"
|
|
1267
|
+
local csv_export="$4"
|
|
1268
|
+
|
|
1269
|
+
print_header "Autocomplete Research"
|
|
1270
|
+
print_info "Keyword: $keyword"
|
|
1271
|
+
print_info "Provider: $provider"
|
|
1272
|
+
print_info "Locale: $locale"
|
|
1273
|
+
|
|
1274
|
+
check_credentials "$provider" || return 1
|
|
1275
|
+
|
|
1276
|
+
local location_code
|
|
1277
|
+
location_code=$(get_location_code "$locale")
|
|
1278
|
+
local language_code
|
|
1279
|
+
language_code=$(get_language_code "$locale")
|
|
1280
|
+
|
|
1281
|
+
local results="[]"
|
|
1282
|
+
|
|
1283
|
+
if [[ "$provider" == "dataforseo" ]] || [[ "$provider" == "both" ]]; then
|
|
1284
|
+
local response
|
|
1285
|
+
response=$(dataforseo_autocomplete "$keyword" "$location_code" "$language_code")
|
|
1286
|
+
|
|
1287
|
+
local parsed
|
|
1288
|
+
# Parse keyword_suggestions response format (same as keyword research)
|
|
1289
|
+
parsed=$(echo "$response" | jq '[.tasks[0].result[0].items[]? | {
|
|
1290
|
+
keyword: .keyword,
|
|
1291
|
+
volume: (.keyword_info.search_volume // 0),
|
|
1292
|
+
cpc: (.keyword_info.cpc // 0),
|
|
1293
|
+
difficulty: (.keyword_properties.keyword_difficulty // 0),
|
|
1294
|
+
intent: (.search_intent_info.main_intent // "unknown")
|
|
1295
|
+
}]' 2>/dev/null || echo "[]")
|
|
1296
|
+
|
|
1297
|
+
results=$(echo "$results $parsed" | jq -s 'add')
|
|
1298
|
+
fi
|
|
1299
|
+
|
|
1300
|
+
if [[ "$provider" == "serper" ]] || [[ "$provider" == "both" ]]; then
|
|
1301
|
+
local gl_code="${locale%-*}"
|
|
1302
|
+
local response
|
|
1303
|
+
response=$(serper_autocomplete "$keyword" "$gl_code")
|
|
1304
|
+
|
|
1305
|
+
local parsed
|
|
1306
|
+
# Serper returns suggestions[].value
|
|
1307
|
+
parsed=$(echo "$response" | jq '[.suggestions[]? | {
|
|
1308
|
+
keyword: .value,
|
|
1309
|
+
volume: 0,
|
|
1310
|
+
cpc: 0,
|
|
1311
|
+
difficulty: 0,
|
|
1312
|
+
intent: "unknown"
|
|
1313
|
+
}]' 2>/dev/null || echo "[]")
|
|
1314
|
+
|
|
1315
|
+
results=$(echo "$results $parsed" | jq -s 'add | unique_by(.keyword)')
|
|
1316
|
+
fi
|
|
1317
|
+
|
|
1318
|
+
local count
|
|
1319
|
+
count=$(echo "$results" | jq 'length')
|
|
1320
|
+
print_success "Found $count autocomplete suggestions"
|
|
1321
|
+
|
|
1322
|
+
print_research_table "$results" "basic"
|
|
1323
|
+
|
|
1324
|
+
if [[ "$csv_export" == "true" ]]; then
|
|
1325
|
+
local timestamp
|
|
1326
|
+
timestamp=$(date +"%Y%m%d-%H%M%S")
|
|
1327
|
+
export_csv "$results" "basic" "autocomplete-research-$timestamp.csv"
|
|
1328
|
+
fi
|
|
1329
|
+
|
|
1330
|
+
return 0
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
do_extended_research() {
|
|
1334
|
+
local keywords="$1"
|
|
1335
|
+
local provider="$2"
|
|
1336
|
+
local locale="$3"
|
|
1337
|
+
local limit="$4"
|
|
1338
|
+
local csv_export="$5"
|
|
1339
|
+
local quick_mode="$6"
|
|
1340
|
+
local include_ahrefs="$7"
|
|
1341
|
+
local mode="$8" # domain, competitor, gap, or empty for keyword
|
|
1342
|
+
local target="$9"
|
|
1343
|
+
|
|
1344
|
+
print_header "Extended Keyword Research"
|
|
1345
|
+
print_info "Mode: ${mode:-keyword}"
|
|
1346
|
+
print_info "Provider: $provider"
|
|
1347
|
+
print_info "Locale: $locale"
|
|
1348
|
+
print_info "Quick mode: $quick_mode"
|
|
1349
|
+
print_info "Include Ahrefs: $include_ahrefs"
|
|
1350
|
+
|
|
1351
|
+
check_credentials "$provider" || return 1
|
|
1352
|
+
|
|
1353
|
+
if [[ "$include_ahrefs" == "true" ]]; then
|
|
1354
|
+
check_credentials "ahrefs" || print_warning "Ahrefs credentials not found. Skipping DR/UR metrics."
|
|
1355
|
+
fi
|
|
1356
|
+
|
|
1357
|
+
local location_code
|
|
1358
|
+
location_code=$(get_location_code "$locale")
|
|
1359
|
+
local language_code
|
|
1360
|
+
language_code=$(get_language_code "$locale")
|
|
1361
|
+
|
|
1362
|
+
local results="[]"
|
|
1363
|
+
|
|
1364
|
+
case "$mode" in
|
|
1365
|
+
"domain")
|
|
1366
|
+
print_info "Domain research for: $target"
|
|
1367
|
+
# Use ranked keywords for domain research
|
|
1368
|
+
local response
|
|
1369
|
+
response=$(dataforseo_ranked_keywords "$target" "$location_code" "$language_code" "$limit")
|
|
1370
|
+
|
|
1371
|
+
results=$(echo "$response" | jq '[.tasks[0].result[0].items[]? | {
|
|
1372
|
+
keyword: .keyword_data.keyword,
|
|
1373
|
+
volume: (.keyword_data.keyword_info.search_volume // 0),
|
|
1374
|
+
cpc: (.keyword_data.keyword_info.cpc // 0),
|
|
1375
|
+
difficulty: (.keyword_data.keyword_info.keyword_difficulty // 0),
|
|
1376
|
+
intent: (.keyword_data.search_intent_info.main_intent // "unknown"),
|
|
1377
|
+
position: .ranked_serp_element.serp_item.rank_absolute,
|
|
1378
|
+
est_traffic: (.ranked_serp_element.serp_item.etv // 0),
|
|
1379
|
+
ranking_url: .ranked_serp_element.serp_item.url
|
|
1380
|
+
}]' 2>/dev/null || echo "[]")
|
|
1381
|
+
;;
|
|
1382
|
+
"competitor")
|
|
1383
|
+
print_info "Competitor research for: $target"
|
|
1384
|
+
local response
|
|
1385
|
+
response=$(dataforseo_ranked_keywords "$target" "$location_code" "$language_code" "$limit")
|
|
1386
|
+
|
|
1387
|
+
results=$(echo "$response" | jq '[.tasks[0].result[0].items[]? | {
|
|
1388
|
+
keyword: .keyword_data.keyword,
|
|
1389
|
+
volume: (.keyword_data.keyword_info.search_volume // 0),
|
|
1390
|
+
cpc: (.keyword_data.keyword_info.cpc // 0),
|
|
1391
|
+
difficulty: (.keyword_data.keyword_info.keyword_difficulty // 0),
|
|
1392
|
+
intent: (.keyword_data.search_intent_info.main_intent // "unknown"),
|
|
1393
|
+
position: .ranked_serp_element.serp_item.rank_absolute,
|
|
1394
|
+
est_traffic: (.ranked_serp_element.serp_item.etv // 0),
|
|
1395
|
+
ranking_url: .ranked_serp_element.serp_item.url
|
|
1396
|
+
}]' 2>/dev/null || echo "[]")
|
|
1397
|
+
;;
|
|
1398
|
+
"gap")
|
|
1399
|
+
IFS=',' read -ra domains <<< "$target"
|
|
1400
|
+
local your_domain="${domains[0]}"
|
|
1401
|
+
local competitor_domain="${domains[1]}"
|
|
1402
|
+
print_info "Keyword gap: $your_domain vs $competitor_domain"
|
|
1403
|
+
|
|
1404
|
+
local response
|
|
1405
|
+
response=$(dataforseo_keyword_gap "$your_domain" "$competitor_domain" "$location_code" "$language_code" "$limit")
|
|
1406
|
+
|
|
1407
|
+
results=$(echo "$response" | jq '[.tasks[0].result[0].items[]? | {
|
|
1408
|
+
keyword: .keyword_data.keyword,
|
|
1409
|
+
volume: (.keyword_data.keyword_info.search_volume // 0),
|
|
1410
|
+
cpc: (.keyword_data.keyword_info.cpc // 0),
|
|
1411
|
+
difficulty: (.keyword_data.keyword_info.keyword_difficulty // 0),
|
|
1412
|
+
intent: (.keyword_data.search_intent_info.main_intent // "unknown"),
|
|
1413
|
+
position: .first_domain_serp_element.serp_item.rank_absolute,
|
|
1414
|
+
est_traffic: (.first_domain_serp_element.serp_item.etv // 0),
|
|
1415
|
+
ranking_url: .first_domain_serp_element.serp_item.url
|
|
1416
|
+
}]' 2>/dev/null || echo "[]")
|
|
1417
|
+
;;
|
|
1418
|
+
*)
|
|
1419
|
+
# Standard keyword research with SERP analysis
|
|
1420
|
+
IFS=',' read -ra keyword_array <<< "$keywords"
|
|
1421
|
+
|
|
1422
|
+
# For quick mode, just use keyword suggestions directly
|
|
1423
|
+
if [[ "$quick_mode" == "true" ]]; then
|
|
1424
|
+
for keyword in "${keyword_array[@]}"; do
|
|
1425
|
+
keyword=$(echo "$keyword" | xargs)
|
|
1426
|
+
print_info "Researching: $keyword"
|
|
1427
|
+
|
|
1428
|
+
local suggestions
|
|
1429
|
+
suggestions=$(dataforseo_keyword_suggestions "$keyword" "$location_code" "$language_code" "$limit")
|
|
1430
|
+
|
|
1431
|
+
local parsed
|
|
1432
|
+
parsed=$(echo "$suggestions" | jq '[.tasks[0].result[0].items[]? | {
|
|
1433
|
+
keyword: .keyword,
|
|
1434
|
+
volume: (.keyword_info.search_volume // 0),
|
|
1435
|
+
cpc: (.keyword_info.cpc // 0),
|
|
1436
|
+
difficulty: (.keyword_properties.keyword_difficulty // 0),
|
|
1437
|
+
intent: (.search_intent_info.main_intent // "unknown"),
|
|
1438
|
+
keyword_score: 0,
|
|
1439
|
+
domain_score: 0,
|
|
1440
|
+
page_score: 0,
|
|
1441
|
+
weakness_count: 0,
|
|
1442
|
+
weaknesses: "N/A (quick mode)"
|
|
1443
|
+
}]' 2>/dev/null || echo "[]")
|
|
1444
|
+
|
|
1445
|
+
results=$(echo "$results $parsed" | jq -s 'add')
|
|
1446
|
+
done
|
|
1447
|
+
else
|
|
1448
|
+
# Full SERP analysis mode - process each keyword individually
|
|
1449
|
+
for keyword in "${keyword_array[@]}"; do
|
|
1450
|
+
keyword=$(echo "$keyword" | xargs)
|
|
1451
|
+
print_info "Analyzing SERP for: $keyword"
|
|
1452
|
+
|
|
1453
|
+
# Get keyword suggestions first
|
|
1454
|
+
local suggestions
|
|
1455
|
+
suggestions=$(dataforseo_keyword_suggestions "$keyword" "$location_code" "$language_code" "$limit")
|
|
1456
|
+
|
|
1457
|
+
# Get list of keywords
|
|
1458
|
+
local kw_list
|
|
1459
|
+
kw_list=$(echo "$suggestions" | jq -r '.tasks[0].result[0].items[]?.keyword' 2>/dev/null | head -n "$limit")
|
|
1460
|
+
|
|
1461
|
+
# Process each keyword
|
|
1462
|
+
while IFS= read -r kw; do
|
|
1463
|
+
if [[ -z "$kw" ]]; then
|
|
1464
|
+
continue
|
|
1465
|
+
fi
|
|
1466
|
+
|
|
1467
|
+
local kw_data
|
|
1468
|
+
kw_data=$(echo "$suggestions" | jq --arg k "$kw" '.tasks[0].result[0].items[] | select(.keyword == $k)' 2>/dev/null)
|
|
1469
|
+
|
|
1470
|
+
local volume
|
|
1471
|
+
volume=$(echo "$kw_data" | jq -r '.keyword_info.search_volume // 0')
|
|
1472
|
+
local cpc
|
|
1473
|
+
cpc=$(echo "$kw_data" | jq -r '.keyword_info.cpc // 0')
|
|
1474
|
+
local difficulty
|
|
1475
|
+
difficulty=$(echo "$kw_data" | jq -r '.keyword_properties.keyword_difficulty // 0')
|
|
1476
|
+
local intent
|
|
1477
|
+
intent=$(echo "$kw_data" | jq -r '.search_intent_info.main_intent // "unknown"')
|
|
1478
|
+
|
|
1479
|
+
# Get SERP data for weakness detection
|
|
1480
|
+
local serp_data
|
|
1481
|
+
serp_data=$(dataforseo_serp_organic "$kw" "$location_code" "$language_code")
|
|
1482
|
+
|
|
1483
|
+
# Detect weaknesses
|
|
1484
|
+
local weakness_result
|
|
1485
|
+
weakness_result=$(detect_weaknesses "$serp_data")
|
|
1486
|
+
local weakness_count
|
|
1487
|
+
weakness_count=$(echo "$weakness_result" | cut -d'|' -f1)
|
|
1488
|
+
local weakness_list
|
|
1489
|
+
weakness_list=$(echo "$weakness_result" | cut -d'|' -f2)
|
|
1490
|
+
|
|
1491
|
+
# Get domain score from first result
|
|
1492
|
+
local domain_score
|
|
1493
|
+
domain_score=$(echo "$serp_data" | jq -r '.tasks[0].result[0].items[0].main_domain_rank // 0' 2>/dev/null || echo "0")
|
|
1494
|
+
local page_score
|
|
1495
|
+
page_score=$(echo "$serp_data" | jq -r '.tasks[0].result[0].items[0].page_rank // 0' 2>/dev/null || echo "0")
|
|
1496
|
+
|
|
1497
|
+
# Normalize scores to 0-100
|
|
1498
|
+
domain_score=$(echo "scale=0; $domain_score / 10" | bc 2>/dev/null || echo "0")
|
|
1499
|
+
page_score=$(echo "scale=0; $page_score / 10" | bc 2>/dev/null || echo "0")
|
|
1500
|
+
|
|
1501
|
+
# Calculate keyword score
|
|
1502
|
+
local serp_features
|
|
1503
|
+
serp_features=$(echo "$serp_data" | jq '.tasks[0].result[0].item_types // []' 2>/dev/null || echo "[]")
|
|
1504
|
+
local keyword_score
|
|
1505
|
+
keyword_score=$(calculate_keyword_score "$weakness_count" "$volume" "$difficulty" "$serp_features")
|
|
1506
|
+
|
|
1507
|
+
# Build result object and add to results
|
|
1508
|
+
local result_obj
|
|
1509
|
+
result_obj="{\"keyword\":\"$kw\",\"volume\":$volume,\"cpc\":$cpc,\"difficulty\":$difficulty,\"intent\":\"$intent\",\"keyword_score\":$keyword_score,\"domain_score\":$domain_score,\"page_score\":$page_score,\"weakness_count\":$weakness_count,\"weaknesses\":\"$weakness_list\"}"
|
|
1510
|
+
results=$(echo "$results [$result_obj]" | jq -s 'add')
|
|
1511
|
+
done <<< "$kw_list"
|
|
1512
|
+
done
|
|
1513
|
+
fi
|
|
1514
|
+
;;
|
|
1515
|
+
esac
|
|
1516
|
+
|
|
1517
|
+
local count
|
|
1518
|
+
count=$(echo "$results" | jq 'length')
|
|
1519
|
+
print_success "Found $count keywords"
|
|
1520
|
+
|
|
1521
|
+
# Print appropriate table
|
|
1522
|
+
if [[ "$mode" == "competitor" ]] || [[ "$mode" == "gap" ]] || [[ "$mode" == "domain" ]]; then
|
|
1523
|
+
print_research_table "$results" "competitor"
|
|
1524
|
+
else
|
|
1525
|
+
print_research_table "$results" "extended"
|
|
1526
|
+
fi
|
|
1527
|
+
|
|
1528
|
+
# Export CSV if requested
|
|
1529
|
+
if [[ "$csv_export" == "true" ]]; then
|
|
1530
|
+
local timestamp
|
|
1531
|
+
timestamp=$(date +"%Y%m%d-%H%M%S")
|
|
1532
|
+
if [[ "$mode" == "competitor" ]] || [[ "$mode" == "gap" ]] || [[ "$mode" == "domain" ]]; then
|
|
1533
|
+
export_csv "$results" "competitor" "keyword-research-extended-$timestamp.csv"
|
|
1534
|
+
else
|
|
1535
|
+
export_csv "$results" "extended" "keyword-research-extended-$timestamp.csv"
|
|
1536
|
+
fi
|
|
1537
|
+
fi
|
|
1538
|
+
|
|
1539
|
+
return 0
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
apply_filters() {
|
|
1543
|
+
local json_data="$1"
|
|
1544
|
+
local filters="$2"
|
|
1545
|
+
|
|
1546
|
+
local result="$json_data"
|
|
1547
|
+
|
|
1548
|
+
# Parse filters (format: min-volume:1000,max-difficulty:40,intent:commercial,contains:term,excludes:term)
|
|
1549
|
+
IFS=',' read -ra filter_array <<< "$filters"
|
|
1550
|
+
|
|
1551
|
+
for filter in "${filter_array[@]}"; do
|
|
1552
|
+
local key="${filter%%:*}"
|
|
1553
|
+
local value="${filter#*:}"
|
|
1554
|
+
|
|
1555
|
+
case "$key" in
|
|
1556
|
+
"min-volume")
|
|
1557
|
+
result=$(echo "$result" | jq --argjson v "$value" '[.[] | select(.volume >= $v)]')
|
|
1558
|
+
;;
|
|
1559
|
+
"max-volume")
|
|
1560
|
+
result=$(echo "$result" | jq --argjson v "$value" '[.[] | select(.volume <= $v)]')
|
|
1561
|
+
;;
|
|
1562
|
+
"min-difficulty")
|
|
1563
|
+
result=$(echo "$result" | jq --argjson v "$value" '[.[] | select(.difficulty >= $v)]')
|
|
1564
|
+
;;
|
|
1565
|
+
"max-difficulty")
|
|
1566
|
+
result=$(echo "$result" | jq --argjson v "$value" '[.[] | select(.difficulty <= $v)]')
|
|
1567
|
+
;;
|
|
1568
|
+
"intent")
|
|
1569
|
+
result=$(echo "$result" | jq --arg v "$value" '[.[] | select(.intent == $v)]')
|
|
1570
|
+
;;
|
|
1571
|
+
"contains")
|
|
1572
|
+
result=$(echo "$result" | jq --arg v "$value" '[.[] | select(.keyword | contains($v))]')
|
|
1573
|
+
;;
|
|
1574
|
+
"excludes")
|
|
1575
|
+
result=$(echo "$result" | jq --arg v "$value" '[.[] | select(.keyword | contains($v) | not)]')
|
|
1576
|
+
;;
|
|
1577
|
+
*)
|
|
1578
|
+
print_warning "Unknown filter: $key"
|
|
1579
|
+
;;
|
|
1580
|
+
esac
|
|
1581
|
+
done
|
|
1582
|
+
|
|
1583
|
+
echo "$result"
|
|
1584
|
+
return 0
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
# =============================================================================
|
|
1588
|
+
# Help
|
|
1589
|
+
# =============================================================================
|
|
1590
|
+
|
|
1591
|
+
show_help() {
|
|
1592
|
+
print_header "Keyword Research Helper"
|
|
1593
|
+
echo ""
|
|
1594
|
+
echo "Usage: $0 <command> [options]"
|
|
1595
|
+
echo ""
|
|
1596
|
+
echo "Commands:"
|
|
1597
|
+
echo " research <keywords> Basic keyword expansion"
|
|
1598
|
+
echo " autocomplete <keyword> Google autocomplete suggestions"
|
|
1599
|
+
echo " extended <keywords> Full SERP analysis with weakness detection"
|
|
1600
|
+
echo " webmaster <site-url> Keywords from GSC + Bing for your verified sites"
|
|
1601
|
+
echo " sites List verified sites in GSC and Bing"
|
|
1602
|
+
echo " config Show current configuration"
|
|
1603
|
+
echo " set-config Set default preferences"
|
|
1604
|
+
echo " help Show this help"
|
|
1605
|
+
echo ""
|
|
1606
|
+
echo "Options:"
|
|
1607
|
+
echo " --provider <name> dataforseo, serper, or both (default: dataforseo)"
|
|
1608
|
+
echo " --locale <code> us-en, uk-en, ca-en, au-en, de-de, fr-fr, es-es"
|
|
1609
|
+
echo " --limit <n> Number of results (default: 100, max: 10000)"
|
|
1610
|
+
echo " --days <n> Days of data for webmaster tools (default: 30)"
|
|
1611
|
+
echo " --csv Export results to CSV"
|
|
1612
|
+
echo " --quick Skip weakness detection (extended only)"
|
|
1613
|
+
echo " --no-enrich Skip DataForSEO enrichment (webmaster only)"
|
|
1614
|
+
echo " --ahrefs Include Ahrefs DR/UR metrics"
|
|
1615
|
+
echo " --domain <domain> Domain research mode"
|
|
1616
|
+
echo " --competitor <domain> Competitor research mode"
|
|
1617
|
+
echo " --gap <your,competitor> Keyword gap analysis"
|
|
1618
|
+
echo ""
|
|
1619
|
+
echo "Filters:"
|
|
1620
|
+
echo " --min-volume <n> Minimum search volume"
|
|
1621
|
+
echo " --max-volume <n> Maximum search volume"
|
|
1622
|
+
echo " --min-difficulty <n> Minimum keyword difficulty"
|
|
1623
|
+
echo " --max-difficulty <n> Maximum keyword difficulty"
|
|
1624
|
+
echo " --intent <type> Filter by intent (informational, commercial, etc.)"
|
|
1625
|
+
echo " --contains <term> Include keywords containing term"
|
|
1626
|
+
echo " --excludes <term> Exclude keywords containing term"
|
|
1627
|
+
echo ""
|
|
1628
|
+
echo "Examples:"
|
|
1629
|
+
echo " $0 research \"best seo tools, keyword research\""
|
|
1630
|
+
echo " $0 autocomplete \"how to lose weight\""
|
|
1631
|
+
echo " $0 extended \"dog training\" --ahrefs"
|
|
1632
|
+
echo " $0 extended --competitor petco.com --limit 500"
|
|
1633
|
+
echo " $0 extended --gap mysite.com,competitor.com"
|
|
1634
|
+
echo " $0 research \"seo\" --min-volume 1000 --max-difficulty 40 --csv"
|
|
1635
|
+
echo ""
|
|
1636
|
+
echo "Webmaster Tools (for your verified sites):"
|
|
1637
|
+
echo " $0 sites # List verified sites"
|
|
1638
|
+
echo " $0 webmaster https://example.com # Get keywords from GSC + Bing"
|
|
1639
|
+
echo " $0 webmaster https://example.com --days 90 # Last 90 days"
|
|
1640
|
+
echo " $0 webmaster https://example.com --no-enrich --csv"
|
|
1641
|
+
echo ""
|
|
1642
|
+
return 0
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
# =============================================================================
|
|
1646
|
+
# Main
|
|
1647
|
+
# =============================================================================
|
|
1648
|
+
|
|
1649
|
+
main() {
|
|
1650
|
+
local command="${1:-help}"
|
|
1651
|
+
shift || true
|
|
1652
|
+
|
|
1653
|
+
# Load configuration
|
|
1654
|
+
load_config
|
|
1655
|
+
|
|
1656
|
+
# Parse options
|
|
1657
|
+
local keywords=""
|
|
1658
|
+
local provider="$DEFAULT_PROVIDER"
|
|
1659
|
+
local locale="$DEFAULT_LOCALE"
|
|
1660
|
+
local limit="$DEFAULT_LIMIT"
|
|
1661
|
+
local days="30"
|
|
1662
|
+
local csv_export="false"
|
|
1663
|
+
local quick_mode="false"
|
|
1664
|
+
local include_ahrefs="false"
|
|
1665
|
+
local enrich="true"
|
|
1666
|
+
local mode=""
|
|
1667
|
+
local target=""
|
|
1668
|
+
local filters=""
|
|
1669
|
+
|
|
1670
|
+
while [[ $# -gt 0 ]]; do
|
|
1671
|
+
case "$1" in
|
|
1672
|
+
--provider)
|
|
1673
|
+
provider="$2"
|
|
1674
|
+
shift 2
|
|
1675
|
+
;;
|
|
1676
|
+
--locale)
|
|
1677
|
+
locale="$2"
|
|
1678
|
+
shift 2
|
|
1679
|
+
;;
|
|
1680
|
+
--limit)
|
|
1681
|
+
limit="$2"
|
|
1682
|
+
shift 2
|
|
1683
|
+
;;
|
|
1684
|
+
--days)
|
|
1685
|
+
days="$2"
|
|
1686
|
+
shift 2
|
|
1687
|
+
;;
|
|
1688
|
+
--csv)
|
|
1689
|
+
csv_export="true"
|
|
1690
|
+
shift
|
|
1691
|
+
;;
|
|
1692
|
+
--quick)
|
|
1693
|
+
quick_mode="true"
|
|
1694
|
+
shift
|
|
1695
|
+
;;
|
|
1696
|
+
--no-enrich)
|
|
1697
|
+
enrich="false"
|
|
1698
|
+
shift
|
|
1699
|
+
;;
|
|
1700
|
+
--ahrefs)
|
|
1701
|
+
include_ahrefs="true"
|
|
1702
|
+
shift
|
|
1703
|
+
;;
|
|
1704
|
+
--domain)
|
|
1705
|
+
mode="domain"
|
|
1706
|
+
target="$2"
|
|
1707
|
+
shift 2
|
|
1708
|
+
;;
|
|
1709
|
+
--competitor)
|
|
1710
|
+
mode="competitor"
|
|
1711
|
+
target="$2"
|
|
1712
|
+
shift 2
|
|
1713
|
+
;;
|
|
1714
|
+
--gap)
|
|
1715
|
+
mode="gap"
|
|
1716
|
+
target="$2"
|
|
1717
|
+
shift 2
|
|
1718
|
+
;;
|
|
1719
|
+
--min-volume)
|
|
1720
|
+
filters="${filters:+$filters,}min-volume:$2"
|
|
1721
|
+
shift 2
|
|
1722
|
+
;;
|
|
1723
|
+
--max-volume)
|
|
1724
|
+
filters="${filters:+$filters,}max-volume:$2"
|
|
1725
|
+
shift 2
|
|
1726
|
+
;;
|
|
1727
|
+
--min-difficulty)
|
|
1728
|
+
filters="${filters:+$filters,}min-difficulty:$2"
|
|
1729
|
+
shift 2
|
|
1730
|
+
;;
|
|
1731
|
+
--max-difficulty)
|
|
1732
|
+
filters="${filters:+$filters,}max-difficulty:$2"
|
|
1733
|
+
shift 2
|
|
1734
|
+
;;
|
|
1735
|
+
--intent)
|
|
1736
|
+
filters="${filters:+$filters,}intent:$2"
|
|
1737
|
+
shift 2
|
|
1738
|
+
;;
|
|
1739
|
+
--contains)
|
|
1740
|
+
filters="${filters:+$filters,}contains:$2"
|
|
1741
|
+
shift 2
|
|
1742
|
+
;;
|
|
1743
|
+
--excludes)
|
|
1744
|
+
filters="${filters:+$filters,}excludes:$2"
|
|
1745
|
+
shift 2
|
|
1746
|
+
;;
|
|
1747
|
+
-*)
|
|
1748
|
+
print_error "Unknown option: $1"
|
|
1749
|
+
show_help
|
|
1750
|
+
return 1
|
|
1751
|
+
;;
|
|
1752
|
+
*)
|
|
1753
|
+
keywords="$1"
|
|
1754
|
+
shift
|
|
1755
|
+
;;
|
|
1756
|
+
esac
|
|
1757
|
+
done
|
|
1758
|
+
|
|
1759
|
+
case "$command" in
|
|
1760
|
+
"research")
|
|
1761
|
+
if [[ -z "$keywords" ]]; then
|
|
1762
|
+
print_error "Keywords required"
|
|
1763
|
+
show_help
|
|
1764
|
+
return 1
|
|
1765
|
+
fi
|
|
1766
|
+
do_keyword_research "$keywords" "$provider" "$locale" "$limit" "$csv_export" "$filters"
|
|
1767
|
+
;;
|
|
1768
|
+
"autocomplete")
|
|
1769
|
+
if [[ -z "$keywords" ]]; then
|
|
1770
|
+
print_error "Keyword required"
|
|
1771
|
+
show_help
|
|
1772
|
+
return 1
|
|
1773
|
+
fi
|
|
1774
|
+
do_autocomplete_research "$keywords" "$provider" "$locale" "$csv_export"
|
|
1775
|
+
;;
|
|
1776
|
+
"extended")
|
|
1777
|
+
if [[ -z "$keywords" ]] && [[ -z "$mode" ]]; then
|
|
1778
|
+
print_error "Keywords or mode (--domain, --competitor, --gap) required"
|
|
1779
|
+
show_help
|
|
1780
|
+
return 1
|
|
1781
|
+
fi
|
|
1782
|
+
do_extended_research "$keywords" "$provider" "$locale" "$limit" "$csv_export" "$quick_mode" "$include_ahrefs" "$mode" "$target"
|
|
1783
|
+
;;
|
|
1784
|
+
"webmaster")
|
|
1785
|
+
if [[ -z "$keywords" ]]; then
|
|
1786
|
+
print_error "Site URL required (e.g., https://example.com)"
|
|
1787
|
+
show_help
|
|
1788
|
+
return 1
|
|
1789
|
+
fi
|
|
1790
|
+
do_webmaster_research "$keywords" "$days" "$limit" "$csv_export" "$enrich"
|
|
1791
|
+
;;
|
|
1792
|
+
"sites")
|
|
1793
|
+
do_list_sites
|
|
1794
|
+
;;
|
|
1795
|
+
"config")
|
|
1796
|
+
show_config
|
|
1797
|
+
;;
|
|
1798
|
+
"set-config")
|
|
1799
|
+
local new_locale
|
|
1800
|
+
new_locale=$(prompt_locale)
|
|
1801
|
+
read -p "Default provider [dataforseo/serper/both] ($DEFAULT_PROVIDER): " new_provider
|
|
1802
|
+
new_provider="${new_provider:-$DEFAULT_PROVIDER}"
|
|
1803
|
+
read -p "Default limit ($DEFAULT_LIMIT): " new_limit
|
|
1804
|
+
new_limit="${new_limit:-$DEFAULT_LIMIT}"
|
|
1805
|
+
save_config "$new_locale" "$new_provider" "$new_limit"
|
|
1806
|
+
;;
|
|
1807
|
+
"help"|*)
|
|
1808
|
+
show_help
|
|
1809
|
+
;;
|
|
1810
|
+
esac
|
|
1811
|
+
|
|
1812
|
+
return 0
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
main "$@"
|