crewly 1.1.2 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/config/constants.ts +5 -0
- package/config/roles/ops/prompt.md +140 -0
- package/config/roles/ops/role.json +13 -0
- package/config/skills/agent/browse-stealth/execute.sh +84 -0
- package/config/skills/agent/browse-stealth/instructions.md +108 -0
- package/config/skills/agent/browse-stealth/launch-chrome-cdp.sh +141 -0
- package/config/skills/agent/browse-stealth/skill.json +20 -0
- package/config/skills/agent/browse-stealth/stealth-browse.py +330 -0
- package/config/skills/agent/competitor-content-tracker/execute.sh +232 -0
- package/config/skills/agent/competitor-content-tracker/instructions.md +210 -0
- package/config/skills/agent/competitor-content-tracker/skill.json +22 -0
- package/config/skills/agent/content-calendar/execute.sh +294 -0
- package/config/skills/agent/content-calendar/instructions.md +122 -0
- package/config/skills/agent/content-calendar/skill.json +22 -0
- package/config/skills/agent/content-repurposer/execute.sh +194 -0
- package/config/skills/agent/content-repurposer/instructions.md +69 -0
- package/config/skills/agent/content-repurposer/skill.json +22 -0
- package/config/skills/agent/content-writer/execute.sh +311 -0
- package/config/skills/agent/content-writer/instructions.md +124 -0
- package/config/skills/agent/content-writer/skill.json +22 -0
- package/config/skills/agent/core/generate-pdf/execute.sh +88 -0
- package/config/skills/agent/core/generate-pdf/instructions.md +46 -0
- package/config/skills/agent/core/generate-pdf/skill.json +20 -0
- package/config/skills/agent/core/report-status/execute.sh +6 -0
- package/config/skills/agent/trend-monitor/execute.sh +211 -0
- package/config/skills/agent/trend-monitor/instructions.md +207 -0
- package/config/skills/agent/trend-monitor/skill.json +22 -0
- package/config/skills/agent/vnc-browser/execute.sh +261 -0
- package/config/skills/agent/vnc-browser/instructions.md +102 -0
- package/config/skills/agent/vnc-browser/skill.json +20 -0
- package/config/skills/orchestrator/delegate-task/execute.sh +65 -4
- package/config/skills/orchestrator/delegate-task/instructions.md +65 -0
- package/config/skills/orchestrator/delegate-task/skill.json +4 -4
- package/config/skills/orchestrator/reply-slack/execute.sh +2 -0
- package/config/skills/orchestrator/send-key/execute.sh +19 -6
- package/config/skills/orchestrator/send-key/instructions.md +44 -0
- package/config/skills/orchestrator/send-key/skill.json +20 -0
- package/config/skills/orchestrator/send-message/execute.sh +9 -1
- package/config/skills/registry.json +256 -0
- package/config/templates/code-review-team/README.md +176 -0
- package/config/templates/code-review-team/team-config.json +16 -0
- package/config/templates/code-review-team.json +62 -0
- package/config/templates/content-generation-team/README.md +128 -0
- package/config/templates/content-generation-team/team-config.json +21 -0
- package/config/templates/content-generation-team.json +67 -0
- package/config/templates/demo-team.json +22 -0
- package/config/templates/social-media-ops-team/README.md +145 -0
- package/config/templates/social-media-ops-team/team-config.json +21 -0
- package/config/templates/social-media-ops-team.json +67 -0
- package/dist/backend/backend/src/constants.d.ts +86 -6
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +91 -6
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/index.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/index.js +2 -0
- package/dist/backend/backend/src/controllers/index.js.map +1 -1
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts +8 -0
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.js +110 -63
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +31 -4
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/oauth/oauth.routes.d.ts +8 -0
- package/dist/backend/backend/src/controllers/oauth/oauth.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/oauth/oauth.routes.js +127 -111
- package/dist/backend/backend/src/controllers/oauth/oauth.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +34 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +219 -2
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/user/user.routes.d.ts +7 -0
- package/dist/backend/backend/src/controllers/user/user.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/user/user.routes.js +45 -38
- package/dist/backend/backend/src/controllers/user/user.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/whatsapp/index.d.ts +17 -0
- package/dist/backend/backend/src/controllers/whatsapp/index.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/index.js +18 -0
- package/dist/backend/backend/src/controllers/whatsapp/index.js.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts +12 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +185 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts +5 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/modules/task-management.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/modules/task-management.routes.js +4 -0
- package/dist/backend/backend/src/routes/modules/task-management.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-heartbeat.service.js +1 -1
- package/dist/backend/backend/src/services/agent/agent-heartbeat.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +14 -3
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +184 -30
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/claude-runtime.service.d.ts +4 -3
- package/dist/backend/backend/src/services/agent/claude-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/claude-runtime.service.js +29 -4
- package/dist/backend/backend/src/services/agent/claude-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.js +11 -0
- package/dist/backend/backend/src/services/agent/context-window-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +32 -2
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +69 -8
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts +30 -20
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js +128 -22
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
- package/dist/backend/backend/src/services/knowledge/knowledge-search.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/knowledge/knowledge-search.service.js +14 -2
- package/dist/backend/backend/src/services/knowledge/knowledge-search.service.js.map +1 -1
- package/dist/backend/backend/src/services/marketplace/marketplace-installer.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/marketplace/marketplace-installer.service.js +11 -2
- package/dist/backend/backend/src/services/marketplace/marketplace-installer.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.d.ts +18 -0
- package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.js +28 -4
- package/dist/backend/backend/src/services/messaging/adapters/discord-messenger.adapter.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/slack-messenger.adapter.js +2 -2
- package/dist/backend/backend/src/services/messaging/adapters/slack-messenger.adapter.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.d.ts +18 -0
- package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.js +26 -4
- package/dist/backend/backend/src/services/messaging/adapters/telegram-messenger.adapter.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/messenger-adapter.interface.d.ts +28 -2
- package/dist/backend/backend/src/services/messaging/messenger-adapter.interface.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/messenger-registry.service.d.ts +33 -2
- package/dist/backend/backend/src/services/messaging/messenger-registry.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/messenger-registry.service.js +33 -0
- package/dist/backend/backend/src/services/messaging/messenger-registry.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +47 -10
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts +8 -0
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +49 -2
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts +6 -0
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js +25 -0
- package/dist/backend/backend/src/services/orchestrator/orchestrator-heartbeat-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.js +4 -3
- package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.js.map +1 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +27 -0
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.js +54 -0
- package/dist/backend/backend/src/services/project/task-tracking.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js +16 -1
- package/dist/backend/backend/src/services/session/session-command-helper.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +36 -6
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +238 -36
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +6 -4
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/user/user-identity.service.d.ts +44 -0
- package/dist/backend/backend/src/services/user/user-identity.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/user/user-identity.service.js +75 -8
- package/dist/backend/backend/src/services/user/user-identity.service.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/index.d.ts +11 -0
- package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/index.js +11 -0
- package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +66 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +96 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts +109 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +234 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +127 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +347 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -0
- package/dist/backend/backend/src/services/workflow/scheduler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/scheduler.service.js +4 -0
- package/dist/backend/backend/src/services/workflow/scheduler.service.js.map +1 -1
- package/dist/backend/backend/src/types/index.d.ts +1 -0
- package/dist/backend/backend/src/types/index.d.ts.map +1 -1
- package/dist/backend/backend/src/types/index.js.map +1 -1
- package/dist/backend/backend/src/types/slack.types.d.ts +24 -0
- package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/slack.types.js.map +1 -1
- package/dist/backend/backend/src/types/task-tracking.types.d.ts +4 -0
- package/dist/backend/backend/src/types/task-tracking.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/task-tracking.types.js.map +1 -1
- package/dist/backend/backend/src/types/whatsapp.types.d.ts +84 -0
- package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/whatsapp.types.js +33 -0
- package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -0
- package/dist/backend/backend/src/websocket/terminal.gateway.d.ts +11 -0
- package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.js +35 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
- package/dist/backend/config/constants.d.ts +5 -0
- package/dist/backend/config/constants.d.ts.map +1 -1
- package/dist/backend/config/constants.js +5 -0
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +86 -6
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +91 -6
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/knowledge/knowledge-search.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js +14 -2
- package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js.map +1 -1
- package/dist/cli/backend/src/types/index.d.ts +1 -0
- package/dist/cli/backend/src/types/index.d.ts.map +1 -1
- package/dist/cli/backend/src/types/index.js.map +1 -1
- package/dist/cli/cli/src/commands/publish.d.ts.map +1 -1
- package/dist/cli/cli/src/commands/publish.js +17 -15
- package/dist/cli/cli/src/commands/publish.js.map +1 -1
- package/dist/cli/cli/src/index.js +2 -2
- package/dist/cli/cli/src/index.js.map +1 -1
- package/dist/cli/cli/src/utils/gh-submit.d.ts +46 -0
- package/dist/cli/cli/src/utils/gh-submit.d.ts.map +1 -0
- package/dist/cli/cli/src/utils/gh-submit.js +167 -0
- package/dist/cli/cli/src/utils/gh-submit.js.map +1 -0
- package/dist/cli/cli/src/utils/marketplace.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/marketplace.js +13 -5
- package/dist/cli/cli/src/utils/marketplace.js.map +1 -1
- package/dist/cli/cli/src/utils/templates.d.ts +3 -2
- package/dist/cli/cli/src/utils/templates.d.ts.map +1 -1
- package/dist/cli/cli/src/utils/templates.js +5 -4
- package/dist/cli/cli/src/utils/templates.js.map +1 -1
- package/dist/cli/config/constants.d.ts +5 -0
- package/dist/cli/config/constants.d.ts.map +1 -1
- package/dist/cli/config/constants.js +5 -0
- package/dist/cli/config/constants.js.map +1 -1
- package/frontend/dist/assets/{index-45eeea99.js → index-a23214ae.js} +241 -241
- package/frontend/dist/assets/{index-6972eeee.css → index-c407fe13.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -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
|