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,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 "$@"