opencode-skills-collection 4.0.16 → 4.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +4 -1
  2. package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +59 -14
  3. package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +1 -0
  4. package/bundled-skills/apify-audience-analysis/reference/scripts/run_actor.js +9 -10
  5. package/bundled-skills/apify-brand-reputation-monitoring/reference/scripts/run_actor.js +9 -10
  6. package/bundled-skills/apify-competitor-intelligence/reference/scripts/run_actor.js +9 -10
  7. package/bundled-skills/apify-content-analytics/reference/scripts/run_actor.js +9 -10
  8. package/bundled-skills/apify-ecommerce/reference/scripts/run_actor.js +9 -10
  9. package/bundled-skills/apify-influencer-discovery/reference/scripts/run_actor.js +9 -10
  10. package/bundled-skills/apify-lead-generation/reference/scripts/run_actor.js +9 -10
  11. package/bundled-skills/apify-market-research/reference/scripts/run_actor.js +9 -10
  12. package/bundled-skills/apify-trend-analysis/reference/scripts/run_actor.js +9 -10
  13. package/bundled-skills/apify-ultimate-scraper/reference/scripts/run_actor.js +9 -10
  14. package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +22 -6
  15. package/bundled-skills/context-guardian/scripts/context_snapshot.py +45 -10
  16. package/bundled-skills/cowork-to-code-bridge/SKILL.md +243 -0
  17. package/bundled-skills/deploy-to-vercel/resources/deploy.sh +13 -0
  18. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  19. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  20. package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
  21. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  22. package/bundled-skills/docs/users/aas-core.md +1 -1
  23. package/bundled-skills/docs/users/bundles.md +1 -1
  24. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  25. package/bundled-skills/docs/users/faq.md +20 -7
  26. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  27. package/bundled-skills/docs/users/getting-started.md +18 -6
  28. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  29. package/bundled-skills/docs/users/usage.md +3 -3
  30. package/bundled-skills/docs/users/visual-guide.md +4 -4
  31. package/bundled-skills/fact-check-x-complete/SKILL.md +232 -0
  32. package/bundled-skills/find-complementary-founders/SKILL.md +47 -9
  33. package/bundled-skills/find-complementary-founders/references/community-growth.md +39 -28
  34. package/bundled-skills/find-complementary-founders/references/moltbook.md +2 -1
  35. package/bundled-skills/find-complementary-founders/references/owner-onboarding.ru.md +29 -14
  36. package/bundled-skills/find-complementary-founders/references/privacy-safety.md +3 -0
  37. package/bundled-skills/find-complementary-founders/references/profile-schema.md +3 -2
  38. package/bundled-skills/find-complementary-founders/scripts/github_thread.py +1 -1
  39. package/bundled-skills/find-complementary-founders/scripts/moltbook_publish.py +19 -6
  40. package/bundled-skills/find-complementary-founders/scripts/test_moltbook_publish.py +151 -0
  41. package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +20 -10
  42. package/bundled-skills/instagram/scripts/config.py +22 -4
  43. package/bundled-skills/instagram/scripts/db.py +8 -0
  44. package/bundled-skills/junta-leiloeiros/scripts/export.py +11 -2
  45. package/bundled-skills/junta-leiloeiros/scripts/serve_api.py +5 -1
  46. package/bundled-skills/loki-mode/CLAUDE.md +1 -1
  47. package/bundled-skills/loki-mode/CONTEXT-EXPORT.md +3 -3
  48. package/bundled-skills/loki-mode/INSTALLATION.md +3 -3
  49. package/bundled-skills/loki-mode/README.md +11 -11
  50. package/bundled-skills/loki-mode/SKILL.md +2 -2
  51. package/bundled-skills/loki-mode/autonomy/README.md +8 -8
  52. package/bundled-skills/loki-mode/autonomy/run.sh +23 -54
  53. package/bundled-skills/loki-mode/demo/README.md +3 -3
  54. package/bundled-skills/loki-mode/demo/run-demo.sh +3 -3
  55. package/bundled-skills/loki-mode/demo/vhs-tape.tape +1 -1
  56. package/bundled-skills/loki-mode/demo/voice-over-script.md +3 -3
  57. package/bundled-skills/loki-mode/docs/COMPETITIVE-ANALYSIS.md +2 -2
  58. package/bundled-skills/loki-mode/docs/screenshots/README.md +1 -1
  59. package/bundled-skills/loki-mode/integrations/vibe-kanban.md +1 -1
  60. package/bundled-skills/loki-mode/references/agent-types.md +2 -2
  61. package/bundled-skills/loki-mode/scripts/loki-wrapper.sh +5 -4
  62. package/bundled-skills/maintain-codex-wiki/SKILL.md +340 -0
  63. package/bundled-skills/notebooklm/README.md +4 -3
  64. package/bundled-skills/notebooklm/SKILL.md +3 -3
  65. package/bundled-skills/notebooklm/scripts/auth_manager.py +6 -4
  66. package/bundled-skills/notebooklm/scripts/browser_utils.py +2 -1
  67. package/bundled-skills/notebooklm/scripts/config.py +31 -1
  68. package/bundled-skills/pptx-official/scripts/html2pptx.js +37 -8
  69. package/bundled-skills/senior-frontend/scripts/frontend_scaffolder.py +6 -0
  70. package/bundled-skills/skill-creator/scripts/init_skill.py +18 -2
  71. package/bundled-skills/telegram/SKILL.md +2 -2
  72. package/bundled-skills/telegram/assets/boilerplate/nodejs/.env.example +1 -1
  73. package/bundled-skills/telegram/assets/boilerplate/nodejs/src/bot-client.ts +7 -6
  74. package/bundled-skills/telegram/assets/boilerplate/nodejs/src/index.ts +3 -0
  75. package/bundled-skills/telegram/assets/boilerplate/python/.env.example +1 -1
  76. package/bundled-skills/telegram/assets/boilerplate/python/webhook_server.py +9 -4
  77. package/bundled-skills/telegram/references/webhook-setup.md +10 -9
  78. package/bundled-skills/youtube-notetaker/SKILL.md +1 -1
  79. package/bundled-skills/youtube-notetaker/scripts/extract_slides.py +19 -6
  80. package/bundled-skills/youtube-notetaker/scripts/setup.sh +1 -0
  81. package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +15 -4
  82. package/package.json +1 -1
  83. package/skills_index.json +94 -1
@@ -265,12 +265,13 @@ All data is stored locally within the skill directory:
265
265
  ```
266
266
  ~/.claude/skills/notebooklm/data/
267
267
  ├── library.json - Your notebook library with metadata
268
- ├── auth_info.json - Authentication status info
269
- └── browser_state/ - Browser cookies and session data
268
+ ~/.local/share/agentic-awesome-skills/notebooklm/
269
+ ├── auth_info.json - Authentication status info (0600)
270
+ └── browser_state/ - Browser cookies and session data (0700)
270
271
  ```
271
272
 
272
273
  **Important Security Note:**
273
- - The `data/` directory contains sensitive authentication data and personal notebooks
274
+ - The private per-user state directory contains sensitive authentication data and personal notebooks
274
275
  - It's automatically excluded from git via `.gitignore`
275
276
  - NEVER manually commit or share the contents of the `data/` directory
276
277
 
@@ -195,8 +195,8 @@ python -m patchright install chromium
195
195
 
196
196
  All data stored in `~/.claude/skills/notebooklm/data/`:
197
197
  - `library.json` - Notebook metadata
198
- - `auth_info.json` - Authentication status
199
- - `browser_state/` - Browser cookies and session
198
+ - `~/.local/share/agentic-awesome-skills/notebooklm/auth_info.json` - private authentication status (`0600`)
199
+ - `~/.local/share/agentic-awesome-skills/notebooklm/browser_state/` - private browser cookies and session (`0700`)
200
200
 
201
201
  **Security:** Protected by `.gitignore`, never commit to git.
202
202
 
@@ -263,7 +263,7 @@ Synthesize and respond to user
263
263
  **Important directories and files:**
264
264
 
265
265
  - `scripts/` - All automation scripts (ask_question.py, notebook_manager.py, etc.)
266
- - `data/` - Local storage for authentication and notebook library
266
+ - `~/.local/share/agentic-awesome-skills/notebooklm/` - private per-user authentication and notebook storage
267
267
  - `references/` - Extended documentation:
268
268
  - `api_reference.md` - Detailed API documentation for all scripts
269
269
  - `troubleshooting.md` - Common issues and solutions
@@ -25,7 +25,7 @@ from patchright.sync_api import sync_playwright, BrowserContext
25
25
  # Add parent directory to path
26
26
  sys.path.insert(0, str(Path(__file__).parent))
27
27
 
28
- from config import BROWSER_STATE_DIR, STATE_FILE, AUTH_INFO_FILE, DATA_DIR
28
+ from config import BROWSER_STATE_DIR, STATE_FILE, AUTH_INFO_FILE, DATA_DIR, ensure_private_state
29
29
  from browser_utils import BrowserFactory
30
30
 
31
31
 
@@ -56,8 +56,7 @@ class AuthManager:
56
56
  def __init__(self):
57
57
  """Initialize the authentication manager"""
58
58
  # Ensure directories exist
59
- DATA_DIR.mkdir(parents=True, exist_ok=True)
60
- BROWSER_STATE_DIR.mkdir(parents=True, exist_ok=True)
59
+ ensure_private_state()
61
60
 
62
61
  self.state_file = STATE_FILE
63
62
  self.auth_info_file = AUTH_INFO_FILE
@@ -176,6 +175,7 @@ class AuthManager:
176
175
  try:
177
176
  # Save storage state (cookies, localStorage)
178
177
  context.storage_state(path=str(self.state_file))
178
+ self.state_file.chmod(0o600)
179
179
  print(f" 💾 Saved browser state to: {self.state_file}")
180
180
  except Exception as e:
181
181
  print(f" ❌ Failed to save browser state: {e}")
@@ -190,6 +190,7 @@ class AuthManager:
190
190
  }
191
191
  with open(self.auth_info_file, 'w') as f:
192
192
  json.dump(info, f, indent=2)
193
+ self.auth_info_file.chmod(0o600)
193
194
  except Exception:
194
195
  pass # Non-critical
195
196
 
@@ -216,7 +217,8 @@ class AuthManager:
216
217
  # Clear entire browser state directory
217
218
  if self.browser_state_dir.exists():
218
219
  shutil.rmtree(self.browser_state_dir)
219
- self.browser_state_dir.mkdir(parents=True, exist_ok=True)
220
+ self.browser_state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
221
+ self.browser_state_dir.chmod(0o700)
220
222
  print(" ✅ Cleared browser data")
221
223
 
222
224
  return True
@@ -8,7 +8,7 @@ import time
8
8
  import random
9
9
 
10
10
  from patchright.sync_api import Playwright, BrowserContext, Page
11
- from config import BROWSER_PROFILE_DIR, STATE_FILE, BROWSER_ARGS, USER_AGENT
11
+ from config import BROWSER_PROFILE_DIR, STATE_FILE, BROWSER_ARGS, USER_AGENT, ensure_private_state
12
12
 
13
13
 
14
14
  class BrowserFactory:
@@ -25,6 +25,7 @@ class BrowserFactory:
25
25
  and cookie workaround.
26
26
  """
27
27
  # Launch persistent context
28
+ ensure_private_state()
28
29
  context = playwright.chromium.launch_persistent_context(
29
30
  user_data_dir=user_data_dir,
30
31
  channel="chrome", # Use real Chrome
@@ -3,17 +3,47 @@ Configuration for NotebookLM Skill
3
3
  Centralizes constants, selectors, and paths
4
4
  """
5
5
 
6
+ import os
6
7
  from pathlib import Path
7
8
 
8
9
  # Paths
9
10
  SKILL_DIR = Path(__file__).parent.parent
10
- DATA_DIR = SKILL_DIR / "data"
11
+ LEGACY_DATA_DIR = SKILL_DIR / "data"
12
+ DATA_DIR = Path(os.environ.get(
13
+ "AAS_NOTEBOOKLM_DATA_DIR",
14
+ Path.home() / ".local" / "share" / "agentic-awesome-skills" / "notebooklm",
15
+ )).expanduser()
11
16
  BROWSER_STATE_DIR = DATA_DIR / "browser_state"
12
17
  BROWSER_PROFILE_DIR = BROWSER_STATE_DIR / "browser_profile"
13
18
  STATE_FILE = BROWSER_STATE_DIR / "state.json"
14
19
  AUTH_INFO_FILE = DATA_DIR / "auth_info.json"
15
20
  LIBRARY_FILE = DATA_DIR / "library.json"
16
21
 
22
+ # Browser profiles and storage-state files contain live Google credentials.
23
+ # A restrictive umask also covers files created later by Chromium.
24
+ os.umask(0o077)
25
+
26
+
27
+ def ensure_private_state():
28
+ """Create/repair the private state tree and migrate the legacy skill-local tree."""
29
+ import shutil
30
+ if LEGACY_DATA_DIR.exists() and LEGACY_DATA_DIR != DATA_DIR and not DATA_DIR.exists():
31
+ DATA_DIR.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
32
+ shutil.move(str(LEGACY_DATA_DIR), str(DATA_DIR))
33
+ print(f"⚠️ Migrated sensitive NotebookLM state to private user storage: {DATA_DIR}")
34
+ for directory in (DATA_DIR.parent.parent, DATA_DIR.parent, DATA_DIR, BROWSER_STATE_DIR, BROWSER_PROFILE_DIR):
35
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
36
+ try:
37
+ directory.chmod(0o700)
38
+ except OSError:
39
+ pass
40
+ for file_path in (STATE_FILE, AUTH_INFO_FILE, LIBRARY_FILE):
41
+ if file_path.exists() and not file_path.is_symlink():
42
+ try:
43
+ file_path.chmod(0o600)
44
+ except OSError:
45
+ pass
46
+
17
47
  # NotebookLM Selectors
18
48
  QUERY_INPUT_SELECTORS = [
19
49
  "textarea.query-box-input", # Primary
@@ -26,13 +26,44 @@
26
26
  */
27
27
 
28
28
  const { chromium } = require('playwright');
29
+ const fs = require('fs');
29
30
  const path = require('path');
31
+ const { fileURLToPath } = require('url');
30
32
  const sharp = require('sharp');
31
33
 
32
34
  const PT_PER_PX = 0.75;
33
35
  const PX_PER_IN = 96;
34
36
  const EMU_PER_IN = 914400;
35
37
 
38
+ function resolveTrustedAsset(assetValue, allowedRoot) {
39
+ let candidate;
40
+ try {
41
+ const parsed = new URL(assetValue, `file://${allowedRoot}${path.sep}`);
42
+ if (parsed.protocol !== 'file:') throw new Error('only local file assets are supported');
43
+ candidate = fileURLToPath(parsed);
44
+ } catch (error) {
45
+ throw new Error(`Unsafe slide asset ${JSON.stringify(assetValue)}: ${error.message}`);
46
+ }
47
+ const realRoot = fs.realpathSync(allowedRoot);
48
+ const realAsset = fs.realpathSync(candidate);
49
+ const relativeAsset = path.relative(realRoot, realAsset);
50
+ if (relativeAsset.startsWith('..') || path.isAbsolute(relativeAsset)) {
51
+ throw new Error(`Slide asset escapes the HTML directory: ${assetValue}`);
52
+ }
53
+ const stat = fs.lstatSync(realAsset);
54
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Slide asset must be a regular file: ${assetValue}`);
55
+ return realAsset;
56
+ }
57
+
58
+ function constrainSlideAssets(slideData, allowedRoot) {
59
+ if (slideData.background?.type === 'image' && slideData.background.path) {
60
+ slideData.background.path = resolveTrustedAsset(slideData.background.path, allowedRoot);
61
+ }
62
+ for (const element of slideData.elements) {
63
+ if (element.type === 'image') element.src = resolveTrustedAsset(element.src, allowedRoot);
64
+ }
65
+ }
66
+
36
67
  // Helper: Get body dimensions and check for overflow
37
68
  async function getBodyDimensions(page) {
38
69
  const bodyDimensions = await page.evaluate(() => {
@@ -120,10 +151,7 @@ function validateTextBoxPosition(slideData, bodyDimensions) {
120
151
  // Helper: Add background to slide
121
152
  async function addBackground(slideData, targetSlide, tmpDir) {
122
153
  if (slideData.background.type === 'image' && slideData.background.path) {
123
- let imagePath = slideData.background.path.startsWith('file://')
124
- ? slideData.background.path.replace('file://', '')
125
- : slideData.background.path;
126
- targetSlide.background = { path: imagePath };
154
+ targetSlide.background = { path: slideData.background.path };
127
155
  } else if (slideData.background.type === 'color' && slideData.background.value) {
128
156
  targetSlide.background = { color: slideData.background.value };
129
157
  }
@@ -133,9 +161,8 @@ async function addBackground(slideData, targetSlide, tmpDir) {
133
161
  function addElements(slideData, targetSlide, pres) {
134
162
  for (const el of slideData.elements) {
135
163
  if (el.type === 'image') {
136
- let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src;
137
164
  targetSlide.addImage({
138
- path: imagePath,
165
+ path: el.src,
139
166
  x: el.position.x,
140
167
  y: el.position.y,
141
168
  w: el.position.w,
@@ -911,7 +938,8 @@ async function html2pptx(htmlFile, pres, options = {}) {
911
938
  let bodyDimensions;
912
939
  let slideData;
913
940
 
914
- const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
941
+ const filePath = fs.realpathSync(path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile));
942
+ const assetRoot = path.dirname(filePath);
915
943
  const validationErrors = [];
916
944
 
917
945
  try {
@@ -931,6 +959,7 @@ async function html2pptx(htmlFile, pres, options = {}) {
931
959
  });
932
960
 
933
961
  slideData = await extractSlideData(page);
962
+ constrainSlideAssets(slideData, assetRoot);
934
963
  } finally {
935
964
  await browser.close();
936
965
  }
@@ -976,4 +1005,4 @@ async function html2pptx(htmlFile, pres, options = {}) {
976
1005
  }
977
1006
  }
978
1007
 
979
- module.exports = html2pptx;
1008
+ module.exports = html2pptx;
@@ -13,6 +13,7 @@ Usage:
13
13
 
14
14
  import argparse
15
15
  import json
16
+ import re
16
17
  import os
17
18
  import sys
18
19
  from pathlib import Path
@@ -854,6 +855,11 @@ def scaffold_project(
854
855
  ) -> Dict:
855
856
  """Scaffold a complete frontend project."""
856
857
  features = features or []
858
+ if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) or len(name) > 64:
859
+ return {"error": "Project name must be 1-64 lowercase letters, digits, or hyphen-separated words"}
860
+ output_dir = output_dir.resolve()
861
+ if output_dir.is_symlink():
862
+ return {"error": "Output directory must not be a symbolic link"}
857
863
  project_path = output_dir / name
858
864
 
859
865
  if project_path.exists() and not dry_run:
@@ -12,6 +12,7 @@ Examples:
12
12
  """
13
13
 
14
14
  import sys
15
+ import re
15
16
  from pathlib import Path
16
17
 
17
18
 
@@ -204,6 +205,16 @@ def title_case_skill_name(skill_name):
204
205
  return ' '.join(word.capitalize() for word in skill_name.split('-'))
205
206
 
206
207
 
208
+ SKILL_NAME_RE = re.compile(r"\A[a-z0-9]+(?:-[a-z0-9]+)*\Z")
209
+
210
+
211
+ def validate_skill_name(skill_name):
212
+ """Accept only one portable directory component."""
213
+ if not SKILL_NAME_RE.fullmatch(skill_name) or len(skill_name) > 64:
214
+ raise ValueError("skill name must be 1-64 lowercase letters, digits, or hyphen-separated words")
215
+ return skill_name
216
+
217
+
207
218
  def init_skill(skill_name, path):
208
219
  """
209
220
  Initialize a new skill directory with template SKILL.md.
@@ -215,8 +226,13 @@ def init_skill(skill_name, path):
215
226
  Returns:
216
227
  Path to created skill directory, or None if error
217
228
  """
218
- # Determine skill directory path
219
- skill_dir = safe_user_path(path).resolve() / skill_name
229
+ # Determine skill directory path. Validation prevents absolute paths,
230
+ # separators, dot segments, and traversal from becoming filesystem names.
231
+ skill_name = validate_skill_name(skill_name)
232
+ parent_dir = safe_user_path(path).resolve()
233
+ if parent_dir.is_symlink():
234
+ raise ValueError("skill parent must not be a symbolic link")
235
+ skill_dir = parent_dir / skill_name
220
236
 
221
237
  # Check if directory already exists
222
238
  if skill_dir.exists():
@@ -379,7 +379,7 @@ app = Flask(__name__)
379
379
  TOKEN = "SEU_TOKEN"
380
380
  BASE = f"https://api.telegram.org/bot{TOKEN}"
381
381
 
382
- @app.route(f"/webhook/{TOKEN}", methods=["POST"])
382
+ @app.route("/webhook", methods=["POST"])
383
383
  def webhook():
384
384
  update = request.get_json()
385
385
  if "message" in update and "text" in update["message"]:
@@ -394,7 +394,7 @@ def webhook():
394
394
  ## Registrar Webhook
395
395
 
396
396
  requests.post(f"{BASE}/setWebhook", json={
397
- "url": "https://seu-dominio.com/webhook/" + TOKEN,
397
+ "url": "https://seu-dominio.com/webhook",
398
398
  "allowed_updates": ["message", "callback_query"],
399
399
  "secret_token": "seu_secret_seguro_aqui"
400
400
  })
@@ -3,7 +3,7 @@ TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
3
3
 
4
4
  # Webhook Configuration (optional - for production)
5
5
  WEBHOOK_URL=https://your-domain.com
6
- WEBHOOK_SECRET=your-secret-token-here
6
+ WEBHOOK_SECRET=replace-with-at-least-32-random-characters
7
7
  PORT=3000
8
8
 
9
9
  # AI Integration (optional)
@@ -17,17 +17,18 @@ export class TelegramBotClient {
17
17
  console.log(`Bot @${me.username} (${me.first_name}) started with polling`);
18
18
  }
19
19
 
20
- async startWebhook(port: number, webhookUrl: string, secret?: string): Promise<void> {
20
+ async startWebhook(port: number, webhookUrl: string, secret: string): Promise<void> {
21
+ if (!/^[A-Za-z0-9_-]{32,256}$/.test(secret)) {
22
+ throw new Error('WEBHOOK_SECRET must contain 32-256 random letters, digits, underscores, or hyphens');
23
+ }
21
24
  const app = express();
22
25
  app.disable('x-powered-by');
23
26
  app.use(express.json());
24
27
 
25
28
  app.post('/webhook', async (req, res) => {
26
- if (secret) {
27
- const headerSecret = req.headers['x-telegram-bot-api-secret-token'];
28
- if (headerSecret !== secret) {
29
- return res.sendStatus(403);
30
- }
29
+ const headerSecret = req.headers['x-telegram-bot-api-secret-token'];
30
+ if (headerSecret !== secret) {
31
+ return res.sendStatus(403);
31
32
  }
32
33
 
33
34
  await this.bot.handleUpdate(req.body, res);
@@ -21,6 +21,9 @@ async function main() {
21
21
  const port = parseInt(process.env.PORT || '3000');
22
22
  const webhookUrl = process.env.WEBHOOK_URL!;
23
23
  const secret = process.env.WEBHOOK_SECRET;
24
+ if (!secret) {
25
+ throw new Error('WEBHOOK_SECRET is required in webhook mode');
26
+ }
24
27
  await client.startWebhook(port, webhookUrl, secret);
25
28
  console.log(`Bot running in webhook mode on port ${port}`);
26
29
  } else {
@@ -3,7 +3,7 @@ TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
3
3
 
4
4
  # Webhook Configuration (optional - for production)
5
5
  WEBHOOK_URL=https://your-domain.com
6
- WEBHOOK_SECRET=your-secret-token-here
6
+ WEBHOOK_SECRET=replace-with-at-least-32-random-characters
7
7
  PORT=5000
8
8
 
9
9
  # AI Integration (optional)
@@ -7,6 +7,8 @@ Usage:
7
7
  """
8
8
 
9
9
  import os
10
+ import hmac
11
+ import re
10
12
  import logging
11
13
  import asyncio
12
14
  from dotenv import load_dotenv
@@ -23,7 +25,7 @@ logger = logging.getLogger(__name__)
23
25
 
24
26
  TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
25
27
  WEBHOOK_URL = os.getenv("WEBHOOK_URL")
26
- WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "default-secret")
28
+ WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "")
27
29
  PORT = int(os.getenv("PORT", "5000"))
28
30
 
29
31
  flask_app = Flask(__name__)
@@ -50,12 +52,12 @@ application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
50
52
 
51
53
  # --- Flask Routes ---
52
54
 
53
- @flask_app.route(f"/webhook/{TOKEN}", methods=["POST"])
55
+ @flask_app.route("/webhook", methods=["POST"])
54
56
  def webhook():
55
57
  """Handle incoming Telegram updates."""
56
58
  # Validate secret token
57
59
  secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
58
- if secret != WEBHOOK_SECRET:
60
+ if not secret or not hmac.compare_digest(secret, WEBHOOK_SECRET):
59
61
  logger.warning("Invalid secret token received")
60
62
  return "Forbidden", 403
61
63
 
@@ -85,7 +87,7 @@ def register_webhook():
85
87
  import requests as req
86
88
  url = f"https://api.telegram.org/bot{TOKEN}/setWebhook"
87
89
  resp = req.post(url, json={
88
- "url": f"{WEBHOOK_URL}/webhook/{TOKEN}",
90
+ "url": f"{WEBHOOK_URL.rstrip('/')}/webhook",
89
91
  "allowed_updates": ["message", "callback_query", "inline_query"],
90
92
  "secret_token": WEBHOOK_SECRET,
91
93
  "max_connections": 40
@@ -104,6 +106,9 @@ if __name__ == "__main__":
104
106
  if not WEBHOOK_URL:
105
107
  logger.error("Set WEBHOOK_URL in .env")
106
108
  exit(1)
109
+ if len(WEBHOOK_SECRET) < 32 or len(WEBHOOK_SECRET) > 256 or not re.fullmatch(r"[A-Za-z0-9_-]+", WEBHOOK_SECRET):
110
+ logger.error("Set WEBHOOK_SECRET to 32-256 random characters using letters, digits, _ or -")
111
+ exit(1)
107
112
 
108
113
  register_webhook()
109
114
  logger.info(f"Starting webhook server on port {PORT}")
@@ -26,7 +26,7 @@ Webhooks sao a forma recomendada para producao. O Telegram envia updates via HTT
26
26
  ```
27
27
  POST https://api.telegram.org/bot<TOKEN>/setWebhook
28
28
  {
29
- "url": "https://seu-dominio.com/webhook/<TOKEN>",
29
+ "url": "https://seu-dominio.com/webhook",
30
30
  "allowed_updates": ["message", "callback_query", "inline_query"],
31
31
  "max_connections": 40,
32
32
  "secret_token": "seu_token_secreto_256chars_max"
@@ -55,7 +55,8 @@ import { Telegraf } from 'telegraf';
55
55
  const app = express();
56
56
  const TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
57
57
  const WEBHOOK_URL = process.env.WEBHOOK_URL!; // https://seu-dominio.com
58
- const SECRET_TOKEN = process.env.WEBHOOK_SECRET || 'meu-secret-seguro';
58
+ const SECRET_TOKEN = process.env.WEBHOOK_SECRET;
59
+ if (!SECRET_TOKEN || !/^[A-Za-z0-9_-]{32,256}$/.test(SECRET_TOKEN)) throw new Error('WEBHOOK_SECRET invalido');
59
60
 
60
61
  const bot = new Telegraf(TOKEN);
61
62
 
@@ -109,7 +110,7 @@ from telegram.ext import Application, CommandHandler, MessageHandler, filters
109
110
 
110
111
  TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
111
112
  WEBHOOK_URL = os.getenv('WEBHOOK_URL')
112
- SECRET_TOKEN = os.getenv('WEBHOOK_SECRET', 'meu-secret-seguro')
113
+ SECRET_TOKEN = os.environ['WEBHOOK_SECRET']
113
114
 
114
115
  flask_app = Flask(__name__)
115
116
 
@@ -121,7 +122,7 @@ async def start(update: Update, context):
121
122
 
122
123
  application.add_handler(CommandHandler('start', start))
123
124
 
124
- @flask_app.route(f'/webhook/{TOKEN}', methods=['POST'])
125
+ @flask_app.route('/webhook', methods=['POST'])
125
126
  async def webhook():
126
127
  # Validar secret token
127
128
  secret = request.headers.get('X-Telegram-Bot-Api-Secret-Token')
@@ -141,7 +142,7 @@ import requests
141
142
  requests.post(
142
143
  f'https://api.telegram.org/bot{TOKEN}/setWebhook',
143
144
  json={
144
- 'url': f'{WEBHOOK_URL}/webhook/{TOKEN}',
145
+ 'url': f'{WEBHOOK_URL}/webhook',
145
146
  'allowed_updates': ['message', 'callback_query'],
146
147
  'secret_token': SECRET_TOKEN,
147
148
  'max_connections': 40
@@ -161,7 +162,7 @@ from telegram.ext import Application, CommandHandler, MessageHandler, filters
161
162
 
162
163
  TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
163
164
  WEBHOOK_URL = os.getenv('WEBHOOK_URL')
164
- SECRET_TOKEN = os.getenv('WEBHOOK_SECRET', 'meu-secret-seguro')
165
+ SECRET_TOKEN = os.environ['WEBHOOK_SECRET']
165
166
 
166
167
  app = FastAPI()
167
168
  application = Application.builder().token(TOKEN).build()
@@ -184,7 +185,7 @@ async def on_startup():
184
185
  async def on_shutdown():
185
186
  await application.shutdown()
186
187
 
187
- @app.post(f'/webhook/{TOKEN}')
188
+ @app.post('/webhook')
188
189
  async def webhook(request: Request):
189
190
  secret = request.headers.get('x-telegram-bot-api-secret-token')
190
191
  if secret != SECRET_TOKEN:
@@ -216,7 +217,7 @@ ngrok fornece URL tipo `https://abc123.ngrok-free.app`. Use essa URL para regist
216
217
  ```bash
217
218
  curl -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \
218
219
  -H "Content-Type: application/json" \
219
- -d "{\"url\": \"https://abc123.ngrok-free.app/webhook/$TOKEN\"}"
220
+ -d "{\"url\": \"https://abc123.ngrok-free.app/webhook\"}"
220
221
  ```
221
222
 
222
223
  **Alternativa gratuita:** localtunnel
@@ -290,7 +291,7 @@ CMD ["node", "dist/index.js"]
290
291
  ## Seguranca
291
292
 
292
293
  1. **Secret Token:** Sempre use `secret_token` ao registrar webhook e valide o header `X-Telegram-Bot-Api-Secret-Token`
293
- 2. **URL com token:** Inclua o token na URL do webhook para uma camada extra de seguranca
294
+ 2. **URL sem token:** Nao exponha o bot token na URL; autentique sempre pelo header secreto do Telegram
294
295
  3. **IP whitelist:** Telegram envia webhooks dos IPs:
295
296
  - `149.154.160.0/20`
296
297
  - `91.108.4.0/22`
@@ -76,7 +76,7 @@ default. **Do not use em dashes (—) or arrows (→) in notes/titles.**
76
76
 
77
77
  ### 1. Resolve the id and check embeddability
78
78
  ```
79
- scripts/setup.sh "<youtube_url_or_id>"
79
+ bash scripts/setup.sh "<youtube_url_or_id>"
80
80
  ```
81
81
  Prints the 11-char `YTID`, the scratch dir, the target library path, and whether YouTube
82
82
  **embedding is allowed** (oembed 200) or **blocked** (oembed 401, e.g. some university talks).
@@ -12,11 +12,13 @@ in title + note for each. idx here is just the sequence number; ordering is by t
12
12
 
13
13
  The img URL is served by serve.py at /api/video-deepdives/_media/<file>.
14
14
  """
15
- import subprocess, sys, os, json
15
+ import subprocess, sys, os, json, re, tempfile
16
+ from pathlib import Path
16
17
 
17
18
  LIB = os.path.expanduser(os.environ.get("VIDEO_LIBRARY_DIR", "~/video-deepdives"))
18
19
  MEDIA = os.path.join(LIB, "_media")
19
20
  IMG_PREFIX = "/api/video-deepdives/_media" # served by serve.py
21
+ YTID_RE = re.compile(r"\A[A-Za-z0-9_-]{11}\Z")
20
22
 
21
23
  def mmss(t):
22
24
  t=int(round(float(t))); return f"{t//60:02d}:{t%60:02d}"
@@ -24,19 +26,30 @@ def mmss(t):
24
26
  def main():
25
27
  if len(sys.argv)!=4: sys.exit("usage: extract_slides.py <YTID> <video.mp4> <keep.txt>")
26
28
  ytid,video,keep=sys.argv[1],sys.argv[2],sys.argv[3]
29
+ if not YTID_RE.fullmatch(ytid): sys.exit("YTID must be an exact 11-character YouTube video id")
27
30
  times=sorted({float(l.strip()) for l in open(keep) if l.strip()})
28
31
  if not times: sys.exit("keep.txt is empty")
29
- os.makedirs(MEDIA,exist_ok=True)
32
+ media = Path(MEDIA).expanduser()
33
+ if media.is_symlink(): sys.exit("library media directory must not be a symbolic link")
34
+ media.mkdir(parents=True,exist_ok=True)
35
+ media = media.resolve()
30
36
  scaffold=[]
31
37
  for i,t in enumerate(times,1):
32
38
  fn=f"{ytid}-slide-{i:02d}.jpg"
33
- out=os.path.join(MEDIA,fn)
34
- subprocess.run(["ffmpeg","-hide_banner","-loglevel","error","-ss",f"{t}",
35
- "-i",video,"-frames:v","1","-vf","scale=1280:-1","-q:v","3","-y",out],check=True)
39
+ out=media / fn
40
+ with tempfile.NamedTemporaryFile(prefix=".slide-", suffix=".jpg", dir=media, delete=False) as tmp:
41
+ temporary = Path(tmp.name)
42
+ try:
43
+ subprocess.run(["ffmpeg","-hide_banner","-loglevel","error","-ss",f"{t}",
44
+ "-i",video,"-frames:v","1","-vf","scale=1280:-1","-q:v","3","-y",str(temporary)],check=True)
45
+ os.chmod(temporary, 0o600)
46
+ os.replace(temporary, out)
47
+ finally:
48
+ temporary.unlink(missing_ok=True)
36
49
  scaffold.append({"idx":i,"t":round(t,1),"mmss":mmss(t),"title":"","note":"",
37
50
  "img":f"{IMG_PREFIX}/{fn}"})
38
51
  print(f" wrote {fn} @ {mmss(t)}",file=sys.stderr)
39
- print(f"\nInstalled {len(scaffold)} slides to {MEDIA}",file=sys.stderr)
52
+ print(f"\nInstalled {len(scaffold)} slides to {media}",file=sys.stderr)
40
53
  print("--- slides.json scaffold on stdout; redirect to a file, then fill in title + note ---",file=sys.stderr)
41
54
  print(json.dumps(scaffold,indent=2))
42
55
 
@@ -12,6 +12,7 @@ LIB="${VIDEO_LIBRARY_DIR:-$HOME/video-deepdives}"
12
12
  YTID="$(printf '%s' "$IN" | sed -nE 's#.*(youtu\.be/|v=|/embed/|/shorts/)([A-Za-z0-9_-]{11}).*#\2#p')"
13
13
  [ -z "$YTID" ] && [ "${#IN}" -eq 11 ] && YTID="$IN"
14
14
  [ -z "$YTID" ] && { echo "Could not parse a YouTube id from: $IN" >&2; exit 1; }
15
+ [[ "$YTID" =~ ^[A-Za-z0-9_-]{11}$ ]] || { echo "Invalid YouTube id" >&2; exit 1; }
15
16
 
16
17
  SCRATCH="/tmp/ytnote-$YTID"
17
18
  mkdir -p "$SCRATCH"
@@ -18,7 +18,7 @@ slides.json: a JSON array of slide objects. Each:
18
18
  Writes $VIDEO_LIBRARY_DIR/<YTID>.md (default ~/video-deepdives/<YTID>.md)
19
19
  with YAML frontmatter + transcript body. No em dashes or arrows in titles/notes.
20
20
  """
21
- import argparse, json, os, sys, datetime
21
+ import argparse, json, os, re, sys, datetime
22
22
  from pathlib import Path
23
23
 
24
24
 
@@ -39,6 +39,7 @@ except ImportError:
39
39
  sys.exit("pip install pyyaml")
40
40
 
41
41
  LIB = os.path.expanduser(os.environ.get("VIDEO_LIBRARY_DIR", "~/video-deepdives"))
42
+ YTID_RE = re.compile(r"\A[A-Za-z0-9_-]{11}\Z")
42
43
 
43
44
  def main():
44
45
  ap=argparse.ArgumentParser()
@@ -50,6 +51,8 @@ def main():
50
51
  ap.add_argument("--transcript",required=True)
51
52
  ap.add_argument("--created",default=datetime.date.today().isoformat())
52
53
  a=ap.parse_args()
54
+ if not YTID_RE.fullmatch(a.id):
55
+ sys.exit("--id must be an exact 11-character YouTube video id")
53
56
 
54
57
  slides=json.load(open(a.slides))
55
58
  slides=sorted(slides,key=lambda s:s["t"])
@@ -70,9 +73,17 @@ def main():
70
73
  "slides":slides,
71
74
  }
72
75
  body=open(a.transcript,encoding="utf-8").read().strip()
73
- os.makedirs(LIB,exist_ok=True)
74
- path=os.path.join(LIB,f"{a.id}.md")
75
- with safe_user_path(path).open("w",encoding="utf-8") as f:
76
+ library = Path(LIB).expanduser()
77
+ if library.is_symlink():
78
+ sys.exit("VIDEO_LIBRARY_DIR must not be a symbolic link")
79
+ library.mkdir(parents=True, exist_ok=True)
80
+ library = library.resolve()
81
+ path = library / f"{a.id}.md"
82
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
83
+ if hasattr(os, "O_NOFOLLOW"):
84
+ flags |= os.O_NOFOLLOW
85
+ fd = os.open(path, flags, 0o600)
86
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
76
87
  f.write("---\n")
77
88
  yaml.safe_dump(fm,f,sort_keys=False,allow_unicode=True,width=100)
78
89
  f.write("---\n## Transcript\n")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-skills-collection",
3
- "version": "4.0.16",
3
+ "version": "4.0.18",
4
4
  "description": "OpenCode CLI plugin that automatically downloads and keeps skills up to date.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",