lv24-seo 0.1.0__tar.gz

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 (175) hide show
  1. lv24_seo-0.1.0/.env.example +50 -0
  2. lv24_seo-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +46 -0
  3. lv24_seo-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +36 -0
  4. lv24_seo-0.1.0/.github/pull_request_template.md +29 -0
  5. lv24_seo-0.1.0/.github/workflows/ci.yml +96 -0
  6. lv24_seo-0.1.0/.github/workflows/release.yml +212 -0
  7. lv24_seo-0.1.0/.gitignore +48 -0
  8. lv24_seo-0.1.0/.gitleaks.toml +16 -0
  9. lv24_seo-0.1.0/AGENTS.md +91 -0
  10. lv24_seo-0.1.0/CHANGELOG.md +54 -0
  11. lv24_seo-0.1.0/CLAUDE.md +4 -0
  12. lv24_seo-0.1.0/CODE_OF_CONDUCT.md +85 -0
  13. lv24_seo-0.1.0/CONTRIBUTING.md +63 -0
  14. lv24_seo-0.1.0/LICENSE +202 -0
  15. lv24_seo-0.1.0/PKG-INFO +163 -0
  16. lv24_seo-0.1.0/README.md +125 -0
  17. lv24_seo-0.1.0/README.ru.md +42 -0
  18. lv24_seo-0.1.0/SECURITY.md +44 -0
  19. lv24_seo-0.1.0/TRADEMARKS.md +13 -0
  20. lv24_seo-0.1.0/docs/architecture.md +115 -0
  21. lv24_seo-0.1.0/docs/cli.md +100 -0
  22. lv24_seo-0.1.0/docs/config.md +98 -0
  23. lv24_seo-0.1.0/docs/data-sources.md +126 -0
  24. lv24_seo-0.1.0/docs/decisions/ADR-0001-stack.md +29 -0
  25. lv24_seo-0.1.0/docs/decisions/ADR-0002-honesty-envelope.md +33 -0
  26. lv24_seo-0.1.0/docs/decisions/ADR-0003-write-gating.md +30 -0
  27. lv24_seo-0.1.0/docs/guides/github-actions.md +395 -0
  28. lv24_seo-0.1.0/docs/guides/mcp-clients.md +339 -0
  29. lv24_seo-0.1.0/docs/guides/quickstart.md +78 -0
  30. lv24_seo-0.1.0/docs/guides/troubleshooting.md +390 -0
  31. lv24_seo-0.1.0/docs/guides/vps-cron.md +340 -0
  32. lv24_seo-0.1.0/docs/mcp.md +121 -0
  33. lv24_seo-0.1.0/docs/plans/2026-11-10-implementation-plan.md +379 -0
  34. lv24_seo-0.1.0/docs/releases.md +372 -0
  35. lv24_seo-0.1.0/docs/research/2026-11-landscape.md +124 -0
  36. lv24_seo-0.1.0/docs/scheduling.md +66 -0
  37. lv24_seo-0.1.0/docs/skills.md +60 -0
  38. lv24_seo-0.1.0/examples/config/seo.toml +82 -0
  39. lv24_seo-0.1.0/examples/github-actions/seo-weekly.yml +146 -0
  40. lv24_seo-0.1.0/llms.txt +28 -0
  41. lv24_seo-0.1.0/packages/npm/README.md +50 -0
  42. lv24_seo-0.1.0/packages/npm/bin/lv24-seo.js +74 -0
  43. lv24_seo-0.1.0/packages/npm/package.json +42 -0
  44. lv24_seo-0.1.0/pyproject.toml +79 -0
  45. lv24_seo-0.1.0/schemas/tools.json +222 -0
  46. lv24_seo-0.1.0/server.json +32 -0
  47. lv24_seo-0.1.0/skills/manifest.json +45 -0
  48. lv24_seo-0.1.0/skills/seo-indexing-triage/SKILL.md +63 -0
  49. lv24_seo-0.1.0/skills/seo-technical-audit/SKILL.md +64 -0
  50. lv24_seo-0.1.0/skills/seo-weekly/SKILL.md +69 -0
  51. lv24_seo-0.1.0/src/lv24_seo/__init__.py +2 -0
  52. lv24_seo-0.1.0/src/lv24_seo/analysis/__init__.py +9 -0
  53. lv24_seo-0.1.0/src/lv24_seo/analysis/delta.py +177 -0
  54. lv24_seo-0.1.0/src/lv24_seo/analysis/fusion.py +316 -0
  55. lv24_seo-0.1.0/src/lv24_seo/analysis/indexing.py +196 -0
  56. lv24_seo-0.1.0/src/lv24_seo/analysis/logs.py +383 -0
  57. lv24_seo-0.1.0/src/lv24_seo/analysis/opportunities.py +430 -0
  58. lv24_seo-0.1.0/src/lv24_seo/automation/__init__.py +17 -0
  59. lv24_seo-0.1.0/src/lv24_seo/automation/scheduler.py +499 -0
  60. lv24_seo-0.1.0/src/lv24_seo/cli/__init__.py +91 -0
  61. lv24_seo-0.1.0/src/lv24_seo/cli/auth.py +334 -0
  62. lv24_seo-0.1.0/src/lv24_seo/cli/main.py +1661 -0
  63. lv24_seo-0.1.0/src/lv24_seo/core/__init__.py +1 -0
  64. lv24_seo-0.1.0/src/lv24_seo/core/config.py +365 -0
  65. lv24_seo-0.1.0/src/lv24_seo/core/envelope.py +76 -0
  66. lv24_seo-0.1.0/src/lv24_seo/core/errors.py +82 -0
  67. lv24_seo-0.1.0/src/lv24_seo/core/http.py +314 -0
  68. lv24_seo-0.1.0/src/lv24_seo/core/logging.py +315 -0
  69. lv24_seo-0.1.0/src/lv24_seo/core/ratelimit.py +242 -0
  70. lv24_seo-0.1.0/src/lv24_seo/core/secrets.py +409 -0
  71. lv24_seo-0.1.0/src/lv24_seo/core/timeutil.py +240 -0
  72. lv24_seo-0.1.0/src/lv24_seo/core/write_gate.py +327 -0
  73. lv24_seo-0.1.0/src/lv24_seo/crawl/__init__.py +1 -0
  74. lv24_seo-0.1.0/src/lv24_seo/crawl/fetcher.py +398 -0
  75. lv24_seo-0.1.0/src/lv24_seo/crawl/graph.py +193 -0
  76. lv24_seo-0.1.0/src/lv24_seo/crawl/html.py +409 -0
  77. lv24_seo-0.1.0/src/lv24_seo/crawl/models.py +79 -0
  78. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/__init__.py +28 -0
  79. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/base.py +39 -0
  80. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_broken_internal_link.py +102 -0
  81. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_canonical_mismatch.py +59 -0
  82. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_canonical_missing.py +44 -0
  83. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_client_error.py +43 -0
  84. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_duplicate_organization.py +81 -0
  85. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_h1_missing.py +41 -0
  86. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_h1_multiple.py +42 -0
  87. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_hreflang_reciprocal.py +115 -0
  88. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_hreflang_target.py +94 -0
  89. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_hreflang_x_default.py +53 -0
  90. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_html_lang.py +50 -0
  91. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_html_lang_mismatch.py +82 -0
  92. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_image_alt_missing.py +49 -0
  93. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_jsonld_event.py +107 -0
  94. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_jsonld_organization.py +82 -0
  95. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_jsonld_required.py +102 -0
  96. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_meta_description_duplicate.py +60 -0
  97. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_meta_description_missing.py +43 -0
  98. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_mixed_content.py +65 -0
  99. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_noindex_on_200.py +41 -0
  100. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_redirect_chain.py +40 -0
  101. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_robots_blocked.py +68 -0
  102. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_server_error.py +35 -0
  103. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_sitemap_url_noindex.py +106 -0
  104. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_sitemap_url_not_ok.py +74 -0
  105. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_soft_404.py +52 -0
  106. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_thin_content.py +75 -0
  107. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_title_duplicate.py +60 -0
  108. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_title_length.py +53 -0
  109. lv24_seo-0.1.0/src/lv24_seo/crawl/rules/rule_title_missing.py +41 -0
  110. lv24_seo-0.1.0/src/lv24_seo/crawl/sitemap.py +245 -0
  111. lv24_seo-0.1.0/src/lv24_seo/mcp/__init__.py +28 -0
  112. lv24_seo-0.1.0/src/lv24_seo/mcp/registry.py +158 -0
  113. lv24_seo-0.1.0/src/lv24_seo/mcp/server.py +411 -0
  114. lv24_seo-0.1.0/src/lv24_seo/mcp/tools.py +1051 -0
  115. lv24_seo-0.1.0/src/lv24_seo/providers/__init__.py +5 -0
  116. lv24_seo-0.1.0/src/lv24_seo/providers/base.py +80 -0
  117. lv24_seo-0.1.0/src/lv24_seo/providers/bing/__init__.py +5 -0
  118. lv24_seo-0.1.0/src/lv24_seo/providers/bing/client.py +381 -0
  119. lv24_seo-0.1.0/src/lv24_seo/providers/crux/__init__.py +5 -0
  120. lv24_seo-0.1.0/src/lv24_seo/providers/crux/client.py +201 -0
  121. lv24_seo-0.1.0/src/lv24_seo/providers/gsc/__init__.py +5 -0
  122. lv24_seo-0.1.0/src/lv24_seo/providers/gsc/client.py +288 -0
  123. lv24_seo-0.1.0/src/lv24_seo/providers/psi/__init__.py +5 -0
  124. lv24_seo-0.1.0/src/lv24_seo/providers/psi/client.py +213 -0
  125. lv24_seo-0.1.0/src/lv24_seo/providers/yandex_metrika/__init__.py +5 -0
  126. lv24_seo-0.1.0/src/lv24_seo/providers/yandex_metrika/client.py +345 -0
  127. lv24_seo-0.1.0/src/lv24_seo/providers/yandex_webmaster/__init__.py +5 -0
  128. lv24_seo-0.1.0/src/lv24_seo/providers/yandex_webmaster/client.py +546 -0
  129. lv24_seo-0.1.0/src/lv24_seo/report/__init__.py +1 -0
  130. lv24_seo-0.1.0/src/lv24_seo/report/builders.py +464 -0
  131. lv24_seo-0.1.0/src/lv24_seo/report/delivery.py +402 -0
  132. lv24_seo-0.1.0/src/lv24_seo/report/models.py +58 -0
  133. lv24_seo-0.1.0/src/lv24_seo/report/render.py +309 -0
  134. lv24_seo-0.1.0/src/lv24_seo/services.py +1662 -0
  135. lv24_seo-0.1.0/src/lv24_seo/store/__init__.py +16 -0
  136. lv24_seo-0.1.0/src/lv24_seo/store/db.py +79 -0
  137. lv24_seo-0.1.0/src/lv24_seo/store/repositories.py +581 -0
  138. lv24_seo-0.1.0/src/lv24_seo/store/schema.sql +146 -0
  139. lv24_seo-0.1.0/tests/unit/test_analysis_delta.py +218 -0
  140. lv24_seo-0.1.0/tests/unit/test_analysis_fusion.py +226 -0
  141. lv24_seo-0.1.0/tests/unit/test_analysis_indexing.py +255 -0
  142. lv24_seo-0.1.0/tests/unit/test_analysis_logs.py +366 -0
  143. lv24_seo-0.1.0/tests/unit/test_analysis_opportunities.py +253 -0
  144. lv24_seo-0.1.0/tests/unit/test_automation_scheduler.py +279 -0
  145. lv24_seo-0.1.0/tests/unit/test_cli.py +591 -0
  146. lv24_seo-0.1.0/tests/unit/test_cli_writes.py +388 -0
  147. lv24_seo-0.1.0/tests/unit/test_config.py +462 -0
  148. lv24_seo-0.1.0/tests/unit/test_crawl_fetcher.py +379 -0
  149. lv24_seo-0.1.0/tests/unit/test_crawl_graph.py +263 -0
  150. lv24_seo-0.1.0/tests/unit/test_crawl_html.py +314 -0
  151. lv24_seo-0.1.0/tests/unit/test_crawl_sitemap.py +293 -0
  152. lv24_seo-0.1.0/tests/unit/test_envelope.py +63 -0
  153. lv24_seo-0.1.0/tests/unit/test_http.py +345 -0
  154. lv24_seo-0.1.0/tests/unit/test_logging.py +303 -0
  155. lv24_seo-0.1.0/tests/unit/test_mcp.py +672 -0
  156. lv24_seo-0.1.0/tests/unit/test_provider_base.py +47 -0
  157. lv24_seo-0.1.0/tests/unit/test_provider_bing.py +258 -0
  158. lv24_seo-0.1.0/tests/unit/test_provider_crux_psi.py +347 -0
  159. lv24_seo-0.1.0/tests/unit/test_provider_gsc.py +336 -0
  160. lv24_seo-0.1.0/tests/unit/test_provider_yandex_metrika.py +313 -0
  161. lv24_seo-0.1.0/tests/unit/test_provider_yandex_webmaster.py +387 -0
  162. lv24_seo-0.1.0/tests/unit/test_ratelimit.py +298 -0
  163. lv24_seo-0.1.0/tests/unit/test_registry.py +54 -0
  164. lv24_seo-0.1.0/tests/unit/test_report_builders.py +326 -0
  165. lv24_seo-0.1.0/tests/unit/test_report_delivery.py +590 -0
  166. lv24_seo-0.1.0/tests/unit/test_report_render.py +305 -0
  167. lv24_seo-0.1.0/tests/unit/test_rules_content.py +338 -0
  168. lv24_seo-0.1.0/tests/unit/test_rules_indexability.py +343 -0
  169. lv24_seo-0.1.0/tests/unit/test_rules_structured.py +500 -0
  170. lv24_seo-0.1.0/tests/unit/test_secrets.py +288 -0
  171. lv24_seo-0.1.0/tests/unit/test_services.py +804 -0
  172. lv24_seo-0.1.0/tests/unit/test_store.py +373 -0
  173. lv24_seo-0.1.0/tests/unit/test_timeutil.py +409 -0
  174. lv24_seo-0.1.0/tests/unit/test_write_gate.py +340 -0
  175. lv24_seo-0.1.0/uv.lock +1583 -0
@@ -0,0 +1,50 @@
1
+ # lv24-seo-optimizer — configuration template
2
+ # Copy to `.env` (git-ignored) or export the variables in your shell.
3
+ # Every value is optional: the default path needs no paid key at all.
4
+
5
+ # ---------- Google Search Console ----------
6
+ # Either an OAuth client (loopback flow) or a service-account JSON key that was added
7
+ # to the property as Full/Owner. Prefer OAuth for a domain property you control.
8
+ LV24_SEO_GSC_CLIENT_ID=
9
+ LV24_SEO_GSC_CLIENT_SECRET=
10
+ LV24_SEO_GSC_SERVICE_ACCOUNT_FILE=
11
+ LV24_SEO_GSC_PROPERTY=sc-domain:example.com
12
+
13
+ # ---------- Yandex Webmaster ----------
14
+ # Scope: webmaster:hostinfo (+ webmaster:verify if you add hosts programmatically).
15
+ LV24_SEO_YANDEX_WEBMASTER_TOKEN=
16
+ LV24_SEO_YANDEX_HOST_ID=https:example.com:443
17
+
18
+ # ---------- Yandex Metrika ----------
19
+ # Scope: metrika:read
20
+ LV24_SEO_YANDEX_METRIKA_TOKEN=
21
+ LV24_SEO_YANDEX_METRIKA_COUNTER=
22
+
23
+ # ---------- Bing Webmaster + IndexNow ----------
24
+ LV24_SEO_BING_API_KEY=
25
+ LV24_SEO_INDEXNOW_KEY=
26
+
27
+ # ---------- Chrome UX Report / PageSpeed Insights ----------
28
+ LV24_SEO_CRUX_API_KEY=
29
+ LV24_SEO_PSI_API_KEY=
30
+
31
+ # ---------- Optional paid adapters (BYO key; disabled by default) ----------
32
+ LV24_SEO_DATAFORSEO_LOGIN=
33
+ LV24_SEO_DATAFORSEO_PASSWORD=
34
+ LV24_SEO_SERPAPI_KEY=
35
+
36
+ # ---------- Delivery ----------
37
+ LV24_SEO_SMTP_HOST=
38
+ LV24_SEO_SMTP_PORT=587
39
+ LV24_SEO_SMTP_USER=
40
+ LV24_SEO_SMTP_PASSWORD=
41
+ LV24_SEO_SMTP_FROM=seo@example.com
42
+ LV24_SEO_MAIL_TO=owner@example.com
43
+ LV24_SEO_TELEGRAM_BOT_TOKEN=
44
+ LV24_SEO_TELEGRAM_CHAT_ID=
45
+
46
+ # ---------- Storage / behaviour ----------
47
+ LV24_SEO_DATA_DIR=./data
48
+ LV24_SEO_LOG_LEVEL=INFO
49
+ # Set to 1 only in CI/containers where keyring is unavailable.
50
+ LV24_SEO_ALLOW_FILE_SECRETS=0
@@ -0,0 +1,46 @@
1
+ name: Bug report
2
+ description: Something in lv24-seo-optimizer does not work as documented
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for the report. Please do not paste tokens, cookies or real query data.
9
+ - type: input
10
+ id: version
11
+ attributes:
12
+ label: Version
13
+ description: Output of `lv24-seo version --json`
14
+ validations:
15
+ required: true
16
+ - type: dropdown
17
+ id: surface
18
+ attributes:
19
+ label: Surface
20
+ options: [CLI, MCP server, skill, report/email, crawl, provider API, install, docs]
21
+ validations:
22
+ required: true
23
+ - type: input
24
+ id: provider
25
+ attributes:
26
+ label: Provider (if relevant)
27
+ placeholder: gsc / yandex_webmaster / yandex_metrika / bing / crux / psi / crawl
28
+ - type: textarea
29
+ id: repro
30
+ attributes:
31
+ label: Steps to reproduce
32
+ placeholder: |
33
+ 1. lv24-seo ...
34
+ 2. ...
35
+ - type: textarea
36
+ id: expected
37
+ attributes:
38
+ label: Expected vs actual
39
+ validations:
40
+ required: true
41
+ - type: textarea
42
+ id: logs
43
+ attributes:
44
+ label: Redacted output
45
+ description: Run with `--verbose` and remove tokens, cookies and personal data.
46
+ render: text
@@ -0,0 +1,36 @@
1
+ name: Feature request
2
+ description: Propose a provider, crawl rule, report or skill
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: dropdown
6
+ id: kind
7
+ attributes:
8
+ label: What is it?
9
+ options: [free-tier provider, paid adapter (BYO key), crawl rule, report, MCP tool, CLI command, skill, docs]
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: problem
14
+ attributes:
15
+ label: Problem
16
+ description: What SEO question cannot be answered today?
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: data
21
+ attributes:
22
+ label: Data source
23
+ description: API/docs link, free quota, auth model, what it can and cannot return.
24
+ validations:
25
+ required: true
26
+ - type: checkboxes
27
+ id: rules
28
+ attributes:
29
+ label: Project rules
30
+ options:
31
+ - label: It works on a free tier (or is an optional adapter with a user-supplied key)
32
+ required: true
33
+ - label: It does not require scraping Google/Yandex SERPs
34
+ required: true
35
+ - label: I checked that no existing tool in the repo covers this
36
+ required: true
@@ -0,0 +1,29 @@
1
+ ## Problem
2
+
3
+ <!-- What SEO question or failure does this change address? -->
4
+
5
+ ## Solution
6
+
7
+ <!-- What changed, and why this way. -->
8
+
9
+ ## Provider limits touched
10
+
11
+ <!-- GSC / Yandex / Bing quotas, truncation, data lag. Write "none" if not applicable. -->
12
+
13
+ ## Verification
14
+
15
+ ```bash
16
+ uv run ruff check .
17
+ uv run ruff format --check .
18
+ uv run mypy src
19
+ uv run pytest -q
20
+ ```
21
+
22
+ ## Checklist
23
+
24
+ - [ ] No paid dependency added to the default path
25
+ - [ ] `limitations[]` / truncation honest
26
+ - [ ] Write path gated (`--allow-write`, idempotency key)
27
+ - [ ] Docs updated in the same change
28
+ - [ ] Works on Windows
29
+ - [ ] No secrets, tokens or real query data in tests/fixtures
@@ -0,0 +1,96 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths-ignore: ["docs/**", "**/*.md", "LICENSE"]
7
+ pull_request:
8
+ paths-ignore: ["docs/**", "**/*.md", "LICENSE"]
9
+ workflow_dispatch:
10
+
11
+ # Zero-spend policy: public repo + standard ubuntu runners only, no matrix explosion,
12
+ # no larger runners, no artifacts beyond 7 days, concurrency cancels stale runs.
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: ci-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ lint:
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 10
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+ with:
27
+ fetch-depth: 0
28
+ - uses: astral-sh/setup-uv@v10.3.3
29
+ with:
30
+ enable-cache: true
31
+ - name: Install
32
+ run: uv sync --extra dev
33
+ - name: Ruff lint
34
+ run: uv run ruff check .
35
+ - name: Ruff format
36
+ run: uv run ruff format --check .
37
+ - name: Mypy
38
+ run: uv run mypy src
39
+ - name: Gitleaks
40
+ run: |
41
+ curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.24.3/gitleaks_8.24.3_linux_x64.tar.gz \
42
+ | tar -xz -C /usr/local/bin gitleaks
43
+ gitleaks detect --source . --redact --verbose --exit-code 2 --log-opts="--all"
44
+
45
+ test:
46
+ runs-on: ubuntu-latest
47
+ timeout-minutes: 15
48
+ strategy:
49
+ fail-fast: false
50
+ matrix:
51
+ python: ["3.11", "3.13"]
52
+ steps:
53
+ - uses: actions/checkout@v7
54
+ - uses: astral-sh/setup-uv@v10.3.3
55
+ with:
56
+ enable-cache: true
57
+ python-version: ${{ matrix.python }}
58
+ - name: Install
59
+ run: uv sync --extra dev
60
+ - name: Tests (offline only)
61
+ run: uv run pytest -q -m "not live and not network"
62
+
63
+ schemas:
64
+ runs-on: ubuntu-latest
65
+ timeout-minutes: 10
66
+ steps:
67
+ - uses: actions/checkout@v7
68
+ - uses: astral-sh/setup-uv@v10.3.3
69
+ with:
70
+ enable-cache: true
71
+ - name: Install
72
+ run: uv sync --extra dev
73
+ - name: Regenerate tool schemas
74
+ run: uv run python -m lv24_seo.mcp.registry > /tmp/tools.json
75
+ - name: Fail on drift
76
+ run: diff -u schemas/tools.json /tmp/tools.json
77
+
78
+ npm:
79
+ # The npm package is a thin launcher; this job only proves it is syntactically valid and
80
+ # packages the intended files. Publishing happens in release.yml on a v* tag.
81
+ runs-on: ubuntu-latest
82
+ timeout-minutes: 5
83
+ defaults:
84
+ run:
85
+ working-directory: packages/npm
86
+ steps:
87
+ - uses: actions/checkout@v7
88
+ - uses: actions/setup-node@v7.1.0
89
+ with:
90
+ node-version: "22"
91
+ - name: Syntax check
92
+ run: node --check bin/lv24-seo.js
93
+ - name: Package contents
94
+ run: npm pack --dry-run
95
+
96
+
@@ -0,0 +1,212 @@
1
+ name: release
2
+
3
+ # Build and publish lv24-seo when a v* tag is pushed.
4
+ #
5
+ # Triggers:
6
+ # * push of a v* tag -> build + publish to PyPI;
7
+ # * workflow_dispatch -> build-only dry run unless the selected ref is a v* tag.
8
+ # There is deliberately no pull_request trigger, so releases never run for PRs.
9
+ #
10
+ # Zero-spend policy (same as ci.yml):
11
+ # * public repository + standard ubuntu-latest runners only = $0;
12
+ # * no matrix, no larger runners, no paid services;
13
+ # * two short jobs, artifacts kept for 7 days;
14
+ # * PyPI uses OIDC Trusted Publishing, so no long-lived API token secret exists.
15
+ on:
16
+ push:
17
+ tags: ["v*"]
18
+ workflow_dispatch:
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ concurrency:
24
+ group: release-${{ github.ref }}
25
+ # Never cancel a run that may be halfway through a PyPI upload.
26
+ cancel-in-progress: false
27
+
28
+ jobs:
29
+ build:
30
+ name: build distributions
31
+ runs-on: ubuntu-latest
32
+ timeout-minutes: 10
33
+ steps:
34
+ - name: Check out the repository
35
+ uses: actions/checkout@v7
36
+ with:
37
+ persist-credentials: false
38
+
39
+ - name: Install uv and Python
40
+ uses: astral-sh/setup-uv@v10.3.3
41
+ with:
42
+ enable-cache: true
43
+ python-version: "3.13"
44
+
45
+ - name: Check tag, pyproject, __version__ and server.json agree
46
+ # A release must never publish a tag that disagrees with the package metadata.
47
+ if: startsWith(github.ref, 'refs/tags/v')
48
+ run: |
49
+ python3 - "${GITHUB_REF_NAME#v}" <<'PY'
50
+ import json
51
+ import pathlib
52
+ import re
53
+ import sys
54
+ import tomllib
55
+
56
+ tag = sys.argv[1]
57
+ pyproject = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
58
+ server = json.loads(pathlib.Path("server.json").read_text(encoding="utf-8"))
59
+ npm = json.loads(pathlib.Path("packages/npm/package.json").read_text(encoding="utf-8"))
60
+ init_source = pathlib.Path("src/lv24_seo/__init__.py").read_text(encoding="utf-8")
61
+ init_version = re.search(r'^__version__\s*=\s*"([^"]+)"', init_source, re.MULTILINE)
62
+ versions = {
63
+ "tag": tag,
64
+ "pyproject.toml": pyproject["project"]["version"],
65
+ "src/lv24_seo/__init__.py": init_version.group(1) if init_version else "missing",
66
+ "server.json": server["version"],
67
+ "server.json packages[0]": server["packages"][0]["version"],
68
+ "packages/npm/package.json": npm["version"],
69
+ }
70
+ if len(set(versions.values())) != 1:
71
+ print("::error::version mismatch: " + ", ".join(f"{key}={value}" for key, value in versions.items()))
72
+ raise SystemExit(1)
73
+ print(f"versions aligned at {tag}")
74
+ PY
75
+
76
+ - name: Build sdist and wheel
77
+ # hatchling is the build backend declared in pyproject.toml.
78
+ # uv.lock must be committed: CI and the local release checks use it for
79
+ # reproducible installs, so the tagged commit has a tested dependency set.
80
+ run: uv build
81
+
82
+ - name: List distributions
83
+ run: ls -lh dist/
84
+
85
+ - name: Upload distributions
86
+ uses: actions/upload-artifact@v7
87
+ with:
88
+ name: dist
89
+ path: dist/
90
+ retention-days: 7
91
+ if-no-files-found: error
92
+
93
+ publish-pypi:
94
+ name: publish to PyPI
95
+ needs: build
96
+ # A manual dispatch is a build-only dry run unless it targets a v* tag.
97
+ if: startsWith(github.ref, 'refs/tags/v')
98
+ runs-on: ubuntu-latest
99
+ timeout-minutes: 10
100
+ environment:
101
+ name: pypi
102
+ url: https://pypi.org/p/lv24-seo
103
+ permissions:
104
+ # Job-level permissions replace the workflow-level block.
105
+ # OIDC only: no PyPI password or API token is stored in this repository.
106
+ id-token: write
107
+ steps:
108
+ - name: Download distributions
109
+ uses: actions/download-artifact@v7
110
+ with:
111
+ name: dist
112
+ path: dist/
113
+
114
+ - name: Publish distributions to PyPI
115
+ # One-time setup on PyPI: project lv24-seo -> Publishing -> add a trusted
116
+ # publisher with owner LevapVeeskela, repository lv24-seo-optimizer,
117
+ # workflow release.yml and environment pypi. Without it this step fails.
118
+ # PyPI files are immutable: a version can never be uploaded twice.
119
+ uses: pypa/gh-action-pypi-publish@release/v1
120
+ with:
121
+ packages-dir: dist/
122
+
123
+ publish-npm:
124
+ name: publish @lv24/seo to npm
125
+ needs: build
126
+ # A manual dispatch is a build-only dry run unless it targets a v* tag.
127
+ if: startsWith(github.ref, 'refs/tags/v')
128
+ runs-on: ubuntu-latest
129
+ timeout-minutes: 10
130
+ permissions:
131
+ contents: read
132
+ # npm provenance attests the tarball to this workflow run via OIDC.
133
+ id-token: write
134
+ defaults:
135
+ run:
136
+ working-directory: packages/npm
137
+ steps:
138
+ - name: Check out the repository
139
+ uses: actions/checkout@v7
140
+ with:
141
+ persist-credentials: false
142
+
143
+ - name: Detect the npm token
144
+ # `secrets` cannot be used in `if:` expressions, so the gate is a step output.
145
+ # Without NPM_TOKEN the job skips instead of failing the whole release.
146
+ id: gate
147
+ env:
148
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
149
+ run: |
150
+ if [ -n "$NPM_TOKEN" ]; then
151
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
152
+ else
153
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
154
+ echo "::notice::NPM_TOKEN is not set — skipping the npm publish."
155
+ fi
156
+
157
+ - name: Install Node
158
+ if: steps.gate.outputs.enabled == 'true'
159
+ uses: actions/setup-node@v7.1.0
160
+ with:
161
+ node-version: "22"
162
+ registry-url: https://registry.npmjs.org
163
+
164
+ - name: Check the npm version matches the tag
165
+ if: steps.gate.outputs.enabled == 'true'
166
+ run: |
167
+ node -e '
168
+ const pkg = require("./package.json");
169
+ const tag = process.env.GITHUB_REF_NAME.replace(/^v/, "");
170
+ if (pkg.version !== tag) {
171
+ console.error(`npm version ${pkg.version} != tag ${tag}`);
172
+ process.exit(1);
173
+ }
174
+ console.log(`npm version aligned at ${tag}`);
175
+ '
176
+
177
+ - name: Publish with provenance
178
+ if: steps.gate.outputs.enabled == 'true'
179
+ env:
180
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
181
+ run: npm publish --access public --provenance
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Optional and disabled: GHCR image for Docker/VPS users (WP10.3).
185
+ # Public container packages are free to store and pull, but building and
186
+ # pushing still consumes Actions minutes, and this repository has no
187
+ # Dockerfile yet. Enable only after the first PyPI release is green, then
188
+ # change `needs` to publish-pypi if the image must follow a successful upload.
189
+ #
190
+ # publish-ghcr:
191
+ # name: publish container to GHCR
192
+ # needs: build
193
+ # if: startsWith(github.ref, 'refs/tags/v')
194
+ # runs-on: ubuntu-latest
195
+ # timeout-minutes: 20
196
+ # permissions:
197
+ # contents: read
198
+ # packages: write
199
+ # steps:
200
+ # - uses: actions/checkout@v7
201
+ # with:
202
+ # persist-credentials: false
203
+ # - uses: docker/login-action@v3
204
+ # with:
205
+ # registry: ghcr.io
206
+ # username: ${{ github.actor }}
207
+ # password: ${{ secrets.GITHUB_TOKEN }}
208
+ # - uses: docker/build-push-action@v6
209
+ # with:
210
+ # context: .
211
+ # push: true
212
+ # tags: ghcr.io/levapveeskela/lv24-seo:${{ github.ref_name }}
@@ -0,0 +1,48 @@
1
+ # Byte-compiled / optimized
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ dist/
7
+ build/
8
+
9
+ # Virtual envs
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Tooling caches
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .mypy_cache/
18
+ .coverage
19
+ htmlcov/
20
+ .uv/
21
+
22
+ # Local data (never commit crawled data, tokens or reports)
23
+ data/
24
+ .env
25
+ .env.*
26
+ !.env.example
27
+ *.sqlite
28
+ *.sqlite3
29
+ *.db
30
+ *.sqlite3-wal
31
+ *.sqlite3-shm
32
+ reports/*.html
33
+ reports/*.json
34
+ !reports/.gitkeep
35
+ logs/
36
+ *.log
37
+
38
+ # Editors / OS
39
+ .idea/
40
+ .vscode/*
41
+ !.vscode/extensions.json
42
+ .DS_Store
43
+ Thumbs.db
44
+ desktop.ini
45
+
46
+ # Node (npm launcher package)
47
+ node_modules/
48
+ packages/npm/package-lock.json
@@ -0,0 +1,16 @@
1
+ # Gitleaks configuration.
2
+ # The default rule set is kept; we only allowlist the documentation template whose values are
3
+ # intentionally empty placeholders (a key *name* without a secret still matches generic-api-key).
4
+
5
+ [extend]
6
+ useDefault = true
7
+
8
+ [allowlist]
9
+ description = "Template files contain empty placeholders only; env var NAMES are not secrets"
10
+ paths = [
11
+ '''^\.env\.example$''',
12
+ '''^tests/''',
13
+ ]
14
+ regexes = [
15
+ '''LV24_SEO_[A-Z0-9_]+''',
16
+ ]
@@ -0,0 +1,91 @@
1
+ # AGENTS.md — contract for AI agents working in this repo
2
+
3
+ This file is the canonical instruction set for any coding agent (Claude Code, OpenCode, Codex,
4
+ Cursor, Gemini, Aider). `CLAUDE.md` is a pointer to this file — never let the two diverge.
5
+
6
+ ## 1. What this project is
7
+
8
+ `lv24-seo-optimizer` is a Python package that ships four surfaces over one core:
9
+
10
+ | Surface | Entry point | Contract |
11
+ |---|---|---|
12
+ | CLI | `src/lv24_seo/cli/` | every command supports `--json`, returns `{ok,data,meta,warnings}`, stable exit codes |
13
+ | MCP server | `src/lv24_seo/mcp/` | 8–12 stable tools, read-only by default, `structuredContent` + text fallback |
14
+ | Skills | `skills/*/SKILL.md` | canonical skill + thin mirrors (`.claude/skills`, `.opencode/skills`) |
15
+ | Automation | `src/lv24_seo/automation/` | cron / systemd / Task Scheduler generators, email + Telegram delivery |
16
+
17
+ Single source of truth for tool metadata: `src/lv24_seo/mcp/registry.py`. CLI help, MCP schemas,
18
+ `schemas/tools.json` and the docs tables are **generated** from it. Never hand-edit generated files.
19
+
20
+ ## 2. Hard rules
21
+
22
+ 1. **Zero spend.** Default path must work on free tiers only. Do not add a dependency that requires a
23
+ paid account, a card on file, or a paid CI runner. Optional paid adapters are allowed only behind
24
+ an explicit provider key and must degrade gracefully.
25
+ 2. **Read-only by default.** Any destructive or quota-consuming action (sitemap submit, recrawl
26
+ request, IndexNow submit) requires `--allow-write` (CLI) / `destructiveHint` + confirm token (MCP),
27
+ plus an idempotency key.
28
+ 3. **Honesty over completeness.** Every bounded response must carry `truncated`, `total`, `data_age`
29
+ and `limitations[]`. Never let an agent claim full coverage when a provider capped the data.
30
+ 4. **No secrets in the repo, logs, fixtures or tests.** Tokens live in the OS keychain, `0600` files
31
+ outside the repo, or `LV24_SEO_*` env vars. Tests use synthetic fixtures only.
32
+ 5. **No scraping of SERPs.** Rank tracking uses provider APIs (GSC, Yandex Webmaster), never HTML
33
+ scraping of Google/Yandex result pages.
34
+ 6. **No vendor resale.** If a paid provider is integrated, the user supplies their own key and the
35
+ data stays local.
36
+ 7. **Windows, macOS and Linux must all work.** No POSIX-only paths in user-facing commands; shell
37
+ snippets in docs come in both bash and PowerShell where relevant.
38
+ 8. **Deterministic output.** Report and CLI output is stable enough to diff between runs (sorted keys,
39
+ ISO-8601 UTC timestamps, no random ids in visible output).
40
+
41
+ ## 3. Repo layout
42
+
43
+ ```
44
+ src/lv24_seo/
45
+ core/ config, errors, envelopes, rate limiting, http, time
46
+ providers/ gsc, yandex_webmaster, yandex_metrika, bing, crux, psi, ga4 (optional)
47
+ crawl/ fetcher, parsers, rules, link graph, log ingest
48
+ store/ sqlite schema + repositories
49
+ report/ builders, renderers (html/text/md), delivery
50
+ mcp/ server, registry (SoT), resources, prompts
51
+ cli/ Typer commands
52
+ automation/ scheduler generators, GitHub Actions recipes
53
+ schemas/ generated JSON schemas (tools.json)
54
+ skills/ canonical SKILL.md files
55
+ docs/ architecture, limits, guides, plans, research
56
+ tests/ unit, contract (recorded fixtures), e2e (network-gated)
57
+ ```
58
+
59
+ ## 4. How to work
60
+
61
+ ```bash
62
+ uv sync # install dev env
63
+ uv run ruff check . # lint
64
+ uv run ruff format --check .
65
+ uv run mypy src # types
66
+ uv run pytest -q # offline tests only
67
+ uv run pytest -q -m live # requires tokens; never in CI
68
+ uv run lv24-seo doctor # environment self-check
69
+ ```
70
+
71
+ Before claiming done:
72
+
73
+ - `ruff check` + `ruff format --check` + `mypy` + `pytest -q` are green.
74
+ - Generated artifacts are regenerated, not edited: `uv run python -m lv24_seo.mcp.registry --write`.
75
+ - Docs updated in the same change (command help, tool table, `docs/*`).
76
+ - No new required dependency without a note in the PR body explaining why and its license.
77
+ - Do not weaken `limitations[]`, truncation reporting or write-gating to make a test pass.
78
+
79
+ ## 5. Change review protocol
80
+
81
+ - Small, focused commits. Subject format: `[SEO](<area>) - <what changed>`, e.g.
82
+ `[SEO](providers) - add yandex recrawl quota guard`.
83
+ - One area per commit: `cli`, `mcp`, `providers`, `crawl`, `report`, `store`, `docs`, `infra`, `tests`.
84
+ - PR body: problem, solution, verification commands, and a note about provider limits touched.
85
+ - Never commit generated fixtures containing real customer data or personal queries.
86
+
87
+ ## 6. Where decisions live
88
+
89
+ - Product and architecture decisions: `docs/decisions/ADR-*.md` (append a new ADR, do not rewrite old ones).
90
+ - Execution plan and wave status: `docs/plans/2026-11-10-implementation-plan.md`.
91
+ - Provider capability matrix: `docs/data-sources.md` — update it in the same PR that touches a provider.
@@ -0,0 +1,54 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning is [SemVer](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ### Added — engine (waves W0–W7)
9
+
10
+ - **Core**: config (`seo.toml` + `LV24_SEO_*` env), secret store (keychain → env → `0600` file),
11
+ HTTP client with IPv4 pinning/retry/`Retry-After`, token-bucket rate limiter, redacting JSON logging,
12
+ time/period helpers, typed error codes + response envelope.
13
+ - **Providers**: Google Search Console (search analytics with pagination, URL inspection, sites,
14
+ sitemaps), Yandex Webmaster (hosts, summary, indexing history, search-urls events, diagnostics,
15
+ popular queries, recrawl quota), Yandex Metrika (counters, goals, stat/v1), Bing Webmaster +
16
+ IndexNow, Chrome UX Report, PageSpeed Insights. Every result carries `truncated`, `total`,
17
+ `data_age_days`, `limitations[]`.
18
+ - **Crawler**: robots.txt (RFC 9309), manual redirects, sitemap reader (index, gzip, 50k/50MB caps),
19
+ stdlib HTML extraction (title/meta/canonical/hreflang/JSON-LD/links/images/word count), internal
20
+ link graph, render-parity hook, and **30 rules** (indexability, content, structured data, i18n,
21
+ mixed content, sitemap membership).
22
+ - **Storage**: SQLite (WAL) with 9 tables, repositories, snapshot hashing, delivery ledger.
23
+ - **Analysis**: period deltas with thresholds, striking-distance/CTR-gap/decay opportunities, indexing
24
+ triage buckets with next actions, GSC↔Yandex↔Metrika fusion with explicit caveats, nginx JSON log
25
+ stats (bots, crawl waste, p95).
26
+ - **Reports**: weekly/indexing/opportunities builders, text/Markdown/HTML renderers (email-safe,
27
+ escaped), SMTP + Telegram delivery with content hashing and idempotent sends, change-gated sends.
28
+ - **CLI**: `version`, `doctor`, `init`, `config show`, `auth status|set|login|logout`, `properties`,
29
+ `gsc query`, `yandex queries|indexing`, `crawl`, `report weekly`, `schedule print|install`,
30
+ `install --client`, `export tools` — every command supports `--json` and stable exit codes.
31
+ - **MCP**: 10 registry-driven tools (8 read + 2 gated write), resources `seo://config`,
32
+ `seo://report/latest`, `seo://docs/tools`, prompts `weekly_seo_review` / `indexing_triage` /
33
+ `content_opportunities`; stdio by default, stateless HTTP optional.
34
+ - **Automation**: cron / systemd user units / Windows Task Scheduler generators with `--print` and
35
+ idempotent install; GitHub Actions and VPS cron recipes.
36
+
37
+ ### Added — docs and repository
38
+
39
+ - Apache-2.0 license, agent contract (`AGENTS.md`), `llms.txt`, CI, issue/PR templates, gitleaks.
40
+ - `lv24_seo.mcp.registry` — canonical tool registry (10 tools) and generated `schemas/tools.json`.
41
+ - Docs: architecture, data sources and quotas, MCP surface, CLI, config, scheduling, skills,
42
+ 3 ADRs, quickstart, GitHub Actions and VPS guides.
43
+ - Research: 2026 landscape (Google/Yandex APIs, existing SEO MCP servers, paid tooling, audit signals).
44
+ - Skills: `seo-weekly`, `seo-indexing-triage`, `seo-technical-audit`.
45
+
46
+ ### Known gaps
47
+
48
+ - GSC service-account auth is documented but not wired (OAuth bearer token only).
49
+ - Write operations exist in adapters but have no CLI/MCP confirm-token flow yet.
50
+ - No live end-to-end run against a real property; PyPI/MCP-registry publishing pending.
51
+
52
+ ### Notes
53
+
54
+ - Pre-release: interfaces may change until `v0.1.0`.
@@ -0,0 +1,4 @@
1
+ # CLAUDE.md
2
+
3
+ The canonical agent contract for this repository is [`AGENTS.md`](AGENTS.md).
4
+ Read it before changing anything; this file intentionally contains no separate rules.