opencode-skills-collection 4.0.17 → 4.0.18
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/bundled-skills/.antigravity-install-manifest.json +4 -1
- package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +59 -14
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +1 -0
- package/bundled-skills/apify-audience-analysis/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-brand-reputation-monitoring/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-competitor-intelligence/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-content-analytics/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-ecommerce/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-influencer-discovery/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-lead-generation/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-market-research/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-trend-analysis/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-ultimate-scraper/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +22 -6
- package/bundled-skills/context-guardian/scripts/context_snapshot.py +45 -10
- package/bundled-skills/cowork-to-code-bridge/SKILL.md +243 -0
- package/bundled-skills/deploy-to-vercel/resources/deploy.sh +13 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/aas-core.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/faq.md +20 -7
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +18 -6
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +3 -3
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/fact-check-x-complete/SKILL.md +232 -0
- package/bundled-skills/find-complementary-founders/SKILL.md +47 -9
- package/bundled-skills/find-complementary-founders/references/community-growth.md +39 -28
- package/bundled-skills/find-complementary-founders/references/moltbook.md +2 -1
- package/bundled-skills/find-complementary-founders/references/owner-onboarding.ru.md +29 -14
- package/bundled-skills/find-complementary-founders/references/privacy-safety.md +3 -0
- package/bundled-skills/find-complementary-founders/references/profile-schema.md +3 -2
- package/bundled-skills/find-complementary-founders/scripts/github_thread.py +1 -1
- package/bundled-skills/find-complementary-founders/scripts/moltbook_publish.py +19 -6
- package/bundled-skills/find-complementary-founders/scripts/test_moltbook_publish.py +151 -0
- package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +20 -10
- package/bundled-skills/instagram/scripts/config.py +22 -4
- package/bundled-skills/instagram/scripts/db.py +8 -0
- package/bundled-skills/junta-leiloeiros/scripts/export.py +11 -2
- package/bundled-skills/junta-leiloeiros/scripts/serve_api.py +5 -1
- package/bundled-skills/loki-mode/CLAUDE.md +1 -1
- package/bundled-skills/loki-mode/CONTEXT-EXPORT.md +3 -3
- package/bundled-skills/loki-mode/INSTALLATION.md +3 -3
- package/bundled-skills/loki-mode/README.md +11 -11
- package/bundled-skills/loki-mode/SKILL.md +2 -2
- package/bundled-skills/loki-mode/autonomy/README.md +8 -8
- package/bundled-skills/loki-mode/autonomy/run.sh +23 -54
- package/bundled-skills/loki-mode/demo/README.md +3 -3
- package/bundled-skills/loki-mode/demo/run-demo.sh +3 -3
- package/bundled-skills/loki-mode/demo/vhs-tape.tape +1 -1
- package/bundled-skills/loki-mode/demo/voice-over-script.md +3 -3
- package/bundled-skills/loki-mode/docs/COMPETITIVE-ANALYSIS.md +2 -2
- package/bundled-skills/loki-mode/docs/screenshots/README.md +1 -1
- package/bundled-skills/loki-mode/integrations/vibe-kanban.md +1 -1
- package/bundled-skills/loki-mode/references/agent-types.md +2 -2
- package/bundled-skills/loki-mode/scripts/loki-wrapper.sh +5 -4
- package/bundled-skills/maintain-codex-wiki/SKILL.md +340 -0
- package/bundled-skills/notebooklm/README.md +4 -3
- package/bundled-skills/notebooklm/SKILL.md +3 -3
- package/bundled-skills/notebooklm/scripts/auth_manager.py +6 -4
- package/bundled-skills/notebooklm/scripts/browser_utils.py +2 -1
- package/bundled-skills/notebooklm/scripts/config.py +31 -1
- package/bundled-skills/pptx-official/scripts/html2pptx.js +37 -8
- package/bundled-skills/senior-frontend/scripts/frontend_scaffolder.py +6 -0
- package/bundled-skills/skill-creator/scripts/init_skill.py +18 -2
- package/bundled-skills/telegram/SKILL.md +2 -2
- package/bundled-skills/telegram/assets/boilerplate/nodejs/.env.example +1 -1
- package/bundled-skills/telegram/assets/boilerplate/nodejs/src/bot-client.ts +7 -6
- package/bundled-skills/telegram/assets/boilerplate/nodejs/src/index.ts +3 -0
- package/bundled-skills/telegram/assets/boilerplate/python/.env.example +1 -1
- package/bundled-skills/telegram/assets/boilerplate/python/webhook_server.py +9 -4
- package/bundled-skills/telegram/references/webhook-setup.md +10 -9
- package/bundled-skills/youtube-notetaker/SKILL.md +1 -1
- package/bundled-skills/youtube-notetaker/scripts/extract_slides.py +19 -6
- package/bundled-skills/youtube-notetaker/scripts/setup.sh +1 -0
- package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +15 -4
- package/package.json +1 -1
- package/skills_index.json +94 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"updatedAt": "2026-
|
|
3
|
+
"updatedAt": "2026-08-01T01:42:51.046Z",
|
|
4
4
|
"entries": [
|
|
5
5
|
"00-andruia-consultant",
|
|
6
6
|
"007",
|
|
@@ -520,6 +520,7 @@
|
|
|
520
520
|
"copywriting-psychologist",
|
|
521
521
|
"core-components",
|
|
522
522
|
"cost-optimization",
|
|
523
|
+
"cowork-to-code-bridge",
|
|
523
524
|
"cpp-pro",
|
|
524
525
|
"cqrs-implementation",
|
|
525
526
|
"create-branch",
|
|
@@ -767,6 +768,7 @@
|
|
|
767
768
|
"expo-ui-jetpack-compose",
|
|
768
769
|
"expo-ui-swift-ui",
|
|
769
770
|
"fable-safe-prompt",
|
|
771
|
+
"fact-check-x-complete",
|
|
770
772
|
"faf-context",
|
|
771
773
|
"faf-expert",
|
|
772
774
|
"faf-go",
|
|
@@ -1146,6 +1148,7 @@
|
|
|
1146
1148
|
"mailtrap-sending-emails",
|
|
1147
1149
|
"mailtrap-setting-up-sending-domain",
|
|
1148
1150
|
"mailtrap-testing-with-sandbox",
|
|
1151
|
+
"maintain-codex-wiki",
|
|
1149
1152
|
"make-automation",
|
|
1150
1153
|
"makepad-animation",
|
|
1151
1154
|
"makepad-basics",
|
|
@@ -11,6 +11,9 @@ import argparse
|
|
|
11
11
|
import ipaddress
|
|
12
12
|
import re
|
|
13
13
|
import socket
|
|
14
|
+
import ssl
|
|
15
|
+
import http.client
|
|
16
|
+
import tempfile
|
|
14
17
|
import requests
|
|
15
18
|
from urllib.parse import urlparse
|
|
16
19
|
from typing import Optional, Dict, Any
|
|
@@ -40,15 +43,56 @@ def validate_job_id(job_id: str) -> str:
|
|
|
40
43
|
return job_id
|
|
41
44
|
|
|
42
45
|
|
|
43
|
-
def validate_public_https_url(url: str) -> str:
|
|
46
|
+
def validate_public_https_url(url: str) -> tuple[str, str]:
|
|
44
47
|
parsed = urlparse(url)
|
|
45
|
-
if parsed.scheme != "https" or not parsed.hostname:
|
|
48
|
+
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.port not in (None, 443):
|
|
46
49
|
raise ValueError("Download URL must be HTTPS")
|
|
50
|
+
public_ips = []
|
|
47
51
|
for info in socket.getaddrinfo(parsed.hostname, None):
|
|
48
52
|
ip = ipaddress.ip_address(info[4][0])
|
|
49
|
-
if
|
|
53
|
+
if not ip.is_global:
|
|
50
54
|
raise ValueError("Download URL resolves to a non-public address")
|
|
51
|
-
|
|
55
|
+
public_ips.append(str(ip))
|
|
56
|
+
if not public_ips:
|
|
57
|
+
raise ValueError("Download URL did not resolve")
|
|
58
|
+
return url, public_ips[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def download_pinned_https(url: str, pinned_ip: str, output_path: Path, maximum_bytes: int = 128 * 1024 * 1024) -> int:
|
|
62
|
+
"""Download once from the verified IP while preserving TLS SNI/hostname checks."""
|
|
63
|
+
parsed = urlparse(url)
|
|
64
|
+
raw_socket = socket.create_connection((pinned_ip, 443), timeout=30)
|
|
65
|
+
tls_context = ssl.create_default_context()
|
|
66
|
+
tls_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
67
|
+
tls_socket = tls_context.wrap_socket(raw_socket, server_hostname=parsed.hostname)
|
|
68
|
+
request_target = parsed.path or "/"
|
|
69
|
+
if parsed.query:
|
|
70
|
+
request_target += f"?{parsed.query}"
|
|
71
|
+
host = parsed.hostname
|
|
72
|
+
tls_socket.sendall(
|
|
73
|
+
f"GET {request_target} HTTP/1.1\r\nHost: {host}\r\nUser-Agent: 2slides-downloader/1\r\nAccept: application/zip\r\nConnection: close\r\n\r\n".encode("ascii")
|
|
74
|
+
)
|
|
75
|
+
response = http.client.HTTPResponse(tls_socket)
|
|
76
|
+
response.begin()
|
|
77
|
+
if 300 <= response.status < 400:
|
|
78
|
+
raise ValueError("Download redirects are refused; request a fresh final URL from 2slides")
|
|
79
|
+
if response.status != 200:
|
|
80
|
+
raise ValueError(f"Download failed with HTTP {response.status}")
|
|
81
|
+
declared = response.getheader("Content-Length")
|
|
82
|
+
if declared and int(declared) > maximum_bytes:
|
|
83
|
+
raise ValueError("Download exceeds the 128 MiB limit")
|
|
84
|
+
total = 0
|
|
85
|
+
with output_path.open("wb") as destination:
|
|
86
|
+
while True:
|
|
87
|
+
chunk = response.read(8192)
|
|
88
|
+
if not chunk:
|
|
89
|
+
break
|
|
90
|
+
total += len(chunk)
|
|
91
|
+
if total > maximum_bytes:
|
|
92
|
+
raise ValueError("Download exceeds the 128 MiB limit")
|
|
93
|
+
destination.write(chunk)
|
|
94
|
+
response.close()
|
|
95
|
+
return total
|
|
52
96
|
|
|
53
97
|
|
|
54
98
|
def get_api_key() -> str:
|
|
@@ -120,7 +164,7 @@ def download_slides_pages_voices(
|
|
|
120
164
|
download_url = data.get("downloadUrl")
|
|
121
165
|
if not download_url:
|
|
122
166
|
raise ValueError("No download URL in response")
|
|
123
|
-
download_url = validate_public_https_url(download_url)
|
|
167
|
+
download_url, pinned_ip = validate_public_https_url(download_url)
|
|
124
168
|
|
|
125
169
|
# Optional: log additional info
|
|
126
170
|
file_name = data.get("fileName", "unknown.zip")
|
|
@@ -134,15 +178,16 @@ def download_slides_pages_voices(
|
|
|
134
178
|
|
|
135
179
|
print(f"Downloading ZIP archive to: {output_path}...", file=sys.stderr)
|
|
136
180
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
destination = safe_user_path(output_path)
|
|
182
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
with tempfile.NamedTemporaryFile(prefix=".2slides-", suffix=".zip", dir=destination.parent, delete=False) as tmp:
|
|
184
|
+
temporary = Path(tmp.name)
|
|
185
|
+
try:
|
|
186
|
+
file_size = download_pinned_https(download_url, pinned_ip, temporary)
|
|
187
|
+
os.chmod(temporary, 0o600)
|
|
188
|
+
os.replace(temporary, destination)
|
|
189
|
+
finally:
|
|
190
|
+
temporary.unlink(missing_ok=True)
|
|
146
191
|
print(f"✓ Downloaded successfully!", file=sys.stderr)
|
|
147
192
|
print(f" File: {output_path}", file=sys.stderr)
|
|
148
193
|
print(f" Size: {file_size:,} bytes", file=sys.stderr)
|
|
@@ -125,6 +125,7 @@ Every stable or prerelease version requires full release alignment. Creating the
|
|
|
125
125
|
4. Run `npm run release:prepare -- X.Y.Z`. This creates and pushes `release/vX.Y.Z` and opens the protected release PR.
|
|
126
126
|
5. Merge that release PR through its required checks, update local `main` to equal `origin/main`, and wait for every source, release, or canonical-sync PR in the release path to close. Re-run the release-state and plugin gates if protected `main` moved.
|
|
127
127
|
6. Run `npm run release:publish -- X.Y.Z`. It must resolve exactly one merged release PR from the same repository, authored by the repository owner, with base `main`, exact title `chore: release vX.Y.Z`, and head branch `release/vX.Y.Z`. Zero or multiple candidates fail closed; never select the newest approximate match. The command then verifies that exact protected merge before creating or reusing the tag and GitHub Release.
|
|
128
|
+
The npm publication workflow must first check out protected `main`, verify that the peeled release tag is an ancestor of current `origin/main`, validate the version directly from the tagged `package.json`, and only then check out or execute tag-controlled code. It has no manual-dispatch bypass.
|
|
128
129
|
7. Wait for publishing workflows, then bind every proof to the exact released commit: verify the tag/ref, GitHub Release, npm version and intended dist-tag, required CI, CodeQL, and the explicitly dispatched release-only Pages build from the exact immutable `vX.Y.Z` tag. Never dispatch Pages from `main` or another branch. Verify live `llms.txt`, `skills.json`, catalog and plugin routes, and the legacy redirect bridge; do not accept a successful run for a different SHA.
|
|
129
130
|
8. After npm confirms `X.Y.Z` as the published dist-tag, discover every already-configured local AAS MCP host from its real configuration and update each one to the exact same package version before declaring the release complete. Updating existing AAS host entries is part of the release; creating a previously absent host configuration still requires explicit authorization.
|
|
130
131
|
- Use the published package's `aas mcp configure` two-pass flow: first preview the change, then repeat the identical command with its approval digest. Supply absolute host-config, cache, and backup paths; require a backup when replacing an existing configuration.
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -176,6 +176,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function csvCell(value) {
|
|
180
|
+
if (value === null || value === undefined) return '';
|
|
181
|
+
let text = String(value);
|
|
182
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
183
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
184
|
+
}
|
|
185
|
+
|
|
179
186
|
// Download dataset items
|
|
180
187
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
181
188
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -200,7 +207,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
200
207
|
// CSV output
|
|
201
208
|
if (data.length > 0) {
|
|
202
209
|
const fieldnames = Object.keys(data[0]);
|
|
203
|
-
const csvLines = [fieldnames.join(',')];
|
|
210
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
204
211
|
|
|
205
212
|
for (const row of data) {
|
|
206
213
|
const values = fieldnames.map((key) => {
|
|
@@ -213,15 +220,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
213
220
|
value = JSON.stringify(value) || '';
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
|
|
217
|
-
if (value === null || value === undefined) {
|
|
218
|
-
return '';
|
|
219
|
-
}
|
|
220
|
-
const strValue = String(value);
|
|
221
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
222
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
223
|
-
}
|
|
224
|
-
return strValue;
|
|
223
|
+
return csvCell(value);
|
|
225
224
|
});
|
|
226
225
|
csvLines.push(values.join(','));
|
|
227
226
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -170,6 +170,13 @@ async function pollUntilComplete(token, runId, timeout, interval) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function csvCell(value) {
|
|
174
|
+
if (value === null || value === undefined) return '';
|
|
175
|
+
let text = String(value);
|
|
176
|
+
if (/^[\u0000-\u0020]*[=+@-]/.test(text)) text = `'${text}`;
|
|
177
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
// Download dataset items
|
|
174
181
|
async function downloadResults(token, datasetId, outputPath, format) {
|
|
175
182
|
const url = `https://api.apify.com/v2/datasets/${datasetId}/items?token=${encodeURIComponent(token)}&format=json`;
|
|
@@ -194,7 +201,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
194
201
|
// CSV output
|
|
195
202
|
if (data.length > 0) {
|
|
196
203
|
const fieldnames = Object.keys(data[0]);
|
|
197
|
-
const csvLines = [fieldnames.join(',')];
|
|
204
|
+
const csvLines = [fieldnames.map(csvCell).join(',')];
|
|
198
205
|
|
|
199
206
|
for (const row of data) {
|
|
200
207
|
const values = fieldnames.map((key) => {
|
|
@@ -207,15 +214,7 @@ async function downloadResults(token, datasetId, outputPath, format) {
|
|
|
207
214
|
value = JSON.stringify(value) || '';
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
|
|
211
|
-
if (value === null || value === undefined) {
|
|
212
|
-
return '';
|
|
213
|
-
}
|
|
214
|
-
const strValue = String(value);
|
|
215
|
-
if (strValue.includes(',') || strValue.includes('"') || strValue.includes('\n')) {
|
|
216
|
-
return `"${strValue.replace(/"/g, '""')}"`;
|
|
217
|
-
}
|
|
218
|
-
return strValue;
|
|
217
|
+
return csvCell(value);
|
|
219
218
|
});
|
|
220
219
|
csvLines.push(values.join(','));
|
|
221
220
|
}
|
|
@@ -247,6 +247,22 @@ function escapeHtml(str) {
|
|
|
247
247
|
return (str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
function safeHttpUrl(value) {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = new URL(String(value || '').trim());
|
|
253
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : '';
|
|
254
|
+
} catch {
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function externalLink(url, label, extra = '') {
|
|
260
|
+
const safe = safeHttpUrl(url);
|
|
261
|
+
return safe
|
|
262
|
+
? `<a href="${escapeHtml(safe)}" target="_blank" rel="noopener noreferrer"${extra}>${escapeHtml(label || safe)}</a>`
|
|
263
|
+
: escapeHtml(label || url || '');
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
function mdToHtml(md) {
|
|
251
267
|
const lines = md.split('\n');
|
|
252
268
|
const out = [];
|
|
@@ -283,7 +299,7 @@ function mdToHtml(md) {
|
|
|
283
299
|
url = url.slice(0, -1);
|
|
284
300
|
}
|
|
285
301
|
if (!url) return raw;
|
|
286
|
-
return
|
|
302
|
+
return `${externalLink(url, url)}${trail}`;
|
|
287
303
|
});
|
|
288
304
|
out.push(`<li>${text}</li>`);
|
|
289
305
|
continue;
|
|
@@ -394,7 +410,7 @@ const tableRows = competitorRows.map(c => {
|
|
|
394
410
|
? `<a href="competitors/${c.slug}.html">${escapeHtml(c.competitor_name)}</a>`
|
|
395
411
|
: escapeHtml(c.competitor_name);
|
|
396
412
|
const websiteHtml = c.website
|
|
397
|
-
? `<span class="muted-line"
|
|
413
|
+
? `<span class="muted-line">${externalLink(c.website, c.website.replace(/^https?:\/\/(www\.)?/, ''), ' style="color:var(--muted);"')}</span>`
|
|
398
414
|
: '';
|
|
399
415
|
// Pricing: prefer pipe-split summary; if there are no pipes (prose drift), truncate hard.
|
|
400
416
|
let pricingShort = splitPipes(c.pricing_tiers).slice(0, 3).join(' · ');
|
|
@@ -573,7 +589,7 @@ for (const c of competitorRows) {
|
|
|
573
589
|
const mentionsHtml = c.mentions.length
|
|
574
590
|
? c.mentions.map(m => {
|
|
575
591
|
const dateStr = m.date ? `<span class="muted-line" style="color:var(--muted);font-size:0.75rem;margin-left:auto;">${escapeHtml(m.date)}</span>` : '';
|
|
576
|
-
const linkText = m.url ?
|
|
592
|
+
const linkText = m.url ? externalLink(m.url, m.title || m.url) : escapeHtml(m.title);
|
|
577
593
|
const snippet = m.snippet ? ` — <span style="color:var(--muted);">${escapeHtml(m.snippet)}</span>` : '';
|
|
578
594
|
return `<div class="mention-item"><span class="src-pill src-${escapeHtml(m.sourceType)}">${escapeHtml(m.sourceType)}</span><div style="flex:1;">${linkText}${snippet}</div>${dateStr}</div>`;
|
|
579
595
|
}).join('\n')
|
|
@@ -581,7 +597,7 @@ for (const c of competitorRows) {
|
|
|
581
597
|
|
|
582
598
|
const benchmarksHtml = c.benchmarks.length
|
|
583
599
|
? `<ul>${c.benchmarks.map(b => {
|
|
584
|
-
const link = b.url ?
|
|
600
|
+
const link = b.url ? externalLink(b.url, b.title || b.url) : escapeHtml(b.title);
|
|
585
601
|
const src = b.source ? ` <span style="color:var(--muted);">(${escapeHtml(b.source)})</span>` : '';
|
|
586
602
|
const finding = b.finding ? ` — ${escapeHtml(b.finding)}` : '';
|
|
587
603
|
return `<li>${link}${src}${finding}</li>`;
|
|
@@ -622,7 +638,7 @@ for (const c of competitorRows) {
|
|
|
622
638
|
<header>
|
|
623
639
|
<h1>${escapeHtml(c.competitor_name)}</h1>
|
|
624
640
|
<div class="meta">
|
|
625
|
-
${c.website ?
|
|
641
|
+
${c.website ? externalLink(c.website, c.website) : ''}
|
|
626
642
|
${c.tagline ? ` · ${escapeHtml(c.tagline)}` : ''}
|
|
627
643
|
</div>
|
|
628
644
|
</header>${screenshotsHtml}
|
|
@@ -838,7 +854,7 @@ const sourceFilterButtons = ['All', ...sourceTypes].map(t =>
|
|
|
838
854
|
).join('');
|
|
839
855
|
|
|
840
856
|
const mentionItems = allMentions.map(m => {
|
|
841
|
-
const link = m.url ?
|
|
857
|
+
const link = m.url ? externalLink(m.url, m.title || m.url) : escapeHtml(m.title);
|
|
842
858
|
const snippet = m.snippet ? `<div class="snippet">${escapeHtml(m.snippet)}</div>` : '';
|
|
843
859
|
const date = m.date ? `<span class="date">${escapeHtml(m.date)}</span>` : '';
|
|
844
860
|
return `<div class="mention" data-type="${escapeHtml(m.sourceType)}">
|