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.
Files changed (55) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +3 -1
  2. package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +2 -2
  3. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  4. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  5. package/bundled-skills/docs/maintainers/release-process.md +2 -2
  6. package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
  7. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  8. package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/README.md +19 -0
  9. package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/evaluation-cases.json +74 -0
  10. package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/evaluation-results.json +86 -0
  11. package/bundled-skills/docs/plugin-submissions/aas-agent-mcp-builder/submission.json +41 -0
  12. package/bundled-skills/docs/users/aas-core.md +1 -1
  13. package/bundled-skills/docs/users/bundles.md +62 -60
  14. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  15. package/bundled-skills/docs/users/faq.md +5 -1
  16. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  17. package/bundled-skills/docs/users/getting-started.md +4 -2
  18. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  19. package/bundled-skills/docs/users/plugins.md +28 -3
  20. package/bundled-skills/docs/users/usage.md +3 -3
  21. package/bundled-skills/docs/users/visual-guide.md +4 -4
  22. package/bundled-skills/ingest-youtube/ingest.py +1 -1
  23. package/bundled-skills/instagram/scripts/auth.py +42 -17
  24. package/bundled-skills/instagram/scripts/csv_utils.py +20 -0
  25. package/bundled-skills/instagram/scripts/export.py +5 -3
  26. package/bundled-skills/instagram/scripts/serve_api.py +2 -1
  27. package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +63 -37
  28. package/bundled-skills/loki-mode/README.md +1 -1
  29. package/bundled-skills/loki-mode/integrations/vibe-kanban.md +1 -1
  30. package/bundled-skills/loki-mode/scripts/export-to-vibe-kanban.sh +97 -26
  31. package/bundled-skills/macos-spm-app-packaging/assets/templates/package_app.sh +34 -1
  32. package/bundled-skills/macos-spm-app-packaging/assets/templates/sign-and-notarize.sh +36 -1
  33. package/bundled-skills/notebooklm/README.md +5 -4
  34. package/bundled-skills/notebooklm/SKILL.md +14 -7
  35. package/bundled-skills/notebooklm/references/api_reference.md +4 -3
  36. package/bundled-skills/notebooklm/references/usage_patterns.md +6 -4
  37. package/bundled-skills/notebooklm/scripts/ask_question.py +8 -16
  38. package/bundled-skills/notebooklm/scripts/browser_session.py +3 -2
  39. package/bundled-skills/notebooklm/scripts/input_safety.py +114 -0
  40. package/bundled-skills/notebooklm/scripts/notebook_manager.py +35 -21
  41. package/bundled-skills/outreachagent/SKILL.md +386 -0
  42. package/bundled-skills/telegram/assets/boilerplate/python/bot.py +4 -3
  43. package/bundled-skills/telegram/assets/boilerplate/python/webhook_server.py +2 -1
  44. package/bundled-skills/vercel-optimize/lib/verify-claim.mjs +22 -8
  45. package/bundled-skills/video-router/SKILL.md +98 -0
  46. package/bundled-skills/web-scraper/SKILL.md +38 -3
  47. package/bundled-skills/youtube-notetaker/SKILL.md +16 -10
  48. package/bundled-skills/youtube-notetaker/scripts/detect_slides.sh +3 -1
  49. package/bundled-skills/youtube-notetaker/scripts/download.sh +4 -1
  50. package/bundled-skills/youtube-notetaker/scripts/scratch_safety.sh +64 -0
  51. package/bundled-skills/youtube-notetaker/scripts/setup.sh +7 -2
  52. package/bundled-skills/youtube-notetaker/scripts/vtt_to_transcript.py +6 -1
  53. package/bundled-skills/youtube-summarizer/SKILL.md +8 -10
  54. package/package.json +1 -1
  55. package/skills_index.json +99 -0
@@ -1,6 +1,6 @@
1
- # Plugins for Claude Code and Codex
1
+ # Plugins for compatible agent clients
2
2
 
3
- Release `9.0.0` adds first-class plugin distributions for both **Claude Code** and **Codex**.
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 or repo-local plugin metadata when you want a curated, installable distribution:
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,003+ skill files** to your computer (default: `~/.agents/skills/`; or a custom path like `~/.agent/skills/` if you used `--path`)
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,003+ skills at once. Here's a sensible approach:
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,003+ skills installed locally, you should **not** concatenate every `SKILL.md` into a single system prompt or context block.
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,003+ skills live here
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,003+ total)
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,003+ SKILLS │
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,003+ total) │
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 or (
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 = 120 # 2 minutos
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
- return OAuthCallbackHandler.authorization_code
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
- scopes = ",".join(OAUTH_SCOPES)
291
- auth_url = (
292
- f"{OAUTH_AUTHORIZE_URL}?"
293
- f"client_id={app_id}&"
294
- f"redirect_uri={OAUTH_REDIRECT_URI}&"
295
- f"scope={scopes}&"
296
- f"response_type=code"
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
- OAuthCallbackHandler.authorization_code = None
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="{l.get("url", "#")}" className="{style["muted"]} hover:{style["text"]} font-medium transition-colors">{l.get("text", "")}</a>'
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="{cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-5 py-2.5 text-sm font-semibold transition-colors">
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="{secondary_cta.get("url", "#")}" className="rounded-lg {style["btn_secondary"]} px-8 py-3 text-lg font-semibold transition-colors">
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="{primary_cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-8 py-3 text-lg font-semibold transition-colors">
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"]}">"{t.get("quote", "")}"</p>
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">&#10003;</span> {feat}</li>'
234
+ f'<li className="flex items-center gap-2 py-2"><span className="text-{accent}-500 font-bold">&#10003;</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="{p.get("cta_url", "#")}" className="block w-full rounded-lg {style["btn"]} py-3 text-center font-semibold transition-colors">
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="{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">
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>&copy; {footer_text}</p>
287
+ <p>&copy; {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
- style_name = config.get("design_style", "clean-minimal")
271
- style = DESIGN_STYLES.get(style_name, DESIGN_STYLES["clean-minimal"])
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: "{title}",
316
- description: "{meta_desc}",
341
+ title: {tsx_string(title)},
342
+ description: {tsx_string(meta_desc)},
317
343
  openGraph: {{
318
- title: "{title}",
319
- description: "{meta_desc}",
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
- ./scripts/export-to-vibe-kanban.sh
458
+ bash scripts/export-to-vibe-kanban.sh
459
459
  ```
460
460
 
461
461
  **Benefits:**
@@ -157,7 +157,7 @@ while true; do
157
157
  inotifywait -e modify,create "$LOKI_DIR/queue/" 2>/dev/null
158
158
  fi
159
159
 
160
- ./scripts/export-to-vibe-kanban.sh
160
+ bash scripts/export-to-vibe-kanban.sh
161
161
  sleep 2
162
162
  done
163
163
  ```