opencode-skills-collection 4.0.24 → 4.0.26
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 +3 -1
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +2 -2
- 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/release-process.md +2 -2
- 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/plugin-submissions/aas-agent-mcp-builder/README.md +19 -0
- package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/evaluation-cases.json +74 -0
- package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/evaluation-results.json +86 -0
- package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/submission.json +41 -0
- package/bundled-skills/docs/users/aas-core.md +1 -1
- package/bundled-skills/docs/users/bundles.md +62 -60
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/faq.md +5 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +4 -2
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/plugins.md +28 -3
- package/bundled-skills/docs/users/usage.md +3 -3
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/ingest-youtube/ingest.py +1 -1
- package/bundled-skills/instagram/scripts/auth.py +42 -17
- package/bundled-skills/instagram/scripts/csv_utils.py +20 -0
- package/bundled-skills/instagram/scripts/export.py +5 -3
- package/bundled-skills/instagram/scripts/serve_api.py +2 -1
- package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +63 -37
- package/bundled-skills/loki-mode/README.md +1 -1
- package/bundled-skills/loki-mode/integrations/vibe-kanban.md +1 -1
- package/bundled-skills/loki-mode/scripts/export-to-vibe-kanban.sh +97 -26
- package/bundled-skills/macos-spm-app-packaging/assets/templates/package_app.sh +34 -1
- package/bundled-skills/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +36 -1
- package/bundled-skills/notebooklm/README.md +5 -4
- package/bundled-skills/notebooklm/SKILL.md +14 -7
- package/bundled-skills/notebooklm/references/api_reference.md +4 -3
- package/bundled-skills/notebooklm/references/usage_patterns.md +6 -4
- package/bundled-skills/notebooklm/scripts/ask_question.py +8 -16
- package/bundled-skills/notebooklm/scripts/browser_session.py +3 -2
- package/bundled-skills/notebooklm/scripts/input_safety.py +114 -0
- package/bundled-skills/notebooklm/scripts/notebook_manager.py +35 -21
- package/bundled-skills/outreachagent/SKILL.md +386 -0
- package/bundled-skills/telegram/assets/boilerplate/python/bot.py +4 -3
- package/bundled-skills/telegram/assets/boilerplate/python/webhook_server.py +2 -1
- package/bundled-skills/vercel-optimize/lib/verify-claim.mjs +22 -8
- package/bundled-skills/video-router/SKILL.md +98 -0
- package/bundled-skills/web-scraper/SKILL.md +38 -3
- package/bundled-skills/youtube-notetaker/SKILL.md +16 -10
- package/bundled-skills/youtube-notetaker/scripts/detect_slides.sh +3 -1
- package/bundled-skills/youtube-notetaker/scripts/download.sh +4 -1
- package/bundled-skills/youtube-notetaker/scripts/scratch_safety.sh +64 -0
- package/bundled-skills/youtube-notetaker/scripts/setup.sh +7 -2
- package/bundled-skills/youtube-notetaker/scripts/vtt_to_transcript.py +6 -1
- package/bundled-skills/youtube-summarizer/SKILL.md +8 -10
- package/package.json +1 -1
- package/skills_index.json +99 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Plugins for
|
|
1
|
+
# Plugins for compatible agent clients
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AAS ships first-class plugin distributions for **Claude Code** and **Codex**, plus portable [Agent Plugins 1.0](https://agent-plugins.org/specification) manifests for compatible specialized bundles.
|
|
4
4
|
|
|
5
5
|
This page explains how plugins fit beneath **AAS Core**, the orchestration layer for Codex and Claude Code. Plugins and direct installs deliver skill payloads; Core exposes the complete catalog and validates, records, and plans the exact stack chosen by the agent.
|
|
6
6
|
|
|
@@ -44,10 +44,11 @@ Choose the full library when you want:
|
|
|
44
44
|
|
|
45
45
|
### Plugin install
|
|
46
46
|
|
|
47
|
-
Use the plugin marketplace
|
|
47
|
+
Use the plugin marketplace, repo-local metadata, or portable package when you want a curated, installable distribution:
|
|
48
48
|
|
|
49
49
|
- **Claude Code** uses `.claude-plugin/marketplace.json` and `.claude-plugin/plugin.json`
|
|
50
50
|
- **Codex** uses `.agents/plugins/marketplace.json` and `plugins/agentic-awesome-skills/.codex-plugin/plugin.json`
|
|
51
|
+
- **Agent Plugins clients** load a specialized bundle's root `plugin.json` and discover its skills from the fixed `skills/` directory
|
|
51
52
|
|
|
52
53
|
Choose the plugin route when you want:
|
|
53
54
|
|
|
@@ -103,6 +104,28 @@ Use a specialized plugin when you want:
|
|
|
103
104
|
- a curated subset instead of the broad root plugin
|
|
104
105
|
- a plugin with a clear promise, such as building web apps, auditing security, maintaining OSS repos, automating documents, or creating growth content
|
|
105
106
|
|
|
107
|
+
## Portable Agent Plugins surface
|
|
108
|
+
|
|
109
|
+
The [Agent Plugins specification](https://agent-plugins.org/specification) defines a shared package floor for Agent Skills and MCP server configuration. A portable AAS bundle has this shape:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
plugins/agentic-bundle-<bundle-id>/
|
|
113
|
+
├── plugin.json
|
|
114
|
+
└── skills/
|
|
115
|
+
└── <skill-id>/
|
|
116
|
+
└── SKILL.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The root `plugin.json` targets the canonical `1.0.0` schema. It deliberately does not copy the host-specific `skills` or `interface` fields used by Codex: Agent Plugins discovers components from fixed locations and its manifest schema is closed.
|
|
120
|
+
|
|
121
|
+
AAS generates this portable manifest only when every skill in the bundle is plugin-safe for both existing host targets and can be represented as a unique immediate child of `skills/`. Canonical qualified paths are flattened in the generated package without changing their instruction bodies; a basename collision fails the packaging gate. AAS-specific frontmatter such as provenance and risk is preserved as string values under the standard `metadata` field instead of leaking non-standard top-level keys. If any condition fails, the generator omits `plugin.json` instead of making a false portability claim. The per-bundle status in [Bundles](bundles.md) shows that result.
|
|
122
|
+
|
|
123
|
+
These packages are currently **skills-only**. They do not bundle AAS Core's MCP server, credentials, hooks, or a portable `mcp.json`. Installation and enablement remain client-owned parts of the ecosystem, so use the instructions for your [compatible client](https://agent-plugins.org/compatible-clients) and point it at the desired `plugins/agentic-bundle-*` directory.
|
|
124
|
+
|
|
125
|
+
`AAS Agent & MCP Builder` is the first public-directory flagship. Its version-controlled [submission dossier](../plugin-submissions/aas-agent-mcp-builder/) contains listing copy, public policy and support URLs, starter prompts, and reviewer-reproducible positive and negative evaluations. The dossier being ready does not mean the plugin is already public: OpenAI Platform review and the verified publisher's final publish action remain separate steps.
|
|
126
|
+
|
|
127
|
+
The broad Codex and Claude root plugins remain host-specific because their filtered skill sets are not identical. They intentionally do not have a root Agent Plugins manifest. Choose a portable specialized bundle when cross-client packaging matters.
|
|
128
|
+
|
|
106
129
|
## Claude Code plugin surface
|
|
107
130
|
|
|
108
131
|
Claude Code uses the repository's root `.claude-plugin` metadata.
|
|
@@ -161,6 +184,7 @@ Choose a **specialized plugin** if:
|
|
|
161
184
|
- you are onboarding a team around one domain
|
|
162
185
|
- you want plugin convenience without the breadth of the root plugin
|
|
163
186
|
- you want the plugin itself to communicate a clear job, audience, and workflow
|
|
187
|
+
- you want one package directory that compatible Agent Plugins clients can load without host-specific manifest fields
|
|
164
188
|
|
|
165
189
|
The hosted [specialized plugin landing page](https://sickn33.github.io/agentic-awesome-skills/plugins) is the quickest way to compare the current AAS plugin packs.
|
|
166
190
|
|
|
@@ -174,3 +198,4 @@ The hosted [specialized plugin landing page](https://sickn33.github.io/agentic-a
|
|
|
174
198
|
- [Bundles](bundles.md)
|
|
175
199
|
- [Specialized Plugin Roadmap](specialized-plugin-roadmap.md)
|
|
176
200
|
- [Usage](usage.md)
|
|
201
|
+
- [AAS Agent & MCP Builder submission dossier](../plugin-submissions/aas-agent-mcp-builder/)
|
|
@@ -39,7 +39,7 @@ If you came in through a **Claude Code** or **Codex** plugin instead of AAS Core
|
|
|
39
39
|
|
|
40
40
|
When you ran `npx agentic-awesome-skills` or cloned the repository, you:
|
|
41
41
|
|
|
42
|
-
✅ **Downloaded 2,
|
|
42
|
+
✅ **Downloaded 2,005+ skill files** to your computer (default: `~/.agents/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`)
|
|
43
43
|
✅ **Made them available** to your AI assistant
|
|
44
44
|
❌ **Did NOT enable them all automatically** (they're just sitting there, waiting)
|
|
45
45
|
|
|
@@ -231,7 +231,7 @@ Let's actually use a skill right now. Follow these steps:
|
|
|
231
231
|
|
|
232
232
|
## Direct-install Step 5: Pick Skills Manually
|
|
233
233
|
|
|
234
|
-
Don't try to use all 2,
|
|
234
|
+
Don't try to use all 2,005+ skills at once. Here's a sensible approach:
|
|
235
235
|
|
|
236
236
|
If you want a tool-specific starting point before choosing skills, use:
|
|
237
237
|
|
|
@@ -362,7 +362,7 @@ Usually no, but if your AI doesn't recognize a skill:
|
|
|
362
362
|
|
|
363
363
|
### "Can I load all skills into the model at once?"
|
|
364
364
|
|
|
365
|
-
No. Even though you have 2,
|
|
365
|
+
No. Even though you have 2,005+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block.
|
|
366
366
|
|
|
367
367
|
The intended pattern is:
|
|
368
368
|
|
|
@@ -40,7 +40,7 @@ agentic-awesome-skills/
|
|
|
40
40
|
├── 📄 CONTRIBUTING.md ← Contributor workflow
|
|
41
41
|
├── 📄 CATALOG.md ← Full generated catalog
|
|
42
42
|
│
|
|
43
|
-
├── 📁 skills/ ← 2,
|
|
43
|
+
├── 📁 skills/ ← 2,005+ skills live here
|
|
44
44
|
│ │
|
|
45
45
|
│ ├── 📁 brainstorming/
|
|
46
46
|
│ │ └── 📄 SKILL.md ← Skill definition
|
|
@@ -53,7 +53,7 @@ agentic-awesome-skills/
|
|
|
53
53
|
│ │ └── 📁 2d-games/
|
|
54
54
|
│ │ └── 📄 SKILL.md ← Nested skills also supported
|
|
55
55
|
│ │
|
|
56
|
-
│ └── ... (2,
|
|
56
|
+
│ └── ... (2,005+ total)
|
|
57
57
|
│
|
|
58
58
|
├── 📁 apps/
|
|
59
59
|
│ └── 📁 web-app/ ← Interactive browser
|
|
@@ -106,7 +106,7 @@ agentic-awesome-skills/
|
|
|
106
106
|
|
|
107
107
|
```
|
|
108
108
|
┌─────────────────────────┐
|
|
109
|
-
│ 2,
|
|
109
|
+
│ 2,005+ SKILLS │
|
|
110
110
|
└────────────┬────────────┘
|
|
111
111
|
│
|
|
112
112
|
┌────────────────────────┼────────────────────────┐
|
|
@@ -207,7 +207,7 @@ If you want a workspace-style manual install instead, cloning into `.agent/skill
|
|
|
207
207
|
│ ├── 📁 brainstorming/ │
|
|
208
208
|
│ ├── 📁 stripe-integration/ │
|
|
209
209
|
│ ├── 📁 react-best-practices/ │
|
|
210
|
-
│ └── ... (2,
|
|
210
|
+
│ └── ... (2,005+ total) │
|
|
211
211
|
└─────────────────────────────────────────┘
|
|
212
212
|
```
|
|
213
213
|
|
|
@@ -310,7 +310,7 @@ def main() -> int:
|
|
|
310
310
|
word_count = len(transcript.split()) if transcript else 0
|
|
311
311
|
seeds = detect_seeds(transcript) if transcript else []
|
|
312
312
|
|
|
313
|
-
body = transcript
|
|
313
|
+
body = markdown_text(transcript) if transcript else (
|
|
314
314
|
f"# {markdown_text(title)}\n\n"
|
|
315
315
|
f"No subtitles or auto-captions available for this video.\n\n"
|
|
316
316
|
"To capture this transcript, add captions to the source video or transcribe the audio "
|
|
@@ -19,16 +19,19 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import argparse
|
|
21
21
|
import asyncio
|
|
22
|
+
import hmac
|
|
22
23
|
import html
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
26
|
+
import secrets
|
|
25
27
|
import sys
|
|
28
|
+
import time
|
|
26
29
|
import webbrowser
|
|
27
30
|
from datetime import datetime, timedelta, timezone
|
|
28
31
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
29
32
|
from pathlib import Path
|
|
30
33
|
from typing import Optional
|
|
31
|
-
from urllib.parse import parse_qs, urlparse
|
|
34
|
+
from urllib.parse import parse_qs, urlencode, urlparse
|
|
32
35
|
|
|
33
36
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
34
37
|
|
|
@@ -62,11 +65,30 @@ def _mask_secret(value: str, keep: int = 4) -> str:
|
|
|
62
65
|
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
63
66
|
"""Servidor HTTP mínimo para capturar o callback OAuth."""
|
|
64
67
|
authorization_code: Optional[str] = None
|
|
68
|
+
expected_state: Optional[str] = None
|
|
65
69
|
|
|
66
70
|
def do_GET(self):
|
|
67
71
|
parsed = urlparse(self.path)
|
|
68
72
|
params = parse_qs(parsed.query)
|
|
69
73
|
|
|
74
|
+
expected_path = urlparse(OAUTH_REDIRECT_URI).path or "/"
|
|
75
|
+
supplied_states = params.get("state", [])
|
|
76
|
+
state_is_valid = (
|
|
77
|
+
len(supplied_states) == 1
|
|
78
|
+
and OAuthCallbackHandler.expected_state is not None
|
|
79
|
+
and hmac.compare_digest(supplied_states[0], OAuthCallbackHandler.expected_state)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if parsed.path != expected_path or not state_is_valid:
|
|
83
|
+
self.send_response(400)
|
|
84
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
85
|
+
self.end_headers()
|
|
86
|
+
self.wfile.write(
|
|
87
|
+
b"<html><body><h2>Callback OAuth non valido.</h2>"
|
|
88
|
+
b"<p>Ripeti l'autorizzazione dal terminale.</p></body></html>"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
70
92
|
if "code" in params:
|
|
71
93
|
OAuthCallbackHandler.authorization_code = params["code"][0]
|
|
72
94
|
self.send_response(200)
|
|
@@ -91,20 +113,23 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
|
91
113
|
pass # silencia logs do servidor
|
|
92
114
|
|
|
93
115
|
|
|
94
|
-
def wait_for_oauth_code() -> Optional[str]:
|
|
116
|
+
def wait_for_oauth_code(expected_state: str) -> Optional[str]:
|
|
95
117
|
"""Inicia servidor local e espera pelo código de autorização."""
|
|
118
|
+
OAuthCallbackHandler.authorization_code = None
|
|
119
|
+
OAuthCallbackHandler.expected_state = expected_state
|
|
96
120
|
server = HTTPServer(("localhost", OAUTH_REDIRECT_PORT), OAuthCallbackHandler)
|
|
97
|
-
server.timeout =
|
|
121
|
+
server.timeout = 1
|
|
122
|
+
deadline = time.monotonic() + 120
|
|
98
123
|
print("Aguardando autorização no callback OAuth local...")
|
|
99
124
|
print("(Timeout: 2 minutos)\n")
|
|
100
125
|
|
|
101
|
-
while OAuthCallbackHandler.authorization_code is None:
|
|
126
|
+
while OAuthCallbackHandler.authorization_code is None and time.monotonic() < deadline:
|
|
102
127
|
server.handle_request()
|
|
103
|
-
if OAuthCallbackHandler.authorization_code is not None:
|
|
104
|
-
break
|
|
105
128
|
|
|
106
129
|
server.server_close()
|
|
107
|
-
|
|
130
|
+
code = OAuthCallbackHandler.authorization_code
|
|
131
|
+
OAuthCallbackHandler.expected_state = None
|
|
132
|
+
return code
|
|
108
133
|
|
|
109
134
|
|
|
110
135
|
# ── Token Exchange ────────────────────────────────────────────────────────────
|
|
@@ -287,22 +312,22 @@ async def setup() -> None:
|
|
|
287
312
|
sys.exit(1)
|
|
288
313
|
|
|
289
314
|
# Construir URL de autorização
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
315
|
+
oauth_state = secrets.token_urlsafe(32)
|
|
316
|
+
auth_params = {
|
|
317
|
+
'client_id': app_id,
|
|
318
|
+
'redirect_uri': OAUTH_REDIRECT_URI,
|
|
319
|
+
'scope': ','.join(OAUTH_SCOPES),
|
|
320
|
+
'response_type': 'code',
|
|
321
|
+
'state': oauth_state,
|
|
322
|
+
}
|
|
323
|
+
auth_url = f"{OAUTH_AUTHORIZE_URL}?{urlencode(auth_params)}"
|
|
298
324
|
|
|
299
325
|
print("\nAbrindo browser para autorização...")
|
|
300
326
|
print("A URL de autorização e o App ID não serão exibidos para evitar vazamento de credenciais.\n")
|
|
301
327
|
webbrowser.open(auth_url)
|
|
302
328
|
|
|
303
329
|
# Esperar callback
|
|
304
|
-
|
|
305
|
-
code = wait_for_oauth_code()
|
|
330
|
+
code = wait_for_oauth_code(oauth_state)
|
|
306
331
|
|
|
307
332
|
if not code:
|
|
308
333
|
print("ERRO: Timeout ou falha na autorização.")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""CSV helpers that keep exported cells inert in spreadsheet applications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def spreadsheet_safe_cell(value: Any) -> Any:
|
|
9
|
+
"""Prefix formula-like strings so spreadsheet software treats them as text."""
|
|
10
|
+
if not isinstance(value, str):
|
|
11
|
+
return value
|
|
12
|
+
candidate = value.lstrip(" \t\r\n")
|
|
13
|
+
if candidate.startswith(("=", "+", "-", "@")):
|
|
14
|
+
return "'" + value
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def spreadsheet_safe_record(record: Mapping[str, Any]) -> dict[str, Any]:
|
|
19
|
+
"""Return a copy of *record* with every string cell neutralized."""
|
|
20
|
+
return {key: spreadsheet_safe_cell(value) for key, value in record.items()}
|
|
@@ -17,6 +17,10 @@ import sys
|
|
|
17
17
|
from datetime import datetime, timezone
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
21
|
+
|
|
22
|
+
from csv_utils import spreadsheet_safe_record
|
|
23
|
+
|
|
20
24
|
|
|
21
25
|
def safe_user_path(path_value, base_dir="."):
|
|
22
26
|
"""Resolve a CLI path under the current workspace."""
|
|
@@ -30,8 +34,6 @@ def safe_user_path(path_value, base_dir="."):
|
|
|
30
34
|
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
31
35
|
return resolved_path
|
|
32
36
|
|
|
33
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
34
|
-
|
|
35
37
|
_db = None
|
|
36
38
|
|
|
37
39
|
|
|
@@ -96,7 +98,7 @@ def export_csv_file(records: list, output_dir: Path, name: str) -> Path:
|
|
|
96
98
|
with safe_user_path(path).open("w", newline="", encoding="utf-8-sig") as f:
|
|
97
99
|
writer = csv.DictWriter(f, fieldnames=list(records[0].keys()), extrasaction="ignore")
|
|
98
100
|
writer.writeheader()
|
|
99
|
-
writer.writerows(records)
|
|
101
|
+
writer.writerows(spreadsheet_safe_record(record) for record in records)
|
|
100
102
|
print(f"[CSV] {len(records)} registros ->{path}")
|
|
101
103
|
return path
|
|
102
104
|
|
|
@@ -30,6 +30,7 @@ from pathlib import Path
|
|
|
30
30
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
31
31
|
|
|
32
32
|
from config import STATIC_DIR
|
|
33
|
+
from csv_utils import spreadsheet_safe_record
|
|
33
34
|
from db import Database
|
|
34
35
|
|
|
35
36
|
try:
|
|
@@ -183,7 +184,7 @@ def export_csv():
|
|
|
183
184
|
output = io.StringIO()
|
|
184
185
|
writer = csv.DictWriter(output, fieldnames=list(posts[0].keys()), extrasaction="ignore")
|
|
185
186
|
writer.writeheader()
|
|
186
|
-
writer.writerows(posts)
|
|
187
|
+
writer.writerows(spreadsheet_safe_record(post) for post in posts)
|
|
187
188
|
output.seek(0)
|
|
188
189
|
return StreamingResponse(
|
|
189
190
|
iter([output.getvalue()]),
|
|
@@ -17,6 +17,7 @@ from typing import Dict, List, Any, Optional
|
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
import html as html_module
|
|
19
19
|
from pathlib import Path
|
|
20
|
+
from urllib.parse import urlsplit
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def safe_user_path(path_value, base_dir="."):
|
|
@@ -37,6 +38,30 @@ def escape(text: str) -> str:
|
|
|
37
38
|
return html_module.escape(str(text))
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def tsx_string(value: object) -> str:
|
|
42
|
+
"""Encode config-derived text as a JavaScript string literal."""
|
|
43
|
+
return json.dumps(str(value), ensure_ascii=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def tsx_value(value: object) -> str:
|
|
47
|
+
"""Encode config-derived text as an inert JSX expression."""
|
|
48
|
+
return "{" + tsx_string(value) + "}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tsx_href(value: object) -> str:
|
|
52
|
+
"""Encode a link while refusing executable URL schemes and controls."""
|
|
53
|
+
raw = str(value).strip()
|
|
54
|
+
if not raw or any(ord(character) < 32 or ord(character) == 127 for character in raw):
|
|
55
|
+
raw = "#"
|
|
56
|
+
try:
|
|
57
|
+
scheme = urlsplit(raw).scheme.lower()
|
|
58
|
+
except ValueError:
|
|
59
|
+
scheme = "invalid"
|
|
60
|
+
if scheme and scheme not in {"http", "https", "mailto", "tel"}:
|
|
61
|
+
raw = "#"
|
|
62
|
+
return tsx_value(raw)
|
|
63
|
+
|
|
64
|
+
|
|
40
65
|
# ---------------------------------------------------------------------------
|
|
41
66
|
# Tailwind style mappings for TSX output
|
|
42
67
|
# ---------------------------------------------------------------------------
|
|
@@ -86,18 +111,18 @@ def tsx_nav(config: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
86
111
|
nav_links = config.get("nav_links", [])
|
|
87
112
|
cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
|
|
88
113
|
links_jsx = "\n ".join(
|
|
89
|
-
f'<a href=
|
|
114
|
+
f'<a href={tsx_href(l.get("url", "#"))} className="{style["muted"]} hover:{style["text"]} font-medium transition-colors">{tsx_value(l.get("text", ""))}</a>'
|
|
90
115
|
for l in nav_links
|
|
91
116
|
)
|
|
92
117
|
return f'''function Navbar() {{
|
|
93
118
|
return (
|
|
94
119
|
<nav className="sticky top-0 z-50 {style["bg"]} border-b {style["border"]} backdrop-blur-sm">
|
|
95
120
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
|
|
96
|
-
<a href="#" className="text-xl font-bold {style["text"]}">{brand}</a>
|
|
121
|
+
<a href="#" className="text-xl font-bold {style["text"]}">{tsx_value(brand)}</a>
|
|
97
122
|
<div className="hidden items-center gap-8 md:flex">
|
|
98
123
|
{links_jsx}
|
|
99
|
-
<a href=
|
|
100
|
-
{cta.get("text", "Get Started")}
|
|
124
|
+
<a href={tsx_href(cta.get("url", "#"))} className="rounded-lg {style["btn"]} px-5 py-2.5 text-sm font-semibold transition-colors">
|
|
125
|
+
{tsx_value(cta.get("text", "Get Started"))}
|
|
101
126
|
</a>
|
|
102
127
|
</div>
|
|
103
128
|
</div>
|
|
@@ -114,22 +139,22 @@ def tsx_hero(hero: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
114
139
|
secondary_jsx = ""
|
|
115
140
|
if secondary_cta:
|
|
116
141
|
secondary_jsx = f'''
|
|
117
|
-
<a href=
|
|
118
|
-
{secondary_cta.get("text", "Learn More")}
|
|
142
|
+
<a href={tsx_href(secondary_cta.get("url", "#"))} className="rounded-lg {style["btn_secondary"]} px-8 py-3 text-lg font-semibold transition-colors">
|
|
143
|
+
{tsx_value(secondary_cta.get("text", "Learn More"))}
|
|
119
144
|
</a>'''
|
|
120
145
|
return f'''function Hero() {{
|
|
121
146
|
return (
|
|
122
147
|
<section className="flex min-h-[80vh] flex-col items-center justify-center px-6 py-24 text-center {style["bg"]}">
|
|
123
148
|
<div className="mx-auto max-w-4xl">
|
|
124
149
|
<h1 className="mb-6 text-5xl font-bold tracking-tight {style["text"]} md:text-7xl">
|
|
125
|
-
{h1}
|
|
150
|
+
{tsx_value(h1)}
|
|
126
151
|
</h1>
|
|
127
152
|
<p className="mx-auto mb-10 max-w-2xl text-xl {style["muted"]}">
|
|
128
|
-
{sub}
|
|
153
|
+
{tsx_value(sub)}
|
|
129
154
|
</p>
|
|
130
155
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
131
|
-
<a href=
|
|
132
|
-
{primary_cta.get("text", "Get Started")}
|
|
156
|
+
<a href={tsx_href(primary_cta.get("url", "#"))} className="rounded-lg {style["btn"]} px-8 py-3 text-lg font-semibold transition-colors">
|
|
157
|
+
{tsx_value(primary_cta.get("text", "Get Started"))}
|
|
133
158
|
</a>{secondary_jsx}
|
|
134
159
|
</div>
|
|
135
160
|
</div>
|
|
@@ -144,9 +169,9 @@ def tsx_features(features: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
144
169
|
items = features.get("items", [])
|
|
145
170
|
cards_jsx = "\n ".join(
|
|
146
171
|
f'''<div className="{style["card_bg"]} rounded-xl p-8">
|
|
147
|
-
<div className="mb-4 text-3xl">{f.get("icon", "")}</div>
|
|
148
|
-
<h3 className="mb-3 text-xl font-semibold {style["text"]}">{f.get("title", "")}</h3>
|
|
149
|
-
<p className="{style["muted"]}">{f.get("description", "")}</p>
|
|
172
|
+
<div className="mb-4 text-3xl">{tsx_value(f.get("icon", ""))}</div>
|
|
173
|
+
<h3 className="mb-3 text-xl font-semibold {style["text"]}">{tsx_value(f.get("title", ""))}</h3>
|
|
174
|
+
<p className="{style["muted"]}">{tsx_value(f.get("description", ""))}</p>
|
|
150
175
|
</div>'''
|
|
151
176
|
for f in items
|
|
152
177
|
)
|
|
@@ -154,8 +179,8 @@ def tsx_features(features: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
154
179
|
return (
|
|
155
180
|
<section className="{style["section_alt"]} px-6 py-24">
|
|
156
181
|
<div className="mx-auto max-w-7xl">
|
|
157
|
-
<h2 className="mb-4 text-center text-4xl font-bold {style["text"]}">{title}</h2>
|
|
158
|
-
<p className="mx-auto mb-16 max-w-2xl text-center text-lg {style["muted"]}">{subtitle}</p>
|
|
182
|
+
<h2 className="mb-4 text-center text-4xl font-bold {style["text"]}">{tsx_value(title)}</h2>
|
|
183
|
+
<p className="mx-auto mb-16 max-w-2xl text-center text-lg {style["muted"]}">{tsx_value(subtitle)}</p>
|
|
159
184
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
160
185
|
{cards_jsx}
|
|
161
186
|
</div>
|
|
@@ -172,10 +197,10 @@ def tsx_testimonials(testimonials: Dict[str, Any], style: Dict[str, str]) -> str
|
|
|
172
197
|
return ""
|
|
173
198
|
cards_jsx = "\n ".join(
|
|
174
199
|
f'''<div className="rounded-xl border {style["border"]} p-8">
|
|
175
|
-
<p className="mb-6 text-lg italic {style["muted"]}">"
|
|
200
|
+
<p className="mb-6 text-lg italic {style["muted"]}">{tsx_value('"' + str(t.get("quote", "")) + '"')}</p>
|
|
176
201
|
<div>
|
|
177
|
-
<p className="font-semibold {style["text"]}">{t.get("name", "")}</p>
|
|
178
|
-
<p className="text-sm {style["muted"]}">{t.get("title", "")}, {t.get("company", "")}</p>
|
|
202
|
+
<p className="font-semibold {style["text"]}">{tsx_value(t.get("name", ""))}</p>
|
|
203
|
+
<p className="text-sm {style["muted"]}">{tsx_value(t.get("title", ""))}, {tsx_value(t.get("company", ""))}</p>
|
|
179
204
|
</div>
|
|
180
205
|
</div>'''
|
|
181
206
|
for t in items
|
|
@@ -184,7 +209,7 @@ def tsx_testimonials(testimonials: Dict[str, Any], style: Dict[str, str]) -> str
|
|
|
184
209
|
return (
|
|
185
210
|
<section className="px-6 py-24 {style["bg"]}">
|
|
186
211
|
<div className="mx-auto max-w-7xl">
|
|
187
|
-
<h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
|
|
212
|
+
<h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{tsx_value(title)}</h2>
|
|
188
213
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
189
214
|
{cards_jsx}
|
|
190
215
|
</div>
|
|
@@ -206,18 +231,18 @@ def tsx_pricing(pricing: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
206
231
|
border_cls = f"border-2 border-{accent}-500 ring-4 ring-{accent}-500/20" if featured else f"border {style['border']}"
|
|
207
232
|
badge = f'\n <div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-{accent}-600 px-4 py-1 text-xs font-semibold text-white">Most Popular</div>' if featured else ""
|
|
208
233
|
features_jsx = "\n ".join(
|
|
209
|
-
f'<li className="flex items-center gap-2 py-2"><span className="text-{accent}-500 font-bold">✓</span> {feat}</li>'
|
|
234
|
+
f'<li className="flex items-center gap-2 py-2"><span className="text-{accent}-500 font-bold">✓</span> {tsx_value(feat)}</li>'
|
|
210
235
|
for feat in p.get("features", [])
|
|
211
236
|
)
|
|
212
237
|
cards.append(f'''<div className="relative rounded-2xl {border_cls} {style["card_bg"]} p-8 text-center">{badge}
|
|
213
|
-
<h3 className="mb-2 text-xl font-semibold {style["text"]}">{p.get("name", "")}</h3>
|
|
214
|
-
<div className="my-6 text-5xl font-extrabold {style["text"]}">${p.get("price", "0")}<span className="text-base font-normal {style["muted"]}">/mo</span></div>
|
|
215
|
-
<p className="{style["muted"]} mb-6">{p.get("description", "")}</p>
|
|
238
|
+
<h3 className="mb-2 text-xl font-semibold {style["text"]}">{tsx_value(p.get("name", ""))}</h3>
|
|
239
|
+
<div className="my-6 text-5xl font-extrabold {style["text"]}">${tsx_value(p.get("price", "0"))}<span className="text-base font-normal {style["muted"]}">/mo</span></div>
|
|
240
|
+
<p className="{style["muted"]} mb-6">{tsx_value(p.get("description", ""))}</p>
|
|
216
241
|
<ul className="mb-8 space-y-1 text-left {style["muted"]}">
|
|
217
242
|
{features_jsx}
|
|
218
243
|
</ul>
|
|
219
|
-
<a href=
|
|
220
|
-
{p.get("cta_text", "Choose Plan")}
|
|
244
|
+
<a href={tsx_href(p.get("cta_url", "#"))} className="block w-full rounded-lg {style["btn"]} py-3 text-center font-semibold transition-colors">
|
|
245
|
+
{tsx_value(p.get("cta_text", "Choose Plan"))}
|
|
221
246
|
</a>
|
|
222
247
|
</div>''')
|
|
223
248
|
cards_jsx = "\n ".join(cards)
|
|
@@ -225,7 +250,7 @@ def tsx_pricing(pricing: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
225
250
|
return (
|
|
226
251
|
<section className="{style["section_alt"]} px-6 py-24">
|
|
227
252
|
<div className="mx-auto max-w-5xl">
|
|
228
|
-
<h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
|
|
253
|
+
<h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{tsx_value(title)}</h2>
|
|
229
254
|
<div className="grid gap-8 lg:grid-cols-{min(len(plans), 3)}">
|
|
230
255
|
{cards_jsx}
|
|
231
256
|
</div>
|
|
@@ -241,10 +266,10 @@ def tsx_cta(cta: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
241
266
|
return (
|
|
242
267
|
<section className="bg-{accent}-600 px-6 py-24 text-center text-white">
|
|
243
268
|
<div className="mx-auto max-w-3xl">
|
|
244
|
-
<h2 className="mb-4 text-4xl font-bold">{cta.get("headline", "Ready to get started?")}</h2>
|
|
245
|
-
<p className="mb-10 text-xl opacity-90">{cta.get("subheadline", "")}</p>
|
|
246
|
-
<a href=
|
|
247
|
-
{cta.get("text", "Start Free Trial")}
|
|
269
|
+
<h2 className="mb-4 text-4xl font-bold">{tsx_value(cta.get("headline", "Ready to get started?"))}</h2>
|
|
270
|
+
<p className="mb-10 text-xl opacity-90">{tsx_value(cta.get("subheadline", ""))}</p>
|
|
271
|
+
<a href={tsx_href(cta.get("url", "#"))} className="rounded-lg bg-white px-8 py-3 text-lg font-semibold text-{accent}-600 transition-colors hover:bg-gray-100">
|
|
272
|
+
{tsx_value(cta.get("text", "Start Free Trial"))}
|
|
248
273
|
</a>
|
|
249
274
|
</div>
|
|
250
275
|
</section>
|
|
@@ -259,7 +284,7 @@ def tsx_footer(config: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
259
284
|
return f'''function Footer() {{
|
|
260
285
|
return (
|
|
261
286
|
<footer className="border-t {style["border"]} {style["bg"]} px-6 py-10 text-center {style["muted"]}">
|
|
262
|
-
<p>© {footer_text}</p>
|
|
287
|
+
<p>© {tsx_value(footer_text)}</p>
|
|
263
288
|
</footer>
|
|
264
289
|
);
|
|
265
290
|
}}'''
|
|
@@ -267,8 +292,9 @@ def tsx_footer(config: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
|
267
292
|
|
|
268
293
|
def generate_tsx(config: Dict[str, Any]) -> str:
|
|
269
294
|
"""Generate complete Next.js/React TSX landing page with Tailwind CSS."""
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
requested_style = config.get("design_style", "clean-minimal")
|
|
296
|
+
style_name = requested_style if requested_style in DESIGN_STYLES else "clean-minimal"
|
|
297
|
+
style = DESIGN_STYLES[style_name]
|
|
272
298
|
|
|
273
299
|
components = []
|
|
274
300
|
component_names = []
|
|
@@ -312,11 +338,11 @@ def generate_tsx(config: Dict[str, Any]) -> str:
|
|
|
312
338
|
import type {{ Metadata }} from "next";
|
|
313
339
|
|
|
314
340
|
export const metadata: Metadata = {{
|
|
315
|
-
title:
|
|
316
|
-
description:
|
|
341
|
+
title: {tsx_string(title)},
|
|
342
|
+
description: {tsx_string(meta_desc)},
|
|
317
343
|
openGraph: {{
|
|
318
|
-
title:
|
|
319
|
-
description:
|
|
344
|
+
title: {tsx_string(title)},
|
|
345
|
+
description: {tsx_string(meta_desc)},
|
|
320
346
|
type: "website",
|
|
321
347
|
}},
|
|
322
348
|
}};
|
|
@@ -455,7 +455,7 @@ Integrate with [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) for a visua
|
|
|
455
455
|
npx vibe-kanban
|
|
456
456
|
|
|
457
457
|
# Export Loki tasks to Vibe Kanban
|
|
458
|
-
|
|
458
|
+
bash scripts/export-to-vibe-kanban.sh
|
|
459
459
|
```
|
|
460
460
|
|
|
461
461
|
**Benefits:**
|