opencode-skills-collection 4.0.47 → 4.0.49
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 +11 -2
- package/bundled-skills/agent-qa-authoring/SKILL.md +3 -1
- package/bundled-skills/agy-delegate/SKILL.md +1 -1
- package/bundled-skills/aider-delegate/SKILL.md +1 -1
- package/bundled-skills/api-rate-limit-handler/SKILL.md +1 -1
- package/bundled-skills/atlas-cloud-media/SKILL.md +52 -18
- package/bundled-skills/babysit-pr/SKILL.md +1 -1
- package/bundled-skills/boost-asio-pro/references/pre-cpp20.md +8 -1
- package/bundled-skills/check-identity-pack/SKILL.md +88 -0
- package/bundled-skills/claude-delegate/SKILL.md +1 -1
- package/bundled-skills/cline-delegate/SKILL.md +1 -1
- package/bundled-skills/codex-delegate/SKILL.md +1 -1
- package/bundled-skills/commandcode-delegate/SKILL.md +1 -1
- package/bundled-skills/copilot-delegate/SKILL.md +1 -1
- package/bundled-skills/cursor-delegate/SKILL.md +1 -1
- package/bundled-skills/detect-ai-text/SKILL.md +91 -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/gemini-cli-skills.md +1 -1
- 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/extract-document-data/SKILL.md +99 -0
- package/bundled-skills/falsify/SKILL.md +18 -1
- package/bundled-skills/find-matching-tenders/SKILL.md +100 -0
- package/bundled-skills/graceful-shutdown/SKILL.md +76 -70
- package/bundled-skills/grok-delegate/SKILL.md +1 -1
- package/bundled-skills/inngest/SKILL.md +2 -0
- package/bundled-skills/kimi-delegate/SKILL.md +1 -1
- package/bundled-skills/liuguang-banlan-ui/SKILL.md +6 -4
- package/bundled-skills/liuguang-banlan-ui/scripts/manifest_parser.py +178 -0
- package/bundled-skills/liuguang-banlan-ui/scripts/measure_preview.py +14 -17
- package/bundled-skills/liuguang-banlan-ui/scripts/validate_manifest.py +12 -16
- package/bundled-skills/lovable-cleanup/SKILL.md +32 -6
- package/bundled-skills/lovable-cleanup/references/favicon-vercel-cleanup.md +106 -0
- package/bundled-skills/lovable-cleanup/scripts/write-transparent-favicon.js +88 -0
- package/bundled-skills/multi-source-search/scripts/validate_report.py +5 -0
- package/bundled-skills/omp-delegate/SKILL.md +1 -1
- package/bundled-skills/opencode-delegate/SKILL.md +1 -1
- package/bundled-skills/pentest-tools/references/pentest-ai-agents-matrix.md +8 -5
- package/bundled-skills/pi-delegate/SKILL.md +1 -1
- package/bundled-skills/prompt-caching/SKILL.md +3 -1
- package/bundled-skills/qoder-delegate/SKILL.md +1 -1
- package/bundled-skills/screen-adverse-media/SKILL.md +100 -0
- package/bundled-skills/slideops/SKILL.md +201 -0
- package/bundled-skills/trigger-dev/SKILL.md +2 -0
- package/bundled-skills/unsloth-finetuning/SKILL.md +45 -2
- package/bundled-skills/upstash-qstash/SKILL.md +1 -1
- package/bundled-skills/upstash-ratelimit/SKILL.md +186 -0
- package/bundled-skills/upstash-redis/SKILL.md +156 -0
- package/bundled-skills/verify-citations/SKILL.md +88 -0
- package/bundled-skills/verify-document/SKILL.md +94 -0
- package/bundled-skills/vibe-delegate/SKILL.md +1 -1
- package/bundled-skills/warp-delegate/SKILL.md +1 -1
- package/bundled-skills/zcode-delegate/SKILL.md +1 -1
- package/package.json +1 -1
- package/skills_index.json +347 -54
- package/bundled-skills/ui-slop-score/SKILL.md +0 -80
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Parse the data-only subset used by Liuguang theme manifests."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MAX_MANIFEST_BYTES = 256 * 1024
|
|
13
|
+
MAX_NESTING_DEPTH = 64
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ManifestSyntaxError(ValueError):
|
|
17
|
+
"""Raised when a manifest contains code or unsupported JavaScript syntax."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Token:
|
|
22
|
+
kind: str
|
|
23
|
+
value: str
|
|
24
|
+
offset: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
TOKEN_RE = re.compile(
|
|
28
|
+
r"""
|
|
29
|
+
(?P<whitespace>\s+)
|
|
30
|
+
| (?P<line_comment>//[^\r\n]*)
|
|
31
|
+
| (?P<block_comment>/\*.*?\*/)
|
|
32
|
+
| (?P<string>"(?:\\["\\/bfnrt]|\\u[0-9A-Fa-f]{4}|[^"\\\x00-\x1f])*")
|
|
33
|
+
| (?P<number>-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)
|
|
34
|
+
| (?P<identifier>[A-Za-z_$][A-Za-z0-9_$]*)
|
|
35
|
+
| (?P<punctuation>[{}\[\]:,;=.])
|
|
36
|
+
""",
|
|
37
|
+
re.VERBOSE | re.DOTALL,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tokenize(source: str) -> list[Token]:
|
|
42
|
+
tokens: list[Token] = []
|
|
43
|
+
offset = 0
|
|
44
|
+
while offset < len(source):
|
|
45
|
+
match = TOKEN_RE.match(source, offset)
|
|
46
|
+
if match is None:
|
|
47
|
+
raise ManifestSyntaxError(f"unsupported syntax at byte offset {offset}")
|
|
48
|
+
kind = match.lastgroup
|
|
49
|
+
if kind not in {"whitespace", "line_comment", "block_comment"}:
|
|
50
|
+
tokens.append(Token(kind or "unknown", match.group(0), offset))
|
|
51
|
+
offset = match.end()
|
|
52
|
+
tokens.append(Token("eof", "", len(source)))
|
|
53
|
+
return tokens
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Parser:
|
|
57
|
+
def __init__(self, source: str) -> None:
|
|
58
|
+
self.tokens = tokenize(source)
|
|
59
|
+
self.index = 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def current(self) -> Token:
|
|
63
|
+
return self.tokens[self.index]
|
|
64
|
+
|
|
65
|
+
def take(self, value: str | None = None, kind: str | None = None) -> Token:
|
|
66
|
+
token = self.current
|
|
67
|
+
if value is not None and token.value != value:
|
|
68
|
+
raise ManifestSyntaxError(
|
|
69
|
+
f"expected {value!r} at byte offset {token.offset}, found {token.value!r}"
|
|
70
|
+
)
|
|
71
|
+
if kind is not None and token.kind != kind:
|
|
72
|
+
raise ManifestSyntaxError(
|
|
73
|
+
f"expected {kind} at byte offset {token.offset}, found {token.value!r}"
|
|
74
|
+
)
|
|
75
|
+
self.index += 1
|
|
76
|
+
return token
|
|
77
|
+
|
|
78
|
+
def accept(self, value: str) -> bool:
|
|
79
|
+
if self.current.value != value:
|
|
80
|
+
return False
|
|
81
|
+
self.index += 1
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def parse(self) -> dict:
|
|
85
|
+
self.take("window")
|
|
86
|
+
self.take(".")
|
|
87
|
+
self.take("SPECTRAL_THEME")
|
|
88
|
+
self.take("=")
|
|
89
|
+
value = self.parse_value(0)
|
|
90
|
+
self.accept(";")
|
|
91
|
+
self.take(kind="eof")
|
|
92
|
+
if not isinstance(value, dict):
|
|
93
|
+
raise ManifestSyntaxError("SPECTRAL_THEME must be an object")
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
def parse_value(self, depth: int):
|
|
97
|
+
if depth > MAX_NESTING_DEPTH:
|
|
98
|
+
raise ManifestSyntaxError("manifest nesting limit exceeded")
|
|
99
|
+
token = self.current
|
|
100
|
+
if token.value == "{":
|
|
101
|
+
return self.parse_object(depth + 1)
|
|
102
|
+
if token.value == "[":
|
|
103
|
+
return self.parse_array(depth + 1)
|
|
104
|
+
if token.kind == "string":
|
|
105
|
+
self.index += 1
|
|
106
|
+
return json.loads(token.value)
|
|
107
|
+
if token.kind == "number":
|
|
108
|
+
self.index += 1
|
|
109
|
+
try:
|
|
110
|
+
return (
|
|
111
|
+
float(token.value)
|
|
112
|
+
if any(mark in token.value for mark in ".eE")
|
|
113
|
+
else int(token.value)
|
|
114
|
+
)
|
|
115
|
+
except ValueError as error:
|
|
116
|
+
raise ManifestSyntaxError("numeric literal exceeds the supported range") from error
|
|
117
|
+
if token.kind == "identifier" and token.value in {"true", "false", "null"}:
|
|
118
|
+
self.index += 1
|
|
119
|
+
return {"true": True, "false": False, "null": None}[token.value]
|
|
120
|
+
raise ManifestSyntaxError(
|
|
121
|
+
f"only data literals are allowed at byte offset {token.offset}; found {token.value!r}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def parse_object(self, depth: int) -> dict:
|
|
125
|
+
result: dict = {}
|
|
126
|
+
self.take("{")
|
|
127
|
+
if self.accept("}"):
|
|
128
|
+
return result
|
|
129
|
+
while True:
|
|
130
|
+
token = self.current
|
|
131
|
+
if token.kind == "identifier":
|
|
132
|
+
key = self.take(kind="identifier").value
|
|
133
|
+
elif token.kind == "string":
|
|
134
|
+
key = json.loads(self.take(kind="string").value)
|
|
135
|
+
else:
|
|
136
|
+
raise ManifestSyntaxError(
|
|
137
|
+
f"object keys must be identifiers or strings at byte offset {token.offset}"
|
|
138
|
+
)
|
|
139
|
+
if key in result:
|
|
140
|
+
raise ManifestSyntaxError(f"duplicate object key: {key}")
|
|
141
|
+
self.take(":")
|
|
142
|
+
result[key] = self.parse_value(depth)
|
|
143
|
+
if self.accept("}"):
|
|
144
|
+
return result
|
|
145
|
+
self.take(",")
|
|
146
|
+
if self.accept("}"):
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
def parse_array(self, depth: int) -> list:
|
|
150
|
+
result: list = []
|
|
151
|
+
self.take("[")
|
|
152
|
+
if self.accept("]"):
|
|
153
|
+
return result
|
|
154
|
+
while True:
|
|
155
|
+
result.append(self.parse_value(depth))
|
|
156
|
+
if self.accept("]"):
|
|
157
|
+
return result
|
|
158
|
+
self.take(",")
|
|
159
|
+
if self.accept("]"):
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_manifest(source: str) -> dict:
|
|
164
|
+
return Parser(source).parse()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def load_manifest(path: pathlib.Path) -> dict:
|
|
168
|
+
with path.open("rb") as manifest:
|
|
169
|
+
raw = manifest.read(MAX_MANIFEST_BYTES + 1)
|
|
170
|
+
if len(raw) > MAX_MANIFEST_BYTES:
|
|
171
|
+
raise ManifestSyntaxError(
|
|
172
|
+
f"manifest exceeds the {MAX_MANIFEST_BYTES}-byte size limit"
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
source = raw.decode("utf-8-sig")
|
|
176
|
+
except UnicodeDecodeError as error:
|
|
177
|
+
raise ManifestSyntaxError("manifest must be valid UTF-8") from error
|
|
178
|
+
return parse_manifest(source)
|
|
@@ -6,9 +6,10 @@ from __future__ import annotations
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
8
|
import pathlib
|
|
9
|
-
import subprocess
|
|
10
9
|
import sys
|
|
11
10
|
|
|
11
|
+
from manifest_parser import ManifestSyntaxError, load_manifest
|
|
12
|
+
|
|
12
13
|
try:
|
|
13
14
|
import numpy as np
|
|
14
15
|
from PIL import Image
|
|
@@ -29,21 +30,6 @@ except ModuleNotFoundError as exc:
|
|
|
29
30
|
raise SystemExit(2)
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def load_config(path: pathlib.Path) -> dict:
|
|
33
|
-
source = (
|
|
34
|
-
"global.window={};require(require('path').resolve(process.argv[1]));"
|
|
35
|
-
"process.stdout.write(JSON.stringify(window.SPECTRAL_THEME));"
|
|
36
|
-
)
|
|
37
|
-
result = subprocess.run(
|
|
38
|
-
["node", "-e", source, str(path)],
|
|
39
|
-
check=True,
|
|
40
|
-
capture_output=True,
|
|
41
|
-
text=True,
|
|
42
|
-
encoding="utf-8",
|
|
43
|
-
)
|
|
44
|
-
return json.loads(result.stdout)
|
|
45
|
-
|
|
46
|
-
|
|
47
33
|
def srgb_to_oklab(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
48
34
|
linear = np.where(
|
|
49
35
|
rgb <= 0.04045,
|
|
@@ -129,7 +115,18 @@ def main() -> None:
|
|
|
129
115
|
parser.add_argument("--image", required=True, type=pathlib.Path)
|
|
130
116
|
parser.add_argument("--config", required=True, type=pathlib.Path)
|
|
131
117
|
args = parser.parse_args()
|
|
132
|
-
|
|
118
|
+
try:
|
|
119
|
+
config = load_manifest(args.config.resolve())
|
|
120
|
+
except (ManifestSyntaxError, OSError) as error:
|
|
121
|
+
print(
|
|
122
|
+
json.dumps(
|
|
123
|
+
{"status": "invalid", "errors": [str(error)]},
|
|
124
|
+
ensure_ascii=False,
|
|
125
|
+
indent=2,
|
|
126
|
+
),
|
|
127
|
+
file=sys.stderr,
|
|
128
|
+
)
|
|
129
|
+
raise SystemExit(1) from error
|
|
133
130
|
payload = {
|
|
134
131
|
"schemaVersion": "1.0",
|
|
135
132
|
"theme": config["label"],
|
|
@@ -8,23 +8,9 @@ import json
|
|
|
8
8
|
import math
|
|
9
9
|
import pathlib
|
|
10
10
|
import re
|
|
11
|
-
import subprocess
|
|
12
11
|
import sys
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
def load_manifest(path: pathlib.Path) -> dict:
|
|
16
|
-
program = (
|
|
17
|
-
"global.window={};require(require('path').resolve(process.argv[1]));"
|
|
18
|
-
"process.stdout.write(JSON.stringify(window.SPECTRAL_THEME));"
|
|
19
|
-
)
|
|
20
|
-
result = subprocess.run(
|
|
21
|
-
["node", "-e", program, str(path)],
|
|
22
|
-
check=True,
|
|
23
|
-
capture_output=True,
|
|
24
|
-
text=True,
|
|
25
|
-
encoding="utf-8",
|
|
26
|
-
)
|
|
27
|
-
return json.loads(result.stdout)
|
|
13
|
+
from manifest_parser import ManifestSyntaxError, load_manifest
|
|
28
14
|
|
|
29
15
|
|
|
30
16
|
def require(condition: bool, message: str, errors: list[str]) -> None:
|
|
@@ -161,7 +147,17 @@ def main() -> None:
|
|
|
161
147
|
parser = argparse.ArgumentParser()
|
|
162
148
|
parser.add_argument("config", type=pathlib.Path)
|
|
163
149
|
args = parser.parse_args()
|
|
164
|
-
|
|
150
|
+
try:
|
|
151
|
+
config = load_manifest(args.config.resolve())
|
|
152
|
+
except (ManifestSyntaxError, OSError) as error:
|
|
153
|
+
print(
|
|
154
|
+
json.dumps(
|
|
155
|
+
{"status": "invalid", "errors": [str(error)]},
|
|
156
|
+
ensure_ascii=False,
|
|
157
|
+
indent=2,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
raise SystemExit(1) from error
|
|
165
161
|
errors = validate(config)
|
|
166
162
|
if errors:
|
|
167
163
|
print(json.dumps({"status": "invalid", "errors": errors}, ensure_ascii=False, indent=2))
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lovable-cleanup
|
|
3
|
-
description: "Audits and strips Lovable scaffolding from Vite + React projects — removes lovable-tagger, swaps placeholder assets, prunes unused Radix deps,
|
|
3
|
+
description: "Audits and strips Lovable scaffolding from Vite + React projects — removes lovable-tagger, swaps placeholder assets, prunes unused Radix deps, cleans generated docs, and neutralizes stale favicon/CDN caching so the codebase ships as yours."
|
|
4
4
|
risk: safe
|
|
5
5
|
source: community
|
|
6
6
|
source_repo: whoisabhishekadhikari/lovable-cleanup
|
|
7
7
|
source_type: community
|
|
8
8
|
author: whoisabhishekadhikari
|
|
9
9
|
date_added: "2026-06-13"
|
|
10
|
-
|
|
10
|
+
date_updated: "2026-08-31"
|
|
11
|
+
version: "2.0.0"
|
|
12
|
+
tags: [lovable, cleanup, vite, react, shadcn, devtools, vercel, favicon]
|
|
11
13
|
tools: [claude, cursor, codex, antigravity, gemini-cli]
|
|
12
14
|
---
|
|
13
15
|
|
|
@@ -23,7 +25,7 @@ tools: [claude, cursor, codex, antigravity, gemini-cli]
|
|
|
23
25
|
Lovable (lovable.dev) bootstraps Vite + React + shadcn/ui projects with its own tagger
|
|
24
26
|
dependency, branding, placeholder assets, and generated markdown docs baked in. Most
|
|
25
27
|
developers export from Lovable and want a clean, ownable codebase before shipping or
|
|
26
|
-
open-sourcing. This skill covers all
|
|
28
|
+
open-sourcing. This skill covers all 15 areas where Lovable leaves fingerprints.
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
@@ -75,6 +77,8 @@ shadcn components via the `asChild` prop.
|
|
|
75
77
|
6. Environment & Git (Areas 9 & 12) — security sweep
|
|
76
78
|
7. SEO / deploy (Area 11) — usually a no-op; confirm and move on
|
|
77
79
|
8. Unused deps (Area 13) — safe to defer until after ship if on a deadline
|
|
80
|
+
9. Favicon / CDN cache (Area 15) — do ASAP after assets are swapped; browser
|
|
81
|
+
or CDN caching can keep the old icon visible, so verify the live response
|
|
78
82
|
|
|
79
83
|
---
|
|
80
84
|
|
|
@@ -137,7 +141,7 @@ Replace these files (keep filenames, swap content):
|
|
|
137
141
|
|
|
138
142
|
| File | Action |
|
|
139
143
|
|---|---|
|
|
140
|
-
| `favicon.ico` |
|
|
144
|
+
| `favicon.ico` | Overwrite with real icon — do NOT just delete, see Area 15 |
|
|
141
145
|
| `favicon.png` | Replace with real icon |
|
|
142
146
|
| `og-image.png` / `logo.png` | Replace with real brand assets |
|
|
143
147
|
| `placeholder.svg` | Usually unused — safe to delete |
|
|
@@ -292,6 +296,17 @@ grep -in "lovable" components.json eslint.config.js
|
|
|
292
296
|
|
|
293
297
|
---
|
|
294
298
|
|
|
299
|
+
### Area 15 · Favicon removal & stale CDN caches (Vercel)
|
|
300
|
+
|
|
301
|
+
Lovable ships a default `favicon.ico` that browsers auto-request from site root
|
|
302
|
+
and that can remain visible after cleanup through browser or CDN caching. Handle
|
|
303
|
+
the four steps — replace the path, link all icon flavours, keep unversioned icon
|
|
304
|
+
URLs revalidatable, and verify after deploy — then purge the confirmed Vercel
|
|
305
|
+
project cache only if the live response stays stale. Full commands/JSON live in
|
|
306
|
+
[references/favicon-vercel-cleanup.md](references/favicon-vercel-cleanup.md).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
295
310
|
## Master Scan Command
|
|
296
311
|
|
|
297
312
|
<!-- security-allowlist: recursive grep across project directory, read-only, no network -->
|
|
@@ -342,6 +357,9 @@ Agent:
|
|
|
342
357
|
- ✅ **Do:** Run dep removal (Areas 2 & 7) before touching source files
|
|
343
358
|
- ✅ **Do:** Skim Lovable-generated docs before deleting — may contain useful arch notes
|
|
344
359
|
- ✅ **Do:** Verify `npm run build` passes after every batch of changes
|
|
360
|
+
- ✅ **Do:** Replace favicons at the existing paths (Area 15), then verify the
|
|
361
|
+
live response and purge only the confirmed project if it remains stale
|
|
362
|
+
- ✅ **Do:** Deploy replacement favicon content and cache headers in the same commit
|
|
345
363
|
- ✅ **Do:** Replace OG image before launch — it directly affects social sharing previews
|
|
346
364
|
- ❌ **Don't:** Remove `@radix-ui/react-slot` — it's an indirect dep of most shadcn components
|
|
347
365
|
- ❌ **Don't:** Leave empty env vars like `LOVABLE_PROJECT_ID=` — delete the whole line
|
|
@@ -350,8 +368,8 @@ Agent:
|
|
|
350
368
|
|
|
351
369
|
## Limitations
|
|
352
370
|
|
|
353
|
-
- This skill does not create or source brand assets (favicons, OG images)
|
|
354
|
-
|
|
371
|
+
- This skill does not create or source real brand assets (favicons, OG images). Area 15
|
|
372
|
+
generates a transparent placeholder ICO only — the user must supply genuine artwork.
|
|
355
373
|
- Dep pruning (Area 13) is safe but not foolproof — some Radix packages are indirect deps
|
|
356
374
|
not caught by a direct `grep`. Always verify with `npm run build`.
|
|
357
375
|
- The skill does not modify `components.json` aliases automatically — it only scans and
|
|
@@ -380,6 +398,14 @@ the `from '@radix-ui/...'` import to find which component depends on it.
|
|
|
380
398
|
**Solution:** Check for a `<Helmet>` or `<Head>` component in `src/App.tsx` or a layout
|
|
381
399
|
wrapper — React-level title tags override `index.html` at runtime.
|
|
382
400
|
|
|
401
|
+
### Problem: Old favicon still serving after deletion (Vercel)
|
|
402
|
+
|
|
403
|
+
**Symptoms:** `curl -sI https://<domain>/favicon.ico` returns the old ETag with
|
|
404
|
+
`x-vercel-cache: HIT` after the replacement deployment.
|
|
405
|
+
**Solution:** Overwrite `public/favicon.ico` with replacement content (a transparent
|
|
406
|
+
1×1 ICO if no real asset yet), verify the custom domain, then use the explicit
|
|
407
|
+
Vercel CDN purge only if the response remains stale — see Area 15.
|
|
408
|
+
|
|
383
409
|
---
|
|
384
410
|
|
|
385
411
|
## Related Skills
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Favicon removal & stale CDN caches (Vercel)
|
|
2
|
+
|
|
3
|
+
Lovable ships a default `favicon.ico`, and browsers auto-request it from site
|
|
4
|
+
root even when `index.html` links a different icon. Browser and CDN layers can
|
|
5
|
+
therefore keep showing the old icon after a replacement deployment. Treat the
|
|
6
|
+
cleanup as both a file-path and cache-verification problem.
|
|
7
|
+
|
|
8
|
+
## 1 · Overwrite in place, don't delete
|
|
9
|
+
|
|
10
|
+
Prefer replacing the same path in the deployment instead of deleting it. The
|
|
11
|
+
production URL then keeps returning a valid icon while the new bytes establish
|
|
12
|
+
a new content identity; browsers that request `/favicon.ico` implicitly do not
|
|
13
|
+
fall back to an old cached asset merely because the HTML link changed.
|
|
14
|
+
|
|
15
|
+
If no real brand icon is ready, use the bundled helper to write a valid
|
|
16
|
+
transparent 1×1 ICO. It resolves the physical project root, rejects a symlinked
|
|
17
|
+
`public/` directory or favicon target, writes an exclusive same-directory
|
|
18
|
+
temporary file with no-follow semantics where available, then atomically
|
|
19
|
+
renames it into place.
|
|
20
|
+
|
|
21
|
+
<!-- security-allowlist: writes a 70-byte ICO into the project's public/, local only -->
|
|
22
|
+
```bash
|
|
23
|
+
node "<skill-dir>/scripts/write-transparent-favicon.js" "$PWD"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 2 · Link all icon flavours in `index.html`
|
|
27
|
+
|
|
28
|
+
Browsers may request `/favicon.ico` even without a link, so keep that path and
|
|
29
|
+
the modern/Apple entry points available:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
33
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
34
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`apple-touch-icon.png` must be a real PNG (recommended 180×180). A solid brand-
|
|
38
|
+
colour square is an acceptable placeholder; flag it for later replacement.
|
|
39
|
+
|
|
40
|
+
## 3 · Keep unversioned icon URLs revalidatable (`vercel.json`)
|
|
41
|
+
|
|
42
|
+
Vercel documents `public, max-age=0, must-revalidate` as its default response
|
|
43
|
+
policy and recommends long-lived `immutable` caching for content-hashed assets.
|
|
44
|
+
The standard favicon entry points below are not content-hashed, so keep them
|
|
45
|
+
revalidatable unless the HTML points at a versioned filename:
|
|
46
|
+
|
|
47
|
+
- `/favicon.ico`, `/favicon.svg`, `/apple-touch-icon.png` →
|
|
48
|
+
`public, max-age=0, must-revalidate`
|
|
49
|
+
- A content-hashed asset such as `/favicon-a1b2c3.svg` may use
|
|
50
|
+
`public, max-age=31536000, immutable`.
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"headers": [
|
|
55
|
+
{
|
|
56
|
+
"source": "/favicon.ico",
|
|
57
|
+
"headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"source": "/favicon.svg",
|
|
61
|
+
"headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"source": "/apple-touch-icon.png",
|
|
65
|
+
"headers": [{ "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" }]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Never mark an unversioned icon URL `immutable`: it tells browsers not to
|
|
72
|
+
revalidate for the max-age lifetime, so a replacement may not propagate for up
|
|
73
|
+
to a year. Use `immutable` only on content-hashed URLs.
|
|
74
|
+
|
|
75
|
+
## 4 · Verify after deploy
|
|
76
|
+
|
|
77
|
+
<!-- security-allowlist: remote curl header check of own domain, read-only -->
|
|
78
|
+
```bash
|
|
79
|
+
curl -sI https://YOUR-DOMAIN/favicon.ico \
|
|
80
|
+
| grep -i "cache-control\|etag\|x-vercel-cache"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Expect the replacement ETag and `Cache-Control: public, max-age=0,
|
|
84
|
+
must-revalidate` on the unversioned icon URLs.
|
|
85
|
+
|
|
86
|
+
If the confirmed production domain still serves the old edge response, verify
|
|
87
|
+
the linked Vercel project and team first, ask for explicit approval, then purge
|
|
88
|
+
that project's CDN cache:
|
|
89
|
+
|
|
90
|
+
<!-- security-allowlist: explicit remote cache purge for the confirmed Vercel project; requires user approval -->
|
|
91
|
+
```bash
|
|
92
|
+
vercel cache purge --type cdn
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Re-run the header check after the purge. Do not purge a project inferred only
|
|
96
|
+
from the current directory or a preview URL.
|
|
97
|
+
|
|
98
|
+
**Gotcha — the staging URL:** a `*.vercel.app` preview may be SSO-protected
|
|
99
|
+
(`_vercel_sso_nonce` 302) and wrap deploys in a provider frame that injects
|
|
100
|
+
platform branding. Always verify icons on the real custom domain.
|
|
101
|
+
|
|
102
|
+
## Official references
|
|
103
|
+
|
|
104
|
+
- [Vercel Cache-Control headers](https://vercel.com/docs/caching/cache-control-headers)
|
|
105
|
+
- [Vercel CDN cache](https://vercel.com/docs/caching/cdn-cache)
|
|
106
|
+
- [Vercel cache purge CLI](https://vercel.com/docs/cli/cache)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
function fail(message) {
|
|
9
|
+
throw new Error(`Refusing favicon write: ${message}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function existingStat(target) {
|
|
13
|
+
try {
|
|
14
|
+
return fs.lstatSync(target);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error && error.code === "ENOENT") return null;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function transparentIco() {
|
|
22
|
+
const bytes = Buffer.alloc(6 + 16 + 40 + 8);
|
|
23
|
+
bytes.writeUInt16LE(1, 2);
|
|
24
|
+
bytes.writeUInt16LE(1, 4);
|
|
25
|
+
bytes.writeUInt8(1, 6);
|
|
26
|
+
bytes.writeUInt8(1, 7);
|
|
27
|
+
bytes.writeUInt16LE(1, 10);
|
|
28
|
+
bytes.writeUInt16LE(32, 12);
|
|
29
|
+
bytes.writeUInt32LE(40 + 8, 14);
|
|
30
|
+
bytes.writeUInt32LE(22, 18);
|
|
31
|
+
bytes.writeUInt32LE(40, 22);
|
|
32
|
+
bytes.writeInt32LE(1, 26);
|
|
33
|
+
bytes.writeInt32LE(2, 30);
|
|
34
|
+
bytes.writeUInt16LE(1, 34);
|
|
35
|
+
bytes.writeUInt16LE(32, 36);
|
|
36
|
+
bytes.writeUInt32LE(0, 38);
|
|
37
|
+
bytes.writeUInt32LE(8, 42);
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function main() {
|
|
42
|
+
const requestedRoot = path.resolve(process.argv[2] || process.cwd());
|
|
43
|
+
const projectRoot = fs.realpathSync(requestedRoot);
|
|
44
|
+
const publicDirectory = path.join(projectRoot, "public");
|
|
45
|
+
const publicStat = fs.lstatSync(publicDirectory);
|
|
46
|
+
if (publicStat.isSymbolicLink() || !publicStat.isDirectory()) {
|
|
47
|
+
fail("public/ must be a real directory, not a symlink or special file");
|
|
48
|
+
}
|
|
49
|
+
if (fs.realpathSync(publicDirectory) !== publicDirectory) {
|
|
50
|
+
fail("public/ resolves outside the physical project path");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const target = path.join(publicDirectory, "favicon.ico");
|
|
54
|
+
const targetStat = existingStat(target);
|
|
55
|
+
if (targetStat && targetStat.isSymbolicLink()) {
|
|
56
|
+
fail("public/favicon.ico is a symbolic link");
|
|
57
|
+
}
|
|
58
|
+
if (targetStat && !targetStat.isFile()) {
|
|
59
|
+
fail("public/favicon.ico is not a regular file");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const temporary = path.join(
|
|
63
|
+
publicDirectory,
|
|
64
|
+
`.favicon.ico.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`,
|
|
65
|
+
);
|
|
66
|
+
const flags =
|
|
67
|
+
fs.constants.O_WRONLY |
|
|
68
|
+
fs.constants.O_CREAT |
|
|
69
|
+
fs.constants.O_EXCL |
|
|
70
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
71
|
+
let descriptor;
|
|
72
|
+
try {
|
|
73
|
+
descriptor = fs.openSync(temporary, flags, 0o600);
|
|
74
|
+
fs.writeFileSync(descriptor, transparentIco());
|
|
75
|
+
fs.fsyncSync(descriptor);
|
|
76
|
+
fs.fchmodSync(descriptor, 0o644);
|
|
77
|
+
fs.closeSync(descriptor);
|
|
78
|
+
descriptor = undefined;
|
|
79
|
+
fs.renameSync(temporary, target);
|
|
80
|
+
} finally {
|
|
81
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
82
|
+
const temporaryStat = existingStat(temporary);
|
|
83
|
+
if (temporaryStat && temporaryStat.isFile()) fs.unlinkSync(temporary);
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write(`Wrote ${target}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main();
|
|
@@ -21,6 +21,11 @@ def canonical_url(value):
|
|
|
21
21
|
"""Return a conservative identity for an HTTP(S) URL, or None if invalid."""
|
|
22
22
|
if not nonempty(value):
|
|
23
23
|
return None
|
|
24
|
+
if any(
|
|
25
|
+
character.isspace() or ord(character) < 0x20 or ord(character) == 0x7F
|
|
26
|
+
for character in value
|
|
27
|
+
):
|
|
28
|
+
return None
|
|
24
29
|
try:
|
|
25
30
|
parsed = urlsplit(value)
|
|
26
31
|
hostname = parsed.hostname
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: omp-delegate
|
|
3
3
|
description: Delegate coding tasks to Oh My Pi (`omp`) only when the user explicitly
|
|
4
4
|
requests it, while the orchestrator retains review and landing responsibility.
|
|
5
|
-
risk:
|
|
5
|
+
risk: critical
|
|
6
6
|
category: agent-orchestration
|
|
7
7
|
source: https://github.com/amElnagdy/delegate-skills
|
|
8
8
|
source_repo: amElnagdy/delegate-skills
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: opencode-delegate
|
|
3
3
|
description: Delegate coding tasks to the OpenCode CLI only when the user explicitly
|
|
4
4
|
requests it, while the orchestrator retains review and landing responsibility.
|
|
5
|
-
risk:
|
|
5
|
+
risk: critical
|
|
6
6
|
category: agent-orchestration
|
|
7
7
|
source: https://github.com/amElnagdy/delegate-skills
|
|
8
8
|
source_repo: amElnagdy/delegate-skills
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
## 安装方式(外部可选)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
默认只参考其 prompt 模式,不安装外部代码。不得从可变的 `main` 分支
|
|
14
|
+
下载并直接执行安装脚本,也不得把下载内容通过管道交给 shell。
|
|
15
|
+
|
|
16
|
+
如果用户明确要求安装,先在仓库页面选择并记录一个完整的 40 位 commit
|
|
17
|
+
SHA,在私有审查目录中下载该 commit 的完整源码,核对仓库身份、许可证、
|
|
18
|
+
文件清单、安装脚本及其后续下载。只有在展示确切变更和命令并获得单独批准
|
|
19
|
+
后,才可从已审查的本地副本执行;任一依赖仍指向分支、标签、`latest` 或未审查
|
|
20
|
+
远程脚本时必须停止。
|
|
18
21
|
|
|
19
22
|
---
|
|
20
23
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: pi-delegate
|
|
3
3
|
description: Delegate coding tasks to the Pi coding agent CLI (`pi`) only when the
|
|
4
4
|
user explicitly requests it, while the orchestrator retains review and landing responsibility.
|
|
5
|
-
risk:
|
|
5
|
+
risk: critical
|
|
6
6
|
category: agent-orchestration
|
|
7
7
|
source: https://github.com/amElnagdy/delegate-skills
|
|
8
8
|
source_repo: amElnagdy/delegate-skills
|
|
@@ -34,7 +34,7 @@ Caching strategies for LLM prompts including Anthropic prompt caching, response
|
|
|
34
34
|
### Primary_tools
|
|
35
35
|
|
|
36
36
|
- Anthropic Prompt Caching - Native prompt caching in Claude API
|
|
37
|
-
- Redis - In-memory cache for responses
|
|
37
|
+
- Redis - In-memory cache for responses (ioredis on servers; @upstash/redis over HTTP on serverless, see `upstash-redis`)
|
|
38
38
|
- OpenAI Caching - Automatic caching in OpenAI API
|
|
39
39
|
|
|
40
40
|
## Patterns
|
|
@@ -91,6 +91,8 @@ import { createHash } from 'crypto';
|
|
|
91
91
|
import Redis from 'ioredis';
|
|
92
92
|
|
|
93
93
|
const redis = new Redis(process.env.REDIS_URL);
|
|
94
|
+
// Serverless/edge alternative without a persistent connection:
|
|
95
|
+
// import { Redis } from '@upstash/redis'; const redis = Redis.fromEnv(); // then use redis.set(key, value, { ex: ttl })
|
|
94
96
|
|
|
95
97
|
class ResponseCache {
|
|
96
98
|
private ttl = 3600; // 1 hour default
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: qoder-delegate
|
|
3
3
|
description: Delegate coding tasks to the Qoder CLI (`qodercli`) only when the user
|
|
4
4
|
explicitly requests it, while the orchestrator retains review and landing responsibility.
|
|
5
|
-
risk:
|
|
5
|
+
risk: critical
|
|
6
6
|
category: agent-orchestration
|
|
7
7
|
source: https://github.com/amElnagdy/delegate-skills
|
|
8
8
|
source_repo: amElnagdy/delegate-skills
|