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.
- package/bundled-skills/.antigravity-install-manifest.json +4 -1
- package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +59 -14
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +1 -0
- package/bundled-skills/apify-audience-analysis/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-brand-reputation-monitoring/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-competitor-intelligence/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-content-analytics/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-ecommerce/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-influencer-discovery/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-lead-generation/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-market-research/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-trend-analysis/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/apify-ultimate-scraper/reference/scripts/run_actor.js +9 -10
- package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +22 -6
- package/bundled-skills/context-guardian/scripts/context_snapshot.py +45 -10
- package/bundled-skills/cowork-to-code-bridge/SKILL.md +243 -0
- package/bundled-skills/deploy-to-vercel/resources/deploy.sh +13 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +1 -1
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/aas-core.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/faq.md +20 -7
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +18 -6
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +3 -3
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/fact-check-x-complete/SKILL.md +232 -0
- package/bundled-skills/find-complementary-founders/SKILL.md +47 -9
- package/bundled-skills/find-complementary-founders/references/community-growth.md +39 -28
- package/bundled-skills/find-complementary-founders/references/moltbook.md +2 -1
- package/bundled-skills/find-complementary-founders/references/owner-onboarding.ru.md +29 -14
- package/bundled-skills/find-complementary-founders/references/privacy-safety.md +3 -0
- package/bundled-skills/find-complementary-founders/references/profile-schema.md +3 -2
- package/bundled-skills/find-complementary-founders/scripts/github_thread.py +1 -1
- package/bundled-skills/find-complementary-founders/scripts/moltbook_publish.py +19 -6
- package/bundled-skills/find-complementary-founders/scripts/test_moltbook_publish.py +151 -0
- package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +20 -10
- package/bundled-skills/instagram/scripts/config.py +22 -4
- package/bundled-skills/instagram/scripts/db.py +8 -0
- package/bundled-skills/junta-leiloeiros/scripts/export.py +11 -2
- package/bundled-skills/junta-leiloeiros/scripts/serve_api.py +5 -1
- package/bundled-skills/loki-mode/CLAUDE.md +1 -1
- package/bundled-skills/loki-mode/CONTEXT-EXPORT.md +3 -3
- package/bundled-skills/loki-mode/INSTALLATION.md +3 -3
- package/bundled-skills/loki-mode/README.md +11 -11
- package/bundled-skills/loki-mode/SKILL.md +2 -2
- package/bundled-skills/loki-mode/autonomy/README.md +8 -8
- package/bundled-skills/loki-mode/autonomy/run.sh +23 -54
- package/bundled-skills/loki-mode/demo/README.md +3 -3
- package/bundled-skills/loki-mode/demo/run-demo.sh +3 -3
- package/bundled-skills/loki-mode/demo/vhs-tape.tape +1 -1
- package/bundled-skills/loki-mode/demo/voice-over-script.md +3 -3
- package/bundled-skills/loki-mode/docs/COMPETITIVE-ANALYSIS.md +2 -2
- package/bundled-skills/loki-mode/docs/screenshots/README.md +1 -1
- package/bundled-skills/loki-mode/integrations/vibe-kanban.md +1 -1
- package/bundled-skills/loki-mode/references/agent-types.md +2 -2
- package/bundled-skills/loki-mode/scripts/loki-wrapper.sh +5 -4
- package/bundled-skills/maintain-codex-wiki/SKILL.md +340 -0
- package/bundled-skills/notebooklm/README.md +4 -3
- package/bundled-skills/notebooklm/SKILL.md +3 -3
- package/bundled-skills/notebooklm/scripts/auth_manager.py +6 -4
- package/bundled-skills/notebooklm/scripts/browser_utils.py +2 -1
- package/bundled-skills/notebooklm/scripts/config.py +31 -1
- package/bundled-skills/pptx-official/scripts/html2pptx.js +37 -8
- package/bundled-skills/senior-frontend/scripts/frontend_scaffolder.py +6 -0
- package/bundled-skills/skill-creator/scripts/init_skill.py +18 -2
- package/bundled-skills/telegram/SKILL.md +2 -2
- package/bundled-skills/telegram/assets/boilerplate/nodejs/.env.example +1 -1
- package/bundled-skills/telegram/assets/boilerplate/nodejs/src/bot-client.ts +7 -6
- package/bundled-skills/telegram/assets/boilerplate/nodejs/src/index.ts +3 -0
- package/bundled-skills/telegram/assets/boilerplate/python/.env.example +1 -1
- package/bundled-skills/telegram/assets/boilerplate/python/webhook_server.py +9 -4
- package/bundled-skills/telegram/references/webhook-setup.md +10 -9
- package/bundled-skills/youtube-notetaker/SKILL.md +1 -1
- package/bundled-skills/youtube-notetaker/scripts/extract_slides.py +19 -6
- package/bundled-skills/youtube-notetaker/scripts/setup.sh +1 -0
- package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +15 -4
- package/package.json +1 -1
- package/skills_index.json +94 -1
|
@@ -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
|
-
|
|
269
|
-
|
|
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
|
|
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
|
-
-
|
|
199
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
|
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=
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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=
|
|
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", "
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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=
|
|
34
|
-
|
|
35
|
-
|
|
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 {
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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