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.
- package/README.md +6 -6
- 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 +63 -4
- package/config/skills/orchestrator/delegate-task/instructions.md +60 -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 +69 -6
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +75 -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 +35 -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 +160 -29
- 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/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/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/monitoring/activity-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/activity-monitor.service.js +4 -2
- package/dist/backend/backend/src/services/monitoring/activity-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/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/cli/backend/src/constants.d.ts +69 -6
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +75 -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/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 +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
|