lushly-botcore 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 (114) hide show
  1. lushly_botcore-0.1.0/.gitignore +28 -0
  2. lushly_botcore-0.1.0/PKG-INFO +35 -0
  3. lushly_botcore-0.1.0/pyproject.toml +98 -0
  4. lushly_botcore-0.1.0/src/botcore/__init__.py +46 -0
  5. lushly_botcore-0.1.0/src/botcore/commands/__init__.py +70 -0
  6. lushly_botcore-0.1.0/src/botcore/commands/cdp/__init__.py +104 -0
  7. lushly_botcore-0.1.0/src/botcore/commands/cdp/core.py +256 -0
  8. lushly_botcore-0.1.0/src/botcore/commands/cdp/diagnostics.py +179 -0
  9. lushly_botcore-0.1.0/src/botcore/commands/cdp/forms.py +207 -0
  10. lushly_botcore-0.1.0/src/botcore/commands/cdp/inspection.py +211 -0
  11. lushly_botcore-0.1.0/src/botcore/commands/cdp/interaction.py +300 -0
  12. lushly_botcore-0.1.0/src/botcore/commands/cdp/launcher.py +198 -0
  13. lushly_botcore-0.1.0/src/botcore/commands/cdp/navigation.py +59 -0
  14. lushly_botcore-0.1.0/src/botcore/commands/cdp_perf.py +291 -0
  15. lushly_botcore-0.1.0/src/botcore/commands/dev/__init__.py +48 -0
  16. lushly_botcore-0.1.0/src/botcore/commands/dev/analysis.py +359 -0
  17. lushly_botcore-0.1.0/src/botcore/commands/dev/core.py +153 -0
  18. lushly_botcore-0.1.0/src/botcore/commands/dev/portability.py +206 -0
  19. lushly_botcore-0.1.0/src/botcore/commands/dev/quality.py +331 -0
  20. lushly_botcore-0.1.0/src/botcore/commands/docs.py +328 -0
  21. lushly_botcore-0.1.0/src/botcore/commands/info.py +90 -0
  22. lushly_botcore-0.1.0/src/botcore/commands/research.py +115 -0
  23. lushly_botcore-0.1.0/src/botcore/commands/skill/__init__.py +28 -0
  24. lushly_botcore-0.1.0/src/botcore/commands/skill/_discovery.py +99 -0
  25. lushly_botcore-0.1.0/src/botcore/commands/skill/adopt.py +64 -0
  26. lushly_botcore-0.1.0/src/botcore/commands/skill/frontmatter.py +137 -0
  27. lushly_botcore-0.1.0/src/botcore/commands/skill/index.py +76 -0
  28. lushly_botcore-0.1.0/src/botcore/commands/skill/lint.py +227 -0
  29. lushly_botcore-0.1.0/src/botcore/commands/skill/list.py +63 -0
  30. lushly_botcore-0.1.0/src/botcore/commands/skill/seed.py +141 -0
  31. lushly_botcore-0.1.0/src/botcore/commands/skill/status.py +117 -0
  32. lushly_botcore-0.1.0/src/botcore/commands/spec.py +160 -0
  33. lushly_botcore-0.1.0/src/botcore/commands/undo.py +88 -0
  34. lushly_botcore-0.1.0/src/botcore/config.py +211 -0
  35. lushly_botcore-0.1.0/src/botcore/docs.py +122 -0
  36. lushly_botcore-0.1.0/src/botcore/plugin.py +111 -0
  37. lushly_botcore-0.1.0/src/botcore/registry.py +53 -0
  38. lushly_botcore-0.1.0/src/botcore/server.py +204 -0
  39. lushly_botcore-0.1.0/src/botcore/skills/__init__.py +0 -0
  40. lushly_botcore-0.1.0/src/botcore/skills/accessibility/SKILL.md +106 -0
  41. lushly_botcore-0.1.0/src/botcore/skills/accessibility/references/aria-patterns.md +141 -0
  42. lushly_botcore-0.1.0/src/botcore/skills/accessibility/references/keyboard-nav.md +128 -0
  43. lushly_botcore-0.1.0/src/botcore/skills/accessibility/references/wcag-guidelines.md +65 -0
  44. lushly_botcore-0.1.0/src/botcore/skills/command-manager/README.md +73 -0
  45. lushly_botcore-0.1.0/src/botcore/skills/command-manager/SKILL.md +130 -0
  46. lushly_botcore-0.1.0/src/botcore/skills/command-manager/examples/sample-commands/commit.md +51 -0
  47. lushly_botcore-0.1.0/src/botcore/skills/command-manager/examples/sample-commands/review.md +57 -0
  48. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/commands-specification.md +110 -0
  49. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/context-injection.md +110 -0
  50. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/format.md +69 -0
  51. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/hooks.md +112 -0
  52. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/namespacing.md +87 -0
  53. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/permissions.md +127 -0
  54. lushly_botcore-0.1.0/src/botcore/skills/command-manager/references/variables.md +95 -0
  55. lushly_botcore-0.1.0/src/botcore/skills/command-manager/templates/advanced.template.md +91 -0
  56. lushly_botcore-0.1.0/src/botcore/skills/command-manager/templates/command.template.md +40 -0
  57. lushly_botcore-0.1.0/src/botcore/skills/doc-management/SKILL.md +138 -0
  58. lushly_botcore-0.1.0/src/botcore/skills/doc-management/references/docsify-setup.md +119 -0
  59. lushly_botcore-0.1.0/src/botcore/skills/doc-management/references/index-generation.md +172 -0
  60. lushly_botcore-0.1.0/src/botcore/skills/documentation/SKILL.md +215 -0
  61. lushly_botcore-0.1.0/src/botcore/skills/duplicates/SKILL.md +254 -0
  62. lushly_botcore-0.1.0/src/botcore/skills/humanize-content/SKILL.md +155 -0
  63. lushly_botcore-0.1.0/src/botcore/skills/humanize-content/references/remediation.md +270 -0
  64. lushly_botcore-0.1.0/src/botcore/skills/humanize-content/references/syntax.md +233 -0
  65. lushly_botcore-0.1.0/src/botcore/skills/humanize-content/references/vocabulary.md +184 -0
  66. lushly_botcore-0.1.0/src/botcore/skills/i18n/SKILL.md +173 -0
  67. lushly_botcore-0.1.0/src/botcore/skills/infrastructure/SKILL.md +172 -0
  68. lushly_botcore-0.1.0/src/botcore/skills/licensing/SKILL.md +143 -0
  69. lushly_botcore-0.1.0/src/botcore/skills/modern-css/SKILL.md +88 -0
  70. lushly_botcore-0.1.0/src/botcore/skills/modern-css/references/css-frontier.md +216 -0
  71. lushly_botcore-0.1.0/src/botcore/skills/observability/SKILL.md +141 -0
  72. lushly_botcore-0.1.0/src/botcore/skills/performance/SKILL.md +96 -0
  73. lushly_botcore-0.1.0/src/botcore/skills/performance/references/bundle-optimization.md +116 -0
  74. lushly_botcore-0.1.0/src/botcore/skills/performance/references/core-web-vitals.md +116 -0
  75. lushly_botcore-0.1.0/src/botcore/skills/performance/references/lazy-loading.md +143 -0
  76. lushly_botcore-0.1.0/src/botcore/skills/persona-creator/SKILL.md +99 -0
  77. lushly_botcore-0.1.0/src/botcore/skills/problem-solver/SKILL.md +97 -0
  78. lushly_botcore-0.1.0/src/botcore/skills/problem-solver/references/debugging-strategies.md +123 -0
  79. lushly_botcore-0.1.0/src/botcore/skills/problem-solver/references/error-patterns.md +143 -0
  80. lushly_botcore-0.1.0/src/botcore/skills/project-management/SKILL.md +134 -0
  81. lushly_botcore-0.1.0/src/botcore/skills/project-management/references/cli-commands.md +76 -0
  82. lushly_botcore-0.1.0/src/botcore/skills/project-management/references/feature-lifecycle.md +69 -0
  83. lushly_botcore-0.1.0/src/botcore/skills/project-management/references/issue-templates.md +93 -0
  84. lushly_botcore-0.1.0/src/botcore/skills/project-management/references/spec-review-workflow.md +106 -0
  85. lushly_botcore-0.1.0/src/botcore/skills/project-management/references/status-tracking.md +94 -0
  86. lushly_botcore-0.1.0/src/botcore/skills/project-management/references/wave-orchestration.md +127 -0
  87. lushly_botcore-0.1.0/src/botcore/skills/researcher/SKILL.md +94 -0
  88. lushly_botcore-0.1.0/src/botcore/skills/researcher/references/azure.md +115 -0
  89. lushly_botcore-0.1.0/src/botcore/skills/researcher/references/fast-element.md +68 -0
  90. lushly_botcore-0.1.0/src/botcore/skills/researcher/references/fluent-ui.md +90 -0
  91. lushly_botcore-0.1.0/src/botcore/skills/reviewer/SKILL.md +408 -0
  92. lushly_botcore-0.1.0/src/botcore/skills/security/SKILL.md +327 -0
  93. lushly_botcore-0.1.0/src/botcore/skills/security/references/owasp-top-10.md +14 -0
  94. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/README.md +61 -0
  95. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/SKILL.md +187 -0
  96. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/proposals/v2.1-upgrades.md +362 -0
  97. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/architecture.md +198 -0
  98. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/descriptions.md +162 -0
  99. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/distributed-skills.md +74 -0
  100. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/format.md +160 -0
  101. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/linter-rules.md +254 -0
  102. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/reference-files.md +242 -0
  103. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/routing.md +173 -0
  104. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/references/skills-specification.md +227 -0
  105. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/templates/reference.template.md +41 -0
  106. lushly_botcore-0.1.0/src/botcore/skills/skill-manager/templates/skill.template.md +105 -0
  107. lushly_botcore-0.1.0/src/botcore/skills/spec-writer/SKILL.md +213 -0
  108. lushly_botcore-0.1.0/src/botcore/skills/testing/SKILL.md +98 -0
  109. lushly_botcore-0.1.0/src/botcore/skills/testing/references/mocking.md +175 -0
  110. lushly_botcore-0.1.0/src/botcore/skills/testing/references/playwright-e2e.md +154 -0
  111. lushly_botcore-0.1.0/src/botcore/skills/testing/references/vitest-patterns.md +129 -0
  112. lushly_botcore-0.1.0/src/botcore/utils/__init__.py +25 -0
  113. lushly_botcore-0.1.0/src/botcore/utils/runner.py +93 -0
  114. lushly_botcore-0.1.0/src/botcore/utils/workspace.py +162 -0
@@ -0,0 +1,28 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .venv/
4
+ __pycache__/
5
+ *.egg-info/
6
+
7
+ # Build outputs
8
+ dist/
9
+ build/
10
+ *.tsbuildinfo
11
+ .turbo/
12
+
13
+ # Test / coverage
14
+ coverage/
15
+ .pytest_cache/
16
+ htmlcov/
17
+
18
+ # IDE
19
+ .idea/
20
+ *.sw?
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Environment
27
+ .env
28
+ .env.local
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: lushly-botcore
3
+ Version: 0.1.0
4
+ Summary: Shared bot infrastructure — config, plugin contract, and extracted commands
5
+ Author: Falkicon
6
+ License: MIT
7
+ Keywords: agent,botcore,cli,mcp,plugins
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: afd>=0.1.0
17
+ Requires-Dist: click>=8.1.0
18
+ Requires-Dist: pydantic>=2.5.0
19
+ Requires-Dist: pyyaml>=6.0.0
20
+ Provides-Extra: all
21
+ Requires-Dist: botcore[cdp,mcp,quality,research]; extra == 'all'
22
+ Provides-Extra: cdp
23
+ Requires-Dist: httpx>=0.24.0; extra == 'cdp'
24
+ Requires-Dist: playwright>=1.40.0; extra == 'cdp'
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
30
+ Provides-Extra: mcp
31
+ Requires-Dist: mcp>=1.0.0; extra == 'mcp'
32
+ Provides-Extra: quality
33
+ Requires-Dist: httpx>=0.24.0; extra == 'quality'
34
+ Provides-Extra: research
35
+ Requires-Dist: google-genai>=0.3.0; extra == 'research'
@@ -0,0 +1,98 @@
1
+ [project]
2
+ name = "lushly-botcore"
3
+ version = "0.1.0"
4
+ description = "Shared bot infrastructure — config, plugin contract, and extracted commands"
5
+ requires-python = ">=3.11"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "Falkicon" }]
8
+ keywords = ["botcore", "cli", "mcp", "plugins", "agent"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ ]
18
+
19
+ dependencies = [
20
+ "afd>=0.1.0",
21
+ "pydantic>=2.5.0",
22
+ "click>=8.1.0",
23
+ "pyyaml>=6.0.0",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest>=8.0.0",
29
+ "pytest-asyncio>=0.23.0",
30
+ "pytest-cov>=4.0.0",
31
+ "ruff>=0.4.0",
32
+ ]
33
+ cdp = [
34
+ "playwright>=1.40.0",
35
+ "httpx>=0.24.0",
36
+ ]
37
+ research = [
38
+ "google-genai>=0.3.0",
39
+ ]
40
+ quality = [
41
+ "httpx>=0.24.0",
42
+ ]
43
+ mcp = [
44
+ "mcp>=1.0.0",
45
+ ]
46
+ all = [
47
+ "botcore[cdp,research,quality,mcp]",
48
+ ]
49
+
50
+ [project.entry-points."botcore.plugins"]
51
+ # Plugins register here, e.g.:
52
+ # lushbot = "lushbot_plugin:LushbotPlugin"
53
+
54
+ [build-system]
55
+ requires = ["hatchling"]
56
+ build-backend = "hatchling.build"
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ packages = ["src/botcore"]
60
+
61
+ [tool.hatch.build]
62
+ include = ["src/botcore/**/*.py", "src/botcore/skills/**/*.md"]
63
+
64
+ [tool.ruff]
65
+ line-length = 100
66
+ target-version = "py311"
67
+
68
+ [tool.ruff.lint]
69
+ select = [
70
+ "E", "F", "I", "UP",
71
+ "C901",
72
+ "PLR0912", "PLR0915", "PLR0913",
73
+ ]
74
+
75
+ [tool.ruff.lint.mccabe]
76
+ max-complexity = 20
77
+
78
+ [tool.ruff.lint.pylint]
79
+ max-args = 8
80
+ max-branches = 20
81
+ max-statements = 80
82
+
83
+ [tool.pytest.ini_options]
84
+ asyncio_mode = "auto"
85
+ testpaths = ["tests"]
86
+
87
+ [tool.coverage.run]
88
+ source = ["src/botcore"]
89
+ branch = true
90
+ omit = ["*/tests/*", "*/__pycache__/*"]
91
+
92
+ [tool.coverage.report]
93
+ show_missing = true
94
+ exclude_lines = [
95
+ "pragma: no cover",
96
+ "if TYPE_CHECKING:",
97
+ "raise NotImplementedError",
98
+ ]
@@ -0,0 +1,46 @@
1
+ """Botcore — shared bot infrastructure for config, plugins, and commands."""
2
+
3
+ from botcore.config import (
4
+ BotCoreConfig,
5
+ EnvConfig,
6
+ PackageOverrideConfig,
7
+ SkillsConfig,
8
+ get_config_for_path,
9
+ load_config,
10
+ )
11
+ from botcore.plugin import (
12
+ BotCorePlugin,
13
+ PluginRegistry,
14
+ discover_plugins,
15
+ )
16
+ from botcore.registry import (
17
+ get_client,
18
+ registry,
19
+ reset_client,
20
+ )
21
+ from botcore.server import build_docs, build_namespace, create_mcp_server
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "__version__",
27
+ # Config
28
+ "BotCoreConfig",
29
+ "SkillsConfig",
30
+ "PackageOverrideConfig",
31
+ "EnvConfig",
32
+ "load_config",
33
+ "get_config_for_path",
34
+ # Plugin
35
+ "PluginRegistry",
36
+ "BotCorePlugin",
37
+ "discover_plugins",
38
+ # Registry
39
+ "registry",
40
+ "get_client",
41
+ "reset_client",
42
+ # Server
43
+ "build_namespace",
44
+ "build_docs",
45
+ "create_mcp_server",
46
+ ]
@@ -0,0 +1,70 @@
1
+ """Botcore command re-exports."""
2
+
3
+ # Dev commands re-exported from subpackage
4
+ from botcore.commands.dev import (
5
+ dev_build,
6
+ dev_check_coverage,
7
+ dev_check_deps,
8
+ dev_check_paths,
9
+ dev_check_size,
10
+ dev_circular_imports,
11
+ dev_dead_code,
12
+ dev_dep_graph,
13
+ dev_lint,
14
+ dev_skill_lint,
15
+ dev_test,
16
+ dev_unused_deps,
17
+ )
18
+ from botcore.commands.docs import docs_check_agents, docs_check_changelog, docs_lint
19
+ from botcore.commands.info import info_env, info_scripts, info_workspace
20
+ from botcore.commands.research import research_query
21
+ from botcore.commands.skill import (
22
+ skill_adopt,
23
+ skill_index,
24
+ skill_lint,
25
+ skill_list,
26
+ skill_seed,
27
+ skill_status,
28
+ )
29
+ from botcore.commands.spec import spec_create, spec_status, spec_validate
30
+ from botcore.commands.undo import undo_clear, undo_status
31
+
32
+ __all__ = [
33
+ # Info
34
+ "info_workspace",
35
+ "info_env",
36
+ "info_scripts",
37
+ # Dev
38
+ "dev_lint",
39
+ "dev_test",
40
+ "dev_build",
41
+ "dev_skill_lint",
42
+ "dev_check_size",
43
+ "dev_check_coverage",
44
+ "dev_check_deps",
45
+ "dev_dead_code",
46
+ "dev_circular_imports",
47
+ "dev_unused_deps",
48
+ "dev_dep_graph",
49
+ "dev_check_paths",
50
+ # Skill registry
51
+ "skill_seed",
52
+ "skill_list",
53
+ "skill_status",
54
+ "skill_lint",
55
+ "skill_adopt",
56
+ "skill_index",
57
+ # Docs
58
+ "docs_lint",
59
+ "docs_check_changelog",
60
+ "docs_check_agents",
61
+ # Research
62
+ "research_query",
63
+ # Spec
64
+ "spec_create",
65
+ "spec_status",
66
+ "spec_validate",
67
+ # Undo
68
+ "undo_status",
69
+ "undo_clear",
70
+ ]
@@ -0,0 +1,104 @@
1
+ """Botcore CDP commands — Chrome debug utilities.
2
+
3
+ Modules:
4
+ - core.py: Shared utilities, session management, constants
5
+ - launcher.py: Browser launch, attach, close
6
+ - navigation.py: Navigate, wait
7
+ - interaction.py: Click, hover, scroll, drag, type, press
8
+ - inspection.py: Inspect, query, snapshot
9
+ - forms.py: Fill, upload, dialogs, page management
10
+ - diagnostics.py: Screenshot, console, network, eval, emulate
11
+ """
12
+
13
+ from botcore.commands.cdp.core import (
14
+ DEEP_QUERY_SINGLE_JS,
15
+ DEFAULT_TIMEOUT_MS,
16
+ CdpSession,
17
+ ConsoleEntry,
18
+ )
19
+ from botcore.commands.cdp.diagnostics import (
20
+ cdp_console,
21
+ cdp_emulate,
22
+ cdp_eval,
23
+ cdp_get_console_message,
24
+ cdp_get_network,
25
+ cdp_list_network,
26
+ cdp_screenshot,
27
+ )
28
+ from botcore.commands.cdp.forms import (
29
+ cdp_close_page,
30
+ cdp_fill,
31
+ cdp_fill_form,
32
+ cdp_handle_dialog,
33
+ cdp_list_pages,
34
+ cdp_new_page,
35
+ cdp_resize,
36
+ cdp_select_page,
37
+ cdp_upload,
38
+ )
39
+ from botcore.commands.cdp.inspection import (
40
+ cdp_inspect,
41
+ cdp_query,
42
+ cdp_snapshot,
43
+ )
44
+ from botcore.commands.cdp.interaction import (
45
+ cdp_click,
46
+ cdp_drag,
47
+ cdp_hover,
48
+ cdp_press,
49
+ cdp_scroll,
50
+ cdp_type,
51
+ )
52
+ from botcore.commands.cdp.launcher import (
53
+ cdp_attach,
54
+ cdp_close,
55
+ cdp_launch,
56
+ )
57
+ from botcore.commands.cdp.navigation import (
58
+ cdp_navigate,
59
+ cdp_wait,
60
+ )
61
+
62
+ __all__ = [
63
+ # Core
64
+ "CdpSession",
65
+ "ConsoleEntry",
66
+ "DEFAULT_TIMEOUT_MS",
67
+ "DEEP_QUERY_SINGLE_JS",
68
+ # Launcher
69
+ "cdp_launch",
70
+ "cdp_attach",
71
+ "cdp_close",
72
+ # Navigation
73
+ "cdp_navigate",
74
+ "cdp_wait",
75
+ # Interaction
76
+ "cdp_click",
77
+ "cdp_hover",
78
+ "cdp_scroll",
79
+ "cdp_drag",
80
+ "cdp_type",
81
+ "cdp_press",
82
+ # Inspection
83
+ "cdp_inspect",
84
+ "cdp_query",
85
+ "cdp_snapshot",
86
+ # Forms
87
+ "cdp_fill",
88
+ "cdp_fill_form",
89
+ "cdp_upload",
90
+ "cdp_handle_dialog",
91
+ "cdp_list_pages",
92
+ "cdp_select_page",
93
+ "cdp_new_page",
94
+ "cdp_close_page",
95
+ "cdp_resize",
96
+ # Diagnostics
97
+ "cdp_screenshot",
98
+ "cdp_console",
99
+ "cdp_get_console_message",
100
+ "cdp_eval",
101
+ "cdp_list_network",
102
+ "cdp_get_network",
103
+ "cdp_emulate",
104
+ ]
@@ -0,0 +1,256 @@
1
+ """CDP core utilities — shared across all CDP modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import socket
9
+ import subprocess
10
+ from collections.abc import Awaitable, Callable
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any, TypeVar
14
+
15
+ from afd import CommandResult, error, success
16
+ from pydantic import BaseModel, Field
17
+
18
+ from botcore.utils.workspace import find_workspace
19
+
20
+ T = TypeVar("T")
21
+
22
+ SESSION_DIRNAME = ".botcore"
23
+ SESSION_FILENAME = "cdp-session.json"
24
+ PROFILE_DIRNAME = "chrome-profile"
25
+ SCREENSHOTS_DIRNAME = "screenshots"
26
+ DEFAULT_TIMEOUT_MS = 30_000
27
+
28
+ DEEP_QUERY_SINGLE_JS = """
29
+ (selector) => {
30
+ function querySelectorDeep(root, sel) {
31
+ let result = root.querySelector(sel);
32
+ if (result) return result;
33
+ const elements = root.querySelectorAll('*');
34
+ for (const el of elements) {
35
+ if (el.shadowRoot) {
36
+ result = querySelectorDeep(el.shadowRoot, sel);
37
+ if (result) return result;
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+ return querySelectorDeep(document, selector);
43
+ }
44
+ """
45
+
46
+
47
+ class ConsoleEntry(BaseModel):
48
+ """Captured console log entry."""
49
+
50
+ timestamp: str
51
+ level: str
52
+ text: str
53
+ url: str | None = None
54
+
55
+
56
+ class CdpSession(BaseModel):
57
+ """CDP session metadata stored on disk."""
58
+
59
+ cdp_endpoint: str
60
+ profile_dir: str
61
+ launched_at: str
62
+ pid: int | None = None
63
+ console_log: list[ConsoleEntry] = Field(default_factory=list)
64
+
65
+
66
+ def _now_iso() -> str:
67
+ return datetime.now(UTC).isoformat()
68
+
69
+
70
+ def _session_root() -> Path:
71
+ return find_workspace() or Path.cwd()
72
+
73
+
74
+ def _session_dir(root: Path) -> Path:
75
+ return root / SESSION_DIRNAME
76
+
77
+
78
+ def _session_file(root: Path) -> Path:
79
+ return _session_dir(root) / SESSION_FILENAME
80
+
81
+
82
+ def _ensure_session_dir(root: Path) -> Path:
83
+ session_dir = _session_dir(root)
84
+ session_dir.mkdir(parents=True, exist_ok=True)
85
+ return session_dir
86
+
87
+
88
+ def _default_profile_dir(root: Path) -> Path:
89
+ root_drive = root.drive.rstrip("\\").lower()
90
+ localapp_drive = os.getenv("LOCALAPPDATA", "").split(":")[0].lower()
91
+ if root_drive and localapp_drive and root_drive != localapp_drive:
92
+ profile_root = Path(os.getenv("LOCALAPPDATA", "")) / "botcore"
93
+ profile_root.mkdir(parents=True, exist_ok=True)
94
+ return profile_root / PROFILE_DIRNAME
95
+ return _ensure_session_dir(root) / PROFILE_DIRNAME
96
+
97
+
98
+ def _default_screenshots_dir(root: Path) -> Path:
99
+ return _ensure_session_dir(root) / SCREENSHOTS_DIRNAME
100
+
101
+
102
+ def _load_session(root: Path) -> CdpSession | None:
103
+ session_path = _session_file(root)
104
+ if not session_path.exists():
105
+ return None
106
+ try:
107
+ data = json.loads(session_path.read_text(encoding="utf-8"))
108
+ return CdpSession(**data)
109
+ except Exception:
110
+ return None
111
+
112
+
113
+ def _save_session(root: Path, session: CdpSession) -> None:
114
+ session_path = _session_file(root)
115
+ _ensure_session_dir(root)
116
+ session_path.write_text(session.model_dump_json(indent=2), encoding="utf-8")
117
+
118
+
119
+ def _clear_session(root: Path) -> None:
120
+ session_path = _session_file(root)
121
+ if session_path.exists():
122
+ session_path.unlink()
123
+
124
+
125
+ def _get_free_port() -> int:
126
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
127
+ sock.bind(("127.0.0.1", 0))
128
+ return sock.getsockname()[1]
129
+
130
+
131
+ async def _probe_cdp(
132
+ endpoint: str,
133
+ timeout_s: float = 5.0,
134
+ attempts: int = 20,
135
+ delay_s: float = 0.5,
136
+ ) -> None:
137
+ """Probe CDP endpoint until it responds."""
138
+ import httpx
139
+
140
+ url = f"{endpoint.rstrip('/')}/json/version"
141
+ last_exc: Exception | None = None
142
+ for attempt in range(attempts):
143
+ try:
144
+ async with httpx.AsyncClient(timeout=timeout_s) as client:
145
+ response = await client.get(url)
146
+ response.raise_for_status()
147
+ return
148
+ except Exception as exc:
149
+ last_exc = exc
150
+ if attempt < attempts - 1:
151
+ await asyncio.sleep(delay_s)
152
+ if last_exc:
153
+ raise last_exc
154
+
155
+
156
+ def _spawn_chrome(args: list[str]) -> subprocess.Popen[bytes]:
157
+ popen_kwargs: dict[str, Any] = {
158
+ "stdout": subprocess.DEVNULL,
159
+ "stderr": subprocess.DEVNULL,
160
+ }
161
+ if os.name == "nt":
162
+ popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
163
+ else:
164
+ popen_kwargs["start_new_session"] = True
165
+ return subprocess.Popen(args, **popen_kwargs)
166
+
167
+
168
+ async def _get_chromium_executable() -> str:
169
+ from playwright.async_api import async_playwright
170
+
171
+ async with async_playwright() as playwright:
172
+ return playwright.chromium.executable_path
173
+
174
+
175
+ async def _connect_over_cdp_with_retry(
176
+ playwright: Any,
177
+ endpoint: str,
178
+ attempts: int = 10,
179
+ delay_s: float = 0.3,
180
+ ) -> Any:
181
+ last_exc: Exception | None = None
182
+ for attempt in range(attempts):
183
+ try:
184
+ browser = await playwright.chromium.connect_over_cdp(endpoint)
185
+ return browser
186
+ except Exception as exc:
187
+ last_exc = exc
188
+ if attempt < attempts - 1:
189
+ await asyncio.sleep(delay_s)
190
+ if last_exc:
191
+ raise last_exc
192
+ raise RuntimeError("Failed to connect to CDP endpoint.")
193
+
194
+
195
+ async def _with_session_page(
196
+ action: Callable[[Any], Awaitable[T]],
197
+ timeout_ms: int = DEFAULT_TIMEOUT_MS,
198
+ ) -> CommandResult[T]:
199
+ """Connect to active CDP session and run action on current page."""
200
+ from playwright.async_api import async_playwright
201
+
202
+ root = _session_root()
203
+ session = _load_session(root)
204
+ if not session:
205
+ return error(
206
+ "CDP_SESSION_NOT_FOUND",
207
+ "No active CDP session found.",
208
+ suggestion="Run `botcore cdp launch` or `botcore cdp attach` first.",
209
+ )
210
+
211
+ console_entries: list[ConsoleEntry] = []
212
+
213
+ def handle_console(msg: Any) -> None:
214
+ console_entries.append(
215
+ ConsoleEntry(
216
+ timestamp=_now_iso(),
217
+ level=msg.type,
218
+ text=msg.text,
219
+ url=msg.location.get("url") if msg.location else None,
220
+ )
221
+ )
222
+
223
+ try:
224
+ async with async_playwright() as playwright:
225
+ browser = await _connect_over_cdp_with_retry(playwright, session.cdp_endpoint)
226
+ context = browser.contexts[0] if browser.contexts else await browser.new_context()
227
+ page = context.pages[0] if context.pages else await context.new_page()
228
+ page.on("console", handle_console)
229
+ result = await action(page)
230
+ if console_entries:
231
+ session.console_log.extend(console_entries)
232
+ _save_session(root, session)
233
+ return success(data=result)
234
+ except Exception as exc:
235
+ import httpx
236
+
237
+ if isinstance(exc, httpx.HTTPError):
238
+ return error(
239
+ "CDP_CONNECT_FAILED",
240
+ f"Failed to reach CDP endpoint: {exc}",
241
+ suggestion="Ensure Chrome is running or run `botcore cdp launch`.",
242
+ )
243
+ exc_str = str(exc)
244
+ if "Timeout" in exc_str and "Locator" in exc_str:
245
+ suggestion = "Element not found. Use `botcore cdp query` to verify selector."
246
+ elif "Timeout" in exc_str:
247
+ suggestion = "Operation timed out. Check if page is responsive."
248
+ elif "Target closed" in exc_str or "Connection" in exc_str:
249
+ suggestion = "Browser disconnected. Run `botcore cdp launch` to start a new session."
250
+ else:
251
+ suggestion = "Check selector and page state. Use `botcore cdp screenshot` to debug."
252
+ return error(
253
+ "CDP_COMMAND_FAILED",
254
+ f"{type(exc).__name__}: {exc}",
255
+ suggestion=suggestion,
256
+ )