crewly 1.1.2 → 1.2.0

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 (216) hide show
  1. package/README.md +6 -6
  2. package/config/roles/ops/prompt.md +140 -0
  3. package/config/roles/ops/role.json +13 -0
  4. package/config/skills/agent/browse-stealth/execute.sh +84 -0
  5. package/config/skills/agent/browse-stealth/instructions.md +108 -0
  6. package/config/skills/agent/browse-stealth/launch-chrome-cdp.sh +141 -0
  7. package/config/skills/agent/browse-stealth/skill.json +20 -0
  8. package/config/skills/agent/browse-stealth/stealth-browse.py +330 -0
  9. package/config/skills/agent/competitor-content-tracker/execute.sh +232 -0
  10. package/config/skills/agent/competitor-content-tracker/instructions.md +210 -0
  11. package/config/skills/agent/competitor-content-tracker/skill.json +22 -0
  12. package/config/skills/agent/content-calendar/execute.sh +294 -0
  13. package/config/skills/agent/content-calendar/instructions.md +122 -0
  14. package/config/skills/agent/content-calendar/skill.json +22 -0
  15. package/config/skills/agent/content-repurposer/execute.sh +194 -0
  16. package/config/skills/agent/content-repurposer/instructions.md +69 -0
  17. package/config/skills/agent/content-repurposer/skill.json +22 -0
  18. package/config/skills/agent/content-writer/execute.sh +311 -0
  19. package/config/skills/agent/content-writer/instructions.md +124 -0
  20. package/config/skills/agent/content-writer/skill.json +22 -0
  21. package/config/skills/agent/core/generate-pdf/execute.sh +88 -0
  22. package/config/skills/agent/core/generate-pdf/instructions.md +46 -0
  23. package/config/skills/agent/core/generate-pdf/skill.json +20 -0
  24. package/config/skills/agent/core/report-status/execute.sh +6 -0
  25. package/config/skills/agent/trend-monitor/execute.sh +211 -0
  26. package/config/skills/agent/trend-monitor/instructions.md +207 -0
  27. package/config/skills/agent/trend-monitor/skill.json +22 -0
  28. package/config/skills/agent/vnc-browser/execute.sh +261 -0
  29. package/config/skills/agent/vnc-browser/instructions.md +102 -0
  30. package/config/skills/agent/vnc-browser/skill.json +20 -0
  31. package/config/skills/orchestrator/delegate-task/execute.sh +63 -4
  32. package/config/skills/orchestrator/delegate-task/instructions.md +60 -0
  33. package/config/skills/orchestrator/delegate-task/skill.json +4 -4
  34. package/config/skills/orchestrator/reply-slack/execute.sh +2 -0
  35. package/config/skills/orchestrator/send-key/execute.sh +19 -6
  36. package/config/skills/orchestrator/send-key/instructions.md +44 -0
  37. package/config/skills/orchestrator/send-key/skill.json +20 -0
  38. package/config/skills/orchestrator/send-message/execute.sh +9 -1
  39. package/config/skills/registry.json +256 -0
  40. package/config/templates/code-review-team/README.md +176 -0
  41. package/config/templates/code-review-team/team-config.json +16 -0
  42. package/config/templates/code-review-team.json +62 -0
  43. package/config/templates/content-generation-team/README.md +128 -0
  44. package/config/templates/content-generation-team/team-config.json +21 -0
  45. package/config/templates/content-generation-team.json +67 -0
  46. package/config/templates/demo-team.json +22 -0
  47. package/config/templates/social-media-ops-team/README.md +145 -0
  48. package/config/templates/social-media-ops-team/team-config.json +21 -0
  49. package/config/templates/social-media-ops-team.json +67 -0
  50. package/dist/backend/backend/src/constants.d.ts +69 -6
  51. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  52. package/dist/backend/backend/src/constants.js +75 -6
  53. package/dist/backend/backend/src/constants.js.map +1 -1
  54. package/dist/backend/backend/src/controllers/index.d.ts.map +1 -1
  55. package/dist/backend/backend/src/controllers/index.js +2 -0
  56. package/dist/backend/backend/src/controllers/index.js.map +1 -1
  57. package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts +8 -0
  58. package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts.map +1 -1
  59. package/dist/backend/backend/src/controllers/messaging/messenger.routes.js +110 -63
  60. package/dist/backend/backend/src/controllers/messaging/messenger.routes.js.map +1 -1
  61. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
  62. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +31 -4
  63. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
  64. package/dist/backend/backend/src/controllers/oauth/oauth.routes.d.ts +8 -0
  65. package/dist/backend/backend/src/controllers/oauth/oauth.routes.d.ts.map +1 -1
  66. package/dist/backend/backend/src/controllers/oauth/oauth.routes.js +127 -111
  67. package/dist/backend/backend/src/controllers/oauth/oauth.routes.js.map +1 -1
  68. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +34 -0
  69. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
  70. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +219 -2
  71. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
  72. package/dist/backend/backend/src/controllers/user/user.routes.d.ts +7 -0
  73. package/dist/backend/backend/src/controllers/user/user.routes.d.ts.map +1 -1
  74. package/dist/backend/backend/src/controllers/user/user.routes.js +45 -38
  75. package/dist/backend/backend/src/controllers/user/user.routes.js.map +1 -1
  76. package/dist/backend/backend/src/controllers/whatsapp/index.d.ts +17 -0
  77. package/dist/backend/backend/src/controllers/whatsapp/index.d.ts.map +1 -0
  78. package/dist/backend/backend/src/controllers/whatsapp/index.js +18 -0
  79. package/dist/backend/backend/src/controllers/whatsapp/index.js.map +1 -0
  80. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts +12 -0
  81. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -0
  82. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +185 -0
  83. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -0
  84. package/dist/backend/backend/src/index.d.ts +5 -0
  85. package/dist/backend/backend/src/index.d.ts.map +1 -1
  86. package/dist/backend/backend/src/index.js +35 -0
  87. package/dist/backend/backend/src/index.js.map +1 -1
  88. package/dist/backend/backend/src/routes/modules/task-management.routes.d.ts.map +1 -1
  89. package/dist/backend/backend/src/routes/modules/task-management.routes.js +4 -0
  90. package/dist/backend/backend/src/routes/modules/task-management.routes.js.map +1 -1
  91. package/dist/backend/backend/src/services/agent/agent-heartbeat.service.js +1 -1
  92. package/dist/backend/backend/src/services/agent/agent-heartbeat.service.js.map +1 -1
  93. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +14 -3
  94. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/agent/agent-registration.service.js +160 -29
  96. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  97. package/dist/backend/backend/src/services/agent/claude-runtime.service.d.ts +4 -3
  98. package/dist/backend/backend/src/services/agent/claude-runtime.service.d.ts.map +1 -1
  99. package/dist/backend/backend/src/services/agent/claude-runtime.service.js +29 -4
  100. package/dist/backend/backend/src/services/agent/claude-runtime.service.js.map +1 -1
  101. package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/agent/context-window-monitor.service.js +11 -0
  103. package/dist/backend/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
  104. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +32 -2
  105. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
  106. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +69 -8
  107. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  108. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.d.ts.map +1 -1
  109. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.js +14 -2
  110. package/dist/backend/backend/src/services/knowledge/knowledge-search.service.js.map +1 -1
  111. package/dist/backend/backend/src/services/marketplace/marketplace-installer.service.d.ts.map +1 -1
  112. package/dist/backend/backend/src/services/marketplace/marketplace-installer.service.js +11 -2
  113. package/dist/backend/backend/src/services/marketplace/marketplace-installer.service.js.map +1 -1
  114. package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.d.ts +18 -0
  115. package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.d.ts.map +1 -1
  116. package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.js +28 -4
  117. package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.js.map +1 -1
  118. package/dist/backend/backend/src/services/messaging/adapters/slack-messenger.adapter.js +2 -2
  119. package/dist/backend/backend/src/services/messaging/adapters/slack-messenger.adapter.js.map +1 -1
  120. package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.d.ts +18 -0
  121. package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.d.ts.map +1 -1
  122. package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.js +26 -4
  123. package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.js.map +1 -1
  124. package/dist/backend/backend/src/services/messaging/messenger-adapter.interface.d.ts +28 -2
  125. package/dist/backend/backend/src/services/messaging/messenger-adapter.interface.d.ts.map +1 -1
  126. package/dist/backend/backend/src/services/messaging/messenger-registry.service.d.ts +33 -2
  127. package/dist/backend/backend/src/services/messaging/messenger-registry.service.d.ts.map +1 -1
  128. package/dist/backend/backend/src/services/messaging/messenger-registry.service.js +33 -0
  129. package/dist/backend/backend/src/services/messaging/messenger-registry.service.js.map +1 -1
  130. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
  131. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +4 -2
  132. package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
  133. package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.d.ts.map +1 -1
  134. package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.js +4 -3
  135. package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.js.map +1 -1
  136. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +27 -0
  137. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +1 -1
  138. package/dist/backend/backend/src/services/project/task-tracking.service.js +54 -0
  139. package/dist/backend/backend/src/services/project/task-tracking.service.js.map +1 -1
  140. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +36 -6
  141. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  142. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +238 -36
  143. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  144. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  145. package/dist/backend/backend/src/services/slack/slack.service.js +6 -4
  146. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  147. package/dist/backend/backend/src/services/user/user-identity.service.d.ts +44 -0
  148. package/dist/backend/backend/src/services/user/user-identity.service.d.ts.map +1 -1
  149. package/dist/backend/backend/src/services/user/user-identity.service.js +75 -8
  150. package/dist/backend/backend/src/services/user/user-identity.service.js.map +1 -1
  151. package/dist/backend/backend/src/services/whatsapp/index.d.ts +11 -0
  152. package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -0
  153. package/dist/backend/backend/src/services/whatsapp/index.js +11 -0
  154. package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -0
  155. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +66 -0
  156. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -0
  157. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +96 -0
  158. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -0
  159. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts +109 -0
  160. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -0
  161. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +234 -0
  162. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -0
  163. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +127 -0
  164. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -0
  165. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +347 -0
  166. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -0
  167. package/dist/backend/backend/src/services/workflow/scheduler.service.d.ts.map +1 -1
  168. package/dist/backend/backend/src/services/workflow/scheduler.service.js +4 -0
  169. package/dist/backend/backend/src/services/workflow/scheduler.service.js.map +1 -1
  170. package/dist/backend/backend/src/types/index.d.ts +1 -0
  171. package/dist/backend/backend/src/types/index.d.ts.map +1 -1
  172. package/dist/backend/backend/src/types/index.js.map +1 -1
  173. package/dist/backend/backend/src/types/slack.types.d.ts +24 -0
  174. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  175. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  176. package/dist/backend/backend/src/types/task-tracking.types.d.ts +4 -0
  177. package/dist/backend/backend/src/types/task-tracking.types.d.ts.map +1 -1
  178. package/dist/backend/backend/src/types/task-tracking.types.js.map +1 -1
  179. package/dist/backend/backend/src/types/whatsapp.types.d.ts +84 -0
  180. package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -0
  181. package/dist/backend/backend/src/types/whatsapp.types.js +33 -0
  182. package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -0
  183. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts +11 -0
  184. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
  185. package/dist/backend/backend/src/websocket/terminal.gateway.js +35 -1
  186. package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
  187. package/dist/cli/backend/src/constants.d.ts +69 -6
  188. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  189. package/dist/cli/backend/src/constants.js +75 -6
  190. package/dist/cli/backend/src/constants.js.map +1 -1
  191. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.d.ts.map +1 -1
  192. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js +14 -2
  193. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js.map +1 -1
  194. package/dist/cli/backend/src/types/index.d.ts +1 -0
  195. package/dist/cli/backend/src/types/index.d.ts.map +1 -1
  196. package/dist/cli/backend/src/types/index.js.map +1 -1
  197. package/dist/cli/cli/src/commands/publish.d.ts.map +1 -1
  198. package/dist/cli/cli/src/commands/publish.js +17 -15
  199. package/dist/cli/cli/src/commands/publish.js.map +1 -1
  200. package/dist/cli/cli/src/index.js +2 -2
  201. package/dist/cli/cli/src/index.js.map +1 -1
  202. package/dist/cli/cli/src/utils/gh-submit.d.ts +46 -0
  203. package/dist/cli/cli/src/utils/gh-submit.d.ts.map +1 -0
  204. package/dist/cli/cli/src/utils/gh-submit.js +167 -0
  205. package/dist/cli/cli/src/utils/gh-submit.js.map +1 -0
  206. package/dist/cli/cli/src/utils/marketplace.d.ts.map +1 -1
  207. package/dist/cli/cli/src/utils/marketplace.js +13 -5
  208. package/dist/cli/cli/src/utils/marketplace.js.map +1 -1
  209. package/dist/cli/cli/src/utils/templates.d.ts +3 -2
  210. package/dist/cli/cli/src/utils/templates.d.ts.map +1 -1
  211. package/dist/cli/cli/src/utils/templates.js +5 -4
  212. package/dist/cli/cli/src/utils/templates.js.map +1 -1
  213. package/frontend/dist/assets/{index-45eeea99.js → index-a23214ae.js} +241 -241
  214. package/frontend/dist/assets/{index-6972eeee.css → index-c407fe13.css} +1 -1
  215. package/frontend/dist/index.html +2 -2
  216. package/package.json +3 -1
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Stealth browser automation using Patchright + Chrome CDP.
4
+
5
+ Connects to a REAL running Chrome browser via CDP (Chrome DevTools Protocol)
6
+ instead of launching a new Chromium instance. This avoids anti-detection
7
+ triggers like navigator.webdriver, headless fingerprints, and missing
8
+ browser history/extensions.
9
+
10
+ CRITICAL CDP MODE RULES (violations cause silent failures):
11
+ - DO NOT call new_context() → breaks DNS/TLS, causes ERR_CONNECTION_CLOSED
12
+ - DO NOT call add_init_script() → conflicts with Chrome's initialization flow
13
+ - DO NOT override User-Agent → creates HTTP/JS inconsistency
14
+ - DO inject cookies via add_cookies() on the existing context
15
+ """
16
+
17
+ import argparse
18
+ import http.client
19
+ import json
20
+ import os
21
+ import random
22
+ import subprocess
23
+ import sys
24
+ import time
25
+
26
+ # Bypass macOS proxy tools (ClashX, Surge) that hijack urllib
27
+ os.environ["no_proxy"] = "localhost,127.0.0.1"
28
+
29
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
30
+ CDP_PORT = 9222
31
+ SCREENSHOT_DIR = os.path.expanduser("~/.crewly/screenshots")
32
+
33
+
34
+ # ── CDP connection helpers ──────────────────────────────────────
35
+
36
+ def get_ws_url(port: int = CDP_PORT) -> str:
37
+ """
38
+ Manually fetch the WebSocket URL from Chrome's CDP endpoint.
39
+
40
+ Uses http.client.HTTPConnection (NOT urllib) to avoid macOS proxy
41
+ tool interference. Note: NO trailing slash on /json/version
42
+ (Patchright 1.58 + Chrome 144 trailing slash bug).
43
+ """
44
+ conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
45
+ try:
46
+ conn.request("GET", "/json/version")
47
+ resp = conn.getresponse()
48
+ if resp.status != 200:
49
+ raise ConnectionError(f"CDP returned HTTP {resp.status}")
50
+ data = json.loads(resp.read().decode())
51
+ return data["webSocketDebuggerUrl"]
52
+ finally:
53
+ conn.close()
54
+
55
+
56
+ def ensure_chrome_running() -> str:
57
+ """Launch Chrome via the launcher script if CDP is not responding."""
58
+ try:
59
+ return get_ws_url()
60
+ except Exception:
61
+ pass
62
+
63
+ launcher = os.path.join(SCRIPT_DIR, "launch-chrome-cdp.sh")
64
+ result = subprocess.run(
65
+ ["bash", launcher, str(CDP_PORT)],
66
+ capture_output=True, text=True, timeout=30
67
+ )
68
+ if result.returncode != 0:
69
+ raise RuntimeError(f"Failed to launch Chrome: {result.stderr}")
70
+
71
+ ws_url = result.stdout.strip().split("\n")[-1]
72
+ if ws_url.startswith("ws://"):
73
+ return ws_url
74
+
75
+ return get_ws_url()
76
+
77
+
78
+ # ── Human-like behavior helpers ─────────────────────────────────
79
+
80
+ def human_delay(min_s: float = 0.5, max_s: float = 2.0):
81
+ """Random delay to simulate human hesitation."""
82
+ time.sleep(random.uniform(min_s, max_s))
83
+
84
+
85
+ def human_scroll(page, direction: str = "down", amount: int = 0):
86
+ """Scroll with natural, variable distances."""
87
+ if amount == 0:
88
+ amount = random.randint(200, 600)
89
+ delta = amount if direction == "down" else -amount
90
+ page.mouse.wheel(0, delta)
91
+ human_delay(0.3, 1.0)
92
+
93
+
94
+ def human_type(page, selector: str, text: str):
95
+ """Type text with variable inter-key delays."""
96
+ page.click(selector)
97
+ human_delay(0.2, 0.5)
98
+ for char in text:
99
+ page.keyboard.type(char)
100
+ time.sleep(random.uniform(0.05, 0.15))
101
+
102
+
103
+ # ── Risk control detection ──────────────────────────────────────
104
+
105
+ def detect_risk_control(page) -> dict:
106
+ """Check if the page is showing CAPTCHA or rate-limit signals."""
107
+ signals = []
108
+
109
+ url = page.url.lower()
110
+ if "captcha" in url or "verify" in url or "challenge" in url:
111
+ signals.append("captcha_url")
112
+
113
+ try:
114
+ content = page.content()
115
+ risk_keywords = [
116
+ "验证码", "滑块验证", "人机验证", "操作频繁",
117
+ "captcha", "verify you are human", "rate limit",
118
+ "too many requests", "access denied", "you have been blocked",
119
+ "your account has been locked", "suspicious activity",
120
+ ]
121
+ content_lower = content.lower()
122
+ for kw in risk_keywords:
123
+ if kw.lower() in content_lower:
124
+ signals.append(f"keyword:{kw}")
125
+ except Exception:
126
+ pass
127
+
128
+ return {
129
+ "detected": len(signals) > 0,
130
+ "signals": signals,
131
+ }
132
+
133
+
134
+ # ── Core actions ────────────────────────────────────────────────
135
+
136
+ def action_read(page, url: str, selectors: list, wait_for: str = None, wait_timeout: int = 15000) -> dict:
137
+ """Navigate to URL, extract text content from selectors."""
138
+ page.goto(url, wait_until="domcontentloaded", timeout=60000)
139
+
140
+ # For SPA sites (X.com, React apps), wait for JS to finish rendering
141
+ try:
142
+ page.wait_for_load_state("networkidle", timeout=15000)
143
+ except Exception:
144
+ pass # Best effort — some SPAs never fully idle
145
+
146
+ # Wait for a specific selector if requested
147
+ if wait_for:
148
+ try:
149
+ page.wait_for_selector(wait_for, timeout=wait_timeout)
150
+ except Exception:
151
+ pass # Continue even if wait_for times out — best effort
152
+
153
+ human_delay(1.0, 3.0)
154
+
155
+ risk = detect_risk_control(page)
156
+ if risk["detected"]:
157
+ return {
158
+ "success": False,
159
+ "error": "risk_control_detected",
160
+ "signals": risk["signals"],
161
+ "advice": "Stop immediately. Risk control triggered.",
162
+ }
163
+
164
+ results = {}
165
+ if not selectors:
166
+ # Extract main content heuristically
167
+ for sel in ["article", "main", "[role='main']", ".content", "#content", "body"]:
168
+ try:
169
+ el = page.query_selector(sel)
170
+ if el:
171
+ text = el.inner_text()
172
+ if len(text) > 50:
173
+ results["content"] = text[:10000]
174
+ break
175
+ except Exception:
176
+ continue
177
+ if not results:
178
+ results["content"] = page.inner_text("body")[:10000]
179
+ else:
180
+ for sel in selectors:
181
+ try:
182
+ el = page.query_selector(sel)
183
+ results[sel] = el.inner_text() if el else None
184
+ except Exception as e:
185
+ results[sel] = f"error: {e}"
186
+
187
+ return {
188
+ "success": True,
189
+ "url": page.url,
190
+ "title": page.title(),
191
+ "results": results,
192
+ }
193
+
194
+
195
+ def action_screenshot(page, url: str) -> dict:
196
+ """Navigate and take a screenshot."""
197
+ os.makedirs(SCREENSHOT_DIR, exist_ok=True)
198
+
199
+ page.goto(url, wait_until="domcontentloaded", timeout=60000)
200
+ try:
201
+ page.wait_for_load_state("networkidle", timeout=15000)
202
+ except Exception:
203
+ pass
204
+ human_delay(1.5, 3.0)
205
+
206
+ risk = detect_risk_control(page)
207
+ if risk["detected"]:
208
+ return {
209
+ "success": False,
210
+ "error": "risk_control_detected",
211
+ "signals": risk["signals"],
212
+ }
213
+
214
+ timestamp = int(time.time())
215
+ filename = f"stealth_{timestamp}.png"
216
+ filepath = os.path.join(SCREENSHOT_DIR, filename)
217
+
218
+ page.screenshot(path=filepath, full_page=False)
219
+
220
+ return {
221
+ "success": True,
222
+ "url": page.url,
223
+ "title": page.title(),
224
+ "screenshot": filepath,
225
+ }
226
+
227
+
228
+ def action_interact(page, url: str, selectors: list) -> dict:
229
+ """Navigate and interact with elements (click, scroll)."""
230
+ page.goto(url, wait_until="domcontentloaded", timeout=60000)
231
+ human_delay(1.0, 2.5)
232
+
233
+ risk = detect_risk_control(page)
234
+ if risk["detected"]:
235
+ return {
236
+ "success": False,
237
+ "error": "risk_control_detected",
238
+ "signals": risk["signals"],
239
+ }
240
+
241
+ results = []
242
+ for sel in selectors:
243
+ try:
244
+ el = page.query_selector(sel)
245
+ if el:
246
+ el.scroll_into_view_if_needed()
247
+ human_delay(0.3, 0.8)
248
+ el.click()
249
+ human_delay(0.5, 1.5)
250
+ results.append({"selector": sel, "action": "clicked", "success": True})
251
+ else:
252
+ results.append({"selector": sel, "action": "not_found", "success": False})
253
+ except Exception as e:
254
+ results.append({"selector": sel, "action": "error", "error": str(e), "success": False})
255
+
256
+ return {
257
+ "success": True,
258
+ "url": page.url,
259
+ "title": page.title(),
260
+ "interactions": results,
261
+ }
262
+
263
+
264
+ # ── Main ────────────────────────────────────────────────────────
265
+
266
+ def main():
267
+ parser = argparse.ArgumentParser(description="Stealth browse via Patchright + CDP")
268
+ parser.add_argument("--url", required=True, help="Target URL")
269
+ parser.add_argument("--action", default="read", choices=["read", "screenshot", "interact"],
270
+ help="Action to perform")
271
+ parser.add_argument("--selectors", nargs="*", default=[], help="CSS selectors")
272
+ parser.add_argument("--wait-for", default=None, help="CSS selector to wait for before extracting")
273
+ parser.add_argument("--wait-timeout", type=int, default=15000, help="Timeout in ms for --wait-for")
274
+ parser.add_argument("--cdp-port", type=int, default=CDP_PORT, help="CDP port")
275
+ args = parser.parse_args()
276
+
277
+ # Ensure patchright is installed
278
+ try:
279
+ from patchright.sync_api import sync_playwright
280
+ except ImportError:
281
+ print(json.dumps({
282
+ "success": False,
283
+ "error": "patchright_not_installed",
284
+ "fix": "pip3 install patchright && python3 -m patchright install chromium",
285
+ }))
286
+ sys.exit(1)
287
+
288
+ # Ensure Chrome is running with CDP
289
+ try:
290
+ ws_url = ensure_chrome_running()
291
+ except Exception as e:
292
+ print(json.dumps({"success": False, "error": f"chrome_launch_failed: {e}"}))
293
+ sys.exit(1)
294
+
295
+ # Connect via Patchright
296
+ with sync_playwright() as pw:
297
+ try:
298
+ browser = pw.chromium.connect_over_cdp(ws_url)
299
+ except Exception as e:
300
+ print(json.dumps({"success": False, "error": f"cdp_connect_failed: {e}"}))
301
+ sys.exit(1)
302
+
303
+ # CRITICAL: Use Chrome's existing default context, NOT new_context()
304
+ contexts = browser.contexts
305
+ if not contexts:
306
+ print(json.dumps({"success": False, "error": "no_browser_context_found"}))
307
+ sys.exit(1)
308
+
309
+ context = contexts[0]
310
+ page = context.new_page()
311
+
312
+ try:
313
+ if args.action == "read":
314
+ result = action_read(page, args.url, args.selectors, args.wait_for, args.wait_timeout)
315
+ elif args.action == "screenshot":
316
+ result = action_screenshot(page, args.url)
317
+ elif args.action == "interact":
318
+ result = action_interact(page, args.url, args.selectors)
319
+ else:
320
+ result = {"success": False, "error": f"unknown_action: {args.action}"}
321
+ except Exception as e:
322
+ result = {"success": False, "error": str(e)}
323
+ finally:
324
+ page.close()
325
+
326
+ print(json.dumps(result, ensure_ascii=False, indent=2))
327
+
328
+
329
+ if __name__ == "__main__":
330
+ main()
@@ -0,0 +1,232 @@
1
+ #!/bin/bash
2
+ # Competitor Content Tracker — store, query, and compare competitor content data
3
+ set -euo pipefail
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "${SCRIPT_DIR}/../_common/lib.sh"
6
+
7
+ INPUT="${1:-}"
8
+ [ -z "$INPUT" ] && error_exit "Usage: execute.sh '{\"action\":\"save|list|compare|latest\",\"competitor\":\"crewai\",...}'"
9
+
10
+ ACTION=$(echo "$INPUT" | jq -r '.action // empty')
11
+ PROJECT_PATH=$(echo "$INPUT" | jq -r '.projectPath // empty')
12
+
13
+ require_param "action" "$ACTION"
14
+
15
+ # Resolve storage directory
16
+ if [ -n "$PROJECT_PATH" ]; then
17
+ TRACKER_DIR="${PROJECT_PATH}/.crewly/content/competitors"
18
+ else
19
+ TRACKER_DIR="${HOME}/.crewly/content/competitors"
20
+ fi
21
+ mkdir -p "$TRACKER_DIR"
22
+
23
+ # Valid competitors
24
+ validate_competitor() {
25
+ case "$1" in
26
+ crewai|n8n|relevance-ai|autogen|langchain|langraph|openai|other) return 0 ;;
27
+ *) error_exit "Unknown competitor: $1. Valid: crewai, n8n, relevance-ai, autogen, langchain, langraph, openai, other" ;;
28
+ esac
29
+ }
30
+
31
+ case "$ACTION" in
32
+
33
+ # ─────────────────────────────────────────────
34
+ # SAVE: Store content items from a competitor scan
35
+ # ─────────────────────────────────────────────
36
+ save)
37
+ COMPETITOR=$(echo "$INPUT" | jq -r '.competitor // empty')
38
+ SOURCE_TYPE=$(echo "$INPUT" | jq -r '.sourceType // empty')
39
+ ITEMS=$(echo "$INPUT" | jq -r '.items // empty')
40
+
41
+ require_param "competitor" "$COMPETITOR"
42
+ require_param "sourceType" "$SOURCE_TYPE"
43
+ require_param "items" "$ITEMS"
44
+
45
+ validate_competitor "$COMPETITOR"
46
+
47
+ # Validate source type
48
+ case "$SOURCE_TYPE" in
49
+ blog|twitter|linkedin|github-release|changelog|youtube|community|press|other) ;;
50
+ *) error_exit "Invalid sourceType: $SOURCE_TYPE. Valid: blog, twitter, linkedin, github-release, changelog, youtube, community, press, other" ;;
51
+ esac
52
+
53
+ # Validate items is a JSON array
54
+ if ! echo "$ITEMS" | jq 'type == "array"' 2>/dev/null | grep -q true; then
55
+ error_exit "items must be a JSON array"
56
+ fi
57
+
58
+ NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
59
+ TODAY=$(date -u +%Y-%m-%d)
60
+ SCAN_ID="comp-$(date +%s)-$((RANDOM % 1000))"
61
+
62
+ # Enrich items with metadata
63
+ ENRICHED=$(echo "$ITEMS" | jq \
64
+ --arg competitor "$COMPETITOR" \
65
+ --arg sourceType "$SOURCE_TYPE" \
66
+ --arg scanId "$SCAN_ID" \
67
+ --arg ts "$NOW" \
68
+ '[.[] | . + {"competitor": $competitor, "sourceType": $sourceType, "scanId": $scanId, "scannedAt": $ts}]')
69
+
70
+ COUNT=$(echo "$ENRICHED" | jq 'length')
71
+
72
+ # Save to competitor-specific file
73
+ COMP_DIR="${TRACKER_DIR}/${COMPETITOR}"
74
+ mkdir -p "$COMP_DIR"
75
+ SCAN_FILE="${COMP_DIR}/${TODAY}-${SOURCE_TYPE}.json"
76
+
77
+ if [ -f "$SCAN_FILE" ]; then
78
+ EXISTING=$(cat "$SCAN_FILE")
79
+ MERGED=$(jq -n --argjson existing "$EXISTING" --argjson new "$ENRICHED" \
80
+ '{"scans": ($existing.scans + [{"scanId": $new[0].scanId, "scannedAt": $new[0].scannedAt, "count": ($new | length), "items": $new}])}')
81
+ echo "$MERGED" > "$SCAN_FILE"
82
+ else
83
+ jq -n --argjson items "$ENRICHED" --arg competitor "$COMPETITOR" --arg sourceType "$SOURCE_TYPE" --arg date "$TODAY" \
84
+ '{"competitor": $competitor, "sourceType": $sourceType, "date": $date, "scans": [{"scanId": $items[0].scanId, "scannedAt": $items[0].scannedAt, "count": ($items | length), "items": $items}]}' > "$SCAN_FILE"
85
+ fi
86
+
87
+ jq -n \
88
+ --arg scanId "$SCAN_ID" \
89
+ --arg competitor "$COMPETITOR" \
90
+ --arg sourceType "$SOURCE_TYPE" \
91
+ --argjson count "$COUNT" \
92
+ --arg file "$SCAN_FILE" \
93
+ '{"success":true,"action":"save","scanId":$scanId,"competitor":$competitor,"sourceType":$sourceType,"count":$count,"file":$file}'
94
+ ;;
95
+
96
+ # ─────────────────────────────────────────────
97
+ # LIST: List tracked content by competitor
98
+ # ─────────────────────────────────────────────
99
+ list)
100
+ FILTER_COMP=$(echo "$INPUT" | jq -r '.competitor // empty')
101
+ LIMIT=$(echo "$INPUT" | jq -r '.limit // "20"')
102
+
103
+ RESULTS="[]"
104
+
105
+ SEARCH_DIRS=""
106
+ if [ -n "$FILTER_COMP" ]; then
107
+ validate_competitor "$FILTER_COMP"
108
+ SEARCH_DIRS="${TRACKER_DIR}/${FILTER_COMP}"
109
+ else
110
+ SEARCH_DIRS="$TRACKER_DIR"
111
+ fi
112
+
113
+ for f in $(find "$SEARCH_DIRS" -name "*.json" -type f 2>/dev/null | sort -r | head -"$LIMIT"); do
114
+ [ -f "$f" ] || continue
115
+ COMP=$(jq -r '.competitor // "unknown"' "$f" 2>/dev/null || echo "unknown")
116
+ STYPE=$(jq -r '.sourceType // "unknown"' "$f" 2>/dev/null || echo "unknown")
117
+ FDATE=$(jq -r '.date // "unknown"' "$f" 2>/dev/null || echo "unknown")
118
+ TOTAL=$(jq '[.scans[].count] | add // 0' "$f" 2>/dev/null || echo "0")
119
+ LAST=$(jq -r '.scans[-1].scannedAt // "unknown"' "$f" 2>/dev/null || echo "unknown")
120
+
121
+ RESULTS=$(echo "$RESULTS" | jq \
122
+ --arg file "$f" \
123
+ --arg competitor "$COMP" \
124
+ --arg sourceType "$STYPE" \
125
+ --arg date "$FDATE" \
126
+ --argjson totalItems "$TOTAL" \
127
+ --arg lastScan "$LAST" \
128
+ '. + [{"file":$file,"competitor":$competitor,"sourceType":$sourceType,"date":$date,"totalItems":$totalItems,"lastScan":$lastScan}]')
129
+ done
130
+
131
+ COUNT=$(echo "$RESULTS" | jq 'length')
132
+ jq -n --argjson results "$RESULTS" --argjson count "$COUNT" \
133
+ '{"success":true,"action":"list","count":$count,"results":$results}'
134
+ ;;
135
+
136
+ # ─────────────────────────────────────────────
137
+ # LATEST: Get latest content from a competitor
138
+ # ─────────────────────────────────────────────
139
+ latest)
140
+ FILTER_COMP=$(echo "$INPUT" | jq -r '.competitor // empty')
141
+ FILTER_TYPE=$(echo "$INPUT" | jq -r '.sourceType // empty')
142
+ LIMIT=$(echo "$INPUT" | jq -r '.limit // "15"')
143
+
144
+ require_param "competitor" "$FILTER_COMP"
145
+ validate_competitor "$FILTER_COMP"
146
+
147
+ COMP_DIR="${TRACKER_DIR}/${FILTER_COMP}"
148
+ ALL_ITEMS="[]"
149
+
150
+ if [ -d "$COMP_DIR" ]; then
151
+ for f in $(ls -t "$COMP_DIR"/*.json 2>/dev/null | head -5); do
152
+ [ -f "$f" ] || continue
153
+ if [ -n "$FILTER_TYPE" ]; then
154
+ STYPE=$(jq -r '.sourceType // ""' "$f")
155
+ [ "$STYPE" != "$FILTER_TYPE" ] && continue
156
+ fi
157
+ ITEMS=$(jq '.scans[-1].items // []' "$f" 2>/dev/null || echo "[]")
158
+ ALL_ITEMS=$(jq -n --argjson a "$ALL_ITEMS" --argjson b "$ITEMS" '$a + $b')
159
+ done
160
+ fi
161
+
162
+ ALL_ITEMS=$(echo "$ALL_ITEMS" | jq --argjson limit "$LIMIT" '.[:$limit]')
163
+ COUNT=$(echo "$ALL_ITEMS" | jq 'length')
164
+
165
+ jq -n --arg competitor "$FILTER_COMP" --argjson items "$ALL_ITEMS" --argjson count "$COUNT" \
166
+ '{"success":true,"action":"latest","competitor":$competitor,"count":$count,"items":$items}'
167
+ ;;
168
+
169
+ # ─────────────────────────────────────────────
170
+ # COMPARE: Compare content activity across competitors
171
+ # ─────────────────────────────────────────────
172
+ compare)
173
+ COMPETITORS_INPUT=$(echo "$INPUT" | jq -r '.competitors // "crewai,n8n,relevance-ai"')
174
+ DAYS=$(echo "$INPUT" | jq -r '.days // "7"')
175
+
176
+ IFS=',' read -ra COMP_ARRAY <<< "$COMPETITORS_INPUT"
177
+
178
+ # Calculate date threshold
179
+ THRESHOLD=$(date -u -v-"${DAYS}"d +%Y-%m-%d 2>/dev/null || date -u -d "-${DAYS} days" +%Y-%m-%d 2>/dev/null || date -u +%Y-%m-%d)
180
+
181
+ COMPARISON="[]"
182
+
183
+ for comp in "${COMP_ARRAY[@]}"; do
184
+ comp=$(echo "$comp" | xargs)
185
+ COMP_DIR="${TRACKER_DIR}/${comp}"
186
+
187
+ TOTAL_ITEMS=0
188
+ BY_TYPE="{}"
189
+ LATEST_TITLES="[]"
190
+
191
+ if [ -d "$COMP_DIR" ]; then
192
+ for f in "$COMP_DIR"/*.json; do
193
+ [ -f "$f" ] || continue
194
+ FDATE=$(jq -r '.date // ""' "$f" 2>/dev/null)
195
+ [ -z "$FDATE" ] && continue
196
+
197
+ if [ "$FDATE" \> "$THRESHOLD" ] || [ "$FDATE" = "$THRESHOLD" ]; then
198
+ STYPE=$(jq -r '.sourceType // "unknown"' "$f" 2>/dev/null)
199
+ FCOUNT=$(jq '[.scans[].count] | add // 0' "$f" 2>/dev/null || echo "0")
200
+ TOTAL_ITEMS=$((TOTAL_ITEMS + FCOUNT))
201
+
202
+ BY_TYPE=$(echo "$BY_TYPE" | jq --arg t "$STYPE" --argjson c "$FCOUNT" \
203
+ '. + {($t): ((.[$t] // 0) + $c)}')
204
+
205
+ # Get latest titles
206
+ TITLES=$(jq '[.scans[-1].items[:3][] | .title // "untitled"]' "$f" 2>/dev/null || echo "[]")
207
+ LATEST_TITLES=$(jq -n --argjson a "$LATEST_TITLES" --argjson b "$TITLES" '$a + $b')
208
+ fi
209
+ done
210
+ fi
211
+
212
+ LATEST_TITLES=$(echo "$LATEST_TITLES" | jq '.[:5]')
213
+
214
+ COMPARISON=$(echo "$COMPARISON" | jq \
215
+ --arg competitor "$comp" \
216
+ --argjson totalItems "$TOTAL_ITEMS" \
217
+ --argjson byType "$BY_TYPE" \
218
+ --argjson latestTitles "$LATEST_TITLES" \
219
+ '. + [{"competitor":$competitor,"totalItems":$totalItems,"bySourceType":$byType,"recentTitles":$latestTitles}]')
220
+ done
221
+
222
+ jq -n \
223
+ --argjson comparison "$COMPARISON" \
224
+ --arg period "${DAYS} days" \
225
+ --arg since "$THRESHOLD" \
226
+ '{"success":true,"action":"compare","period":$period,"since":$since,"competitors":$comparison}'
227
+ ;;
228
+
229
+ *)
230
+ error_exit "Unknown action: $ACTION. Valid: save, list, latest, compare"
231
+ ;;
232
+ esac