claude-skill-lord 2.1.1 → 2.2.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +139 -225
- package/manifests/install-modules.json +1 -1
- package/package.json +3 -7
- package/scripts/lib/profile-utils.js +3 -3
- package/scripts/sl.js +13 -4
- package/.commitlintrc.json +0 -27
- package/.env.example +0 -26
- package/.releaserc.json +0 -119
- package/.repomixignore +0 -22
- package/skills/SKILL.md +0 -207
- package/skills/ai-multimodal/scripts/tests/requirements.txt +0 -20
- package/skills/ai-multimodal/scripts/tests/test_document_converter.py +0 -74
- package/skills/ai-multimodal/scripts/tests/test_failures.log +0 -258
- package/skills/ai-multimodal/scripts/tests/test_gemini_batch_process.py +0 -362
- package/skills/ai-multimodal/scripts/tests/test_media_optimizer.py +0 -373
- package/skills/better-auth/scripts/tests/test_better_auth_init.py +0 -421
- package/skills/chrome-devtools/scripts/__tests__/selector.test.js +0 -210
- package/skills/databases/scripts/tests/coverage-db.json +0 -1
- package/skills/databases/scripts/tests/requirements.txt +0 -4
- package/skills/databases/scripts/tests/test_db_backup.py +0 -340
- package/skills/databases/scripts/tests/test_db_migrate.py +0 -277
- package/skills/databases/scripts/tests/test_db_performance_check.py +0 -370
- package/skills/debugging/scripts/find-polluter.test.md +0 -102
- package/skills/devops/scripts/tests/requirements.txt +0 -3
- package/skills/devops/scripts/tests/test_cloudflare_deploy.py +0 -285
- package/skills/devops/scripts/tests/test_docker_optimize.py +0 -436
- package/skills/docs-seeker/scripts/tests/run-tests.js +0 -72
- package/skills/docs-seeker/scripts/tests/test-analyze-llms.js +0 -119
- package/skills/docs-seeker/scripts/tests/test-detect-topic.js +0 -112
- package/skills/docs-seeker/scripts/tests/test-fetch-docs.js +0 -84
- package/skills/fixtures/compliant_trace.jsonl +0 -5
- package/skills/fixtures/noncompliant_trace.jsonl +0 -3
- package/skills/fixtures/tdd_spec.yaml +0 -44
- package/skills/media-processing/scripts/tests/requirements.txt +0 -2
- package/skills/media-processing/scripts/tests/test_batch_resize.py +0 -372
- package/skills/media-processing/scripts/tests/test_media_convert.py +0 -259
- package/skills/media-processing/scripts/tests/test_video_optimize.py +0 -397
- package/skills/repomix/scripts/tests/test_repomix_batch.py +0 -531
- package/skills/sequential-thinking/tests/format-thought.test.js +0 -133
- package/skills/sequential-thinking/tests/process-thought.test.js +0 -215
- package/skills/shopify/scripts/tests/test_shopify_init.py +0 -385
- package/skills/skill-comply/fixtures/compliant_trace.jsonl +0 -5
- package/skills/skill-comply/fixtures/noncompliant_trace.jsonl +0 -3
- package/skills/skill-comply/fixtures/tdd_spec.yaml +0 -44
- package/skills/skill-comply/tests/test_grader.py +0 -137
- package/skills/skill-comply/tests/test_parser.py +0 -90
- package/skills/tests/test_grader.py +0 -137
- package/skills/tests/test_parser.py +0 -90
- package/skills/ui-styling/scripts/tests/coverage-ui.json +0 -1
- package/skills/ui-styling/scripts/tests/requirements.txt +0 -3
- package/skills/ui-styling/scripts/tests/test_shadcn_add.py +0 -266
- package/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +0 -336
- package/skills/web-frameworks/scripts/tests/coverage-web.json +0 -1
- package/skills/web-frameworks/scripts/tests/requirements.txt +0 -3
- package/skills/web-frameworks/scripts/tests/test_nextjs_init.py +0 -319
- package/skills/web-frameworks/scripts/tests/test_turborepo_migrate.py +0 -374
package/scripts/sl.js
CHANGED
|
@@ -294,9 +294,18 @@ const commands = {
|
|
|
294
294
|
agents.forEach(a => console.log(` - ${a.replace('.md', '')}`));
|
|
295
295
|
|
|
296
296
|
const skillsDir = path.join(rootDir, 'skills');
|
|
297
|
-
const skills =
|
|
298
|
-
|
|
299
|
-
.
|
|
297
|
+
const skills = [];
|
|
298
|
+
const collectSkills = (dir, prefix) => {
|
|
299
|
+
fs.readdirSync(dir, { withFileTypes: true }).forEach(e => {
|
|
300
|
+
if (!e.isDirectory()) return;
|
|
301
|
+
const full = path.join(dir, e.name);
|
|
302
|
+
const label = prefix ? `${prefix}/${e.name}` : e.name;
|
|
303
|
+
if (fs.existsSync(path.join(full, 'SKILL.md'))) skills.push(label);
|
|
304
|
+
else collectSkills(full, label);
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
collectSkills(skillsDir, '');
|
|
308
|
+
skills.sort();
|
|
300
309
|
console.log(`\n Skills: ${skills.length}`);
|
|
301
310
|
skills.forEach(s => console.log(` - ${s}`));
|
|
302
311
|
|
|
@@ -449,7 +458,7 @@ const commands = {
|
|
|
449
458
|
Usage: csl <command> [options]
|
|
450
459
|
|
|
451
460
|
Commands:
|
|
452
|
-
init Install in current project (43 agents,
|
|
461
|
+
init Install in current project (43 agents, 165 skills, 114 commands)
|
|
453
462
|
update Update CLI to latest version
|
|
454
463
|
migrate Update project files after csl update
|
|
455
464
|
diff Compare project files with source package
|
package/.commitlintrc.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": ["@commitlint/config-conventional"],
|
|
3
|
-
"rules": {
|
|
4
|
-
"type-enum": [
|
|
5
|
-
2,
|
|
6
|
-
"always",
|
|
7
|
-
[
|
|
8
|
-
"build",
|
|
9
|
-
"chore",
|
|
10
|
-
"ci",
|
|
11
|
-
"docs",
|
|
12
|
-
"feat",
|
|
13
|
-
"fix",
|
|
14
|
-
"perf",
|
|
15
|
-
"refactor",
|
|
16
|
-
"revert",
|
|
17
|
-
"style",
|
|
18
|
-
"test"
|
|
19
|
-
]
|
|
20
|
-
],
|
|
21
|
-
"subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
|
|
22
|
-
"subject-empty": [2, "never"],
|
|
23
|
-
"subject-full-stop": [2, "never", "."],
|
|
24
|
-
"header-max-length": [2, "always", 100],
|
|
25
|
-
"body-max-line-length": [1, "always", 300]
|
|
26
|
-
}
|
|
27
|
-
}
|
package/.env.example
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Claude Code Notification Hooks - Environment Variables
|
|
2
|
-
# Copy this file to .env and fill in your actual values
|
|
3
|
-
# NEVER commit .env files to version control
|
|
4
|
-
|
|
5
|
-
# ============================================
|
|
6
|
-
# Discord Notifications
|
|
7
|
-
# ============================================
|
|
8
|
-
# Get your webhook URL from Discord:
|
|
9
|
-
# Server Settings → Integrations → Webhooks → New Webhook
|
|
10
|
-
# Format: https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
|
11
|
-
DISCORD_WEBHOOK_URL=
|
|
12
|
-
|
|
13
|
-
# ============================================
|
|
14
|
-
# Telegram Notifications
|
|
15
|
-
# ============================================
|
|
16
|
-
# Get bot token from @BotFather in Telegram
|
|
17
|
-
# Send /newbot to @BotFather and follow the prompts
|
|
18
|
-
# Format: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
|
19
|
-
TELEGRAM_BOT_TOKEN=
|
|
20
|
-
|
|
21
|
-
# Get chat ID from Telegram
|
|
22
|
-
# For DM: Send message to bot, then visit:
|
|
23
|
-
# https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
|
|
24
|
-
# For groups: Add bot to group, send message, check same URL
|
|
25
|
-
# Format: 987654321 (positive) or -100123456789 (negative for groups)
|
|
26
|
-
TELEGRAM_CHAT_ID=
|
package/.releaserc.json
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"branches": [
|
|
3
|
-
"main"
|
|
4
|
-
],
|
|
5
|
-
"plugins": [
|
|
6
|
-
[
|
|
7
|
-
"@semantic-release/commit-analyzer",
|
|
8
|
-
{
|
|
9
|
-
"preset": "conventionalcommits",
|
|
10
|
-
"releaseRules": [
|
|
11
|
-
{
|
|
12
|
-
"type": "docs",
|
|
13
|
-
"scope": "README",
|
|
14
|
-
"release": "patch"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"type": "refactor",
|
|
18
|
-
"release": "patch"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"type": "style",
|
|
22
|
-
"release": "patch"
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
[
|
|
28
|
-
"@semantic-release/release-notes-generator",
|
|
29
|
-
{
|
|
30
|
-
"preset": "conventionalcommits",
|
|
31
|
-
"presetConfig": {
|
|
32
|
-
"types": [
|
|
33
|
-
{
|
|
34
|
-
"type": "feat",
|
|
35
|
-
"section": "🚀 Features"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"type": "fix",
|
|
39
|
-
"section": "🐞 Bug Fixes"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"type": "docs",
|
|
43
|
-
"section": "📚 Documentation"
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"type": "style",
|
|
47
|
-
"section": "💄 Styles"
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"type": "refactor",
|
|
51
|
-
"section": "♻️ Code Refactoring"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"type": "perf",
|
|
55
|
-
"section": "⚡ Performance Improvements"
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
"type": "test",
|
|
59
|
-
"section": "✅ Tests"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"type": "build",
|
|
63
|
-
"section": "🏗️ Build System"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"type": "ci",
|
|
67
|
-
"section": "👷 CI"
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
],
|
|
73
|
-
[
|
|
74
|
-
"@semantic-release/changelog",
|
|
75
|
-
{
|
|
76
|
-
"changelogFile": "CHANGELOG.md"
|
|
77
|
-
}
|
|
78
|
-
],
|
|
79
|
-
[
|
|
80
|
-
"@semantic-release/npm",
|
|
81
|
-
{
|
|
82
|
-
"npmPublish": false
|
|
83
|
-
}
|
|
84
|
-
],
|
|
85
|
-
[
|
|
86
|
-
"@semantic-release/exec",
|
|
87
|
-
{
|
|
88
|
-
"prepareCmd": "node scripts/prepare-release-assets.cjs ${nextRelease.version}"
|
|
89
|
-
}
|
|
90
|
-
],
|
|
91
|
-
[
|
|
92
|
-
"@semantic-release/github",
|
|
93
|
-
{
|
|
94
|
-
"assets": [
|
|
95
|
-
{
|
|
96
|
-
"path": "CHANGELOG.md",
|
|
97
|
-
"label": "Changelog"
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
"path": "dist/claude-skill-lord.zip",
|
|
101
|
-
"label": "Claude Skill Lord Package"
|
|
102
|
-
}
|
|
103
|
-
]
|
|
104
|
-
}
|
|
105
|
-
],
|
|
106
|
-
[
|
|
107
|
-
"@semantic-release/git",
|
|
108
|
-
{
|
|
109
|
-
"assets": [
|
|
110
|
-
"CHANGELOG.md",
|
|
111
|
-
"package.json",
|
|
112
|
-
"package-lock.json",
|
|
113
|
-
".claude/metadata.json"
|
|
114
|
-
],
|
|
115
|
-
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
116
|
-
}
|
|
117
|
-
]
|
|
118
|
-
]
|
|
119
|
-
}
|
package/.repomixignore
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
docs/*
|
|
2
|
-
plans/*
|
|
3
|
-
assets/*
|
|
4
|
-
dist/*
|
|
5
|
-
coverage/*
|
|
6
|
-
build/*
|
|
7
|
-
ios/*
|
|
8
|
-
android/*
|
|
9
|
-
tests/*
|
|
10
|
-
__tests__/*
|
|
11
|
-
__pycache__/*
|
|
12
|
-
node_modules/*
|
|
13
|
-
|
|
14
|
-
.opencode/*
|
|
15
|
-
.claude/*
|
|
16
|
-
.serena/*
|
|
17
|
-
.pnpm-store/*
|
|
18
|
-
.github/*
|
|
19
|
-
.dart_tool/*
|
|
20
|
-
.idea/*
|
|
21
|
-
.husky/*
|
|
22
|
-
.venv/*
|
package/skills/SKILL.md
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: x-api
|
|
3
|
-
description: X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# X API
|
|
7
|
-
|
|
8
|
-
Programmatic interaction with X (Twitter) for posting, reading, searching, and analytics.
|
|
9
|
-
|
|
10
|
-
## When to Activate
|
|
11
|
-
|
|
12
|
-
- User wants to post tweets or threads programmatically
|
|
13
|
-
- Reading timeline, mentions, or user data from X
|
|
14
|
-
- Searching X for content, trends, or conversations
|
|
15
|
-
- Building X integrations or bots
|
|
16
|
-
- Analytics and engagement tracking
|
|
17
|
-
- User says "post to X", "tweet", "X API", or "Twitter API"
|
|
18
|
-
|
|
19
|
-
## Authentication
|
|
20
|
-
|
|
21
|
-
### OAuth 2.0 Bearer Token (App-Only)
|
|
22
|
-
|
|
23
|
-
Best for: read-heavy operations, search, public data.
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# Environment setup
|
|
27
|
-
export X_BEARER_TOKEN="your-bearer-token"
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
```python
|
|
31
|
-
import os
|
|
32
|
-
import requests
|
|
33
|
-
|
|
34
|
-
bearer = os.environ["X_BEARER_TOKEN"]
|
|
35
|
-
headers = {"Authorization": f"Bearer {bearer}"}
|
|
36
|
-
|
|
37
|
-
# Search recent tweets
|
|
38
|
-
resp = requests.get(
|
|
39
|
-
"https://api.x.com/2/tweets/search/recent",
|
|
40
|
-
headers=headers,
|
|
41
|
-
params={"query": "claude code", "max_results": 10}
|
|
42
|
-
)
|
|
43
|
-
tweets = resp.json()
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### OAuth 1.0a (User Context)
|
|
47
|
-
|
|
48
|
-
Required for: posting tweets, managing account, DMs.
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# Environment setup — source before use
|
|
52
|
-
export X_API_KEY="your-api-key"
|
|
53
|
-
export X_API_SECRET="your-api-secret"
|
|
54
|
-
export X_ACCESS_TOKEN="your-access-token"
|
|
55
|
-
export X_ACCESS_SECRET="your-access-secret"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
import os
|
|
60
|
-
from requests_oauthlib import OAuth1Session
|
|
61
|
-
|
|
62
|
-
oauth = OAuth1Session(
|
|
63
|
-
os.environ["X_API_KEY"],
|
|
64
|
-
client_secret=os.environ["X_API_SECRET"],
|
|
65
|
-
resource_owner_key=os.environ["X_ACCESS_TOKEN"],
|
|
66
|
-
resource_owner_secret=os.environ["X_ACCESS_SECRET"],
|
|
67
|
-
)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Core Operations
|
|
71
|
-
|
|
72
|
-
### Post a Tweet
|
|
73
|
-
|
|
74
|
-
```python
|
|
75
|
-
resp = oauth.post(
|
|
76
|
-
"https://api.x.com/2/tweets",
|
|
77
|
-
json={"text": "Hello from Claude Code"}
|
|
78
|
-
)
|
|
79
|
-
resp.raise_for_status()
|
|
80
|
-
tweet_id = resp.json()["data"]["id"]
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Post a Thread
|
|
84
|
-
|
|
85
|
-
```python
|
|
86
|
-
def post_thread(oauth, tweets: list[str]) -> list[str]:
|
|
87
|
-
ids = []
|
|
88
|
-
reply_to = None
|
|
89
|
-
for text in tweets:
|
|
90
|
-
payload = {"text": text}
|
|
91
|
-
if reply_to:
|
|
92
|
-
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
|
|
93
|
-
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
|
|
94
|
-
tweet_id = resp.json()["data"]["id"]
|
|
95
|
-
ids.append(tweet_id)
|
|
96
|
-
reply_to = tweet_id
|
|
97
|
-
return ids
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Read User Timeline
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
resp = requests.get(
|
|
104
|
-
f"https://api.x.com/2/users/{user_id}/tweets",
|
|
105
|
-
headers=headers,
|
|
106
|
-
params={
|
|
107
|
-
"max_results": 10,
|
|
108
|
-
"tweet.fields": "created_at,public_metrics",
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Search Tweets
|
|
114
|
-
|
|
115
|
-
```python
|
|
116
|
-
resp = requests.get(
|
|
117
|
-
"https://api.x.com/2/tweets/search/recent",
|
|
118
|
-
headers=headers,
|
|
119
|
-
params={
|
|
120
|
-
"query": "from:affaanmustafa -is:retweet",
|
|
121
|
-
"max_results": 10,
|
|
122
|
-
"tweet.fields": "public_metrics,created_at",
|
|
123
|
-
}
|
|
124
|
-
)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### Get User by Username
|
|
128
|
-
|
|
129
|
-
```python
|
|
130
|
-
resp = requests.get(
|
|
131
|
-
"https://api.x.com/2/users/by/username/affaanmustafa",
|
|
132
|
-
headers=headers,
|
|
133
|
-
params={"user.fields": "public_metrics,description,created_at"}
|
|
134
|
-
)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Upload Media and Post
|
|
138
|
-
|
|
139
|
-
```python
|
|
140
|
-
# Media upload uses v1.1 endpoint
|
|
141
|
-
|
|
142
|
-
# Step 1: Upload media
|
|
143
|
-
media_resp = oauth.post(
|
|
144
|
-
"https://upload.twitter.com/1.1/media/upload.json",
|
|
145
|
-
files={"media": open("image.png", "rb")}
|
|
146
|
-
)
|
|
147
|
-
media_id = media_resp.json()["media_id_string"]
|
|
148
|
-
|
|
149
|
-
# Step 2: Post with media
|
|
150
|
-
resp = oauth.post(
|
|
151
|
-
"https://api.x.com/2/tweets",
|
|
152
|
-
json={"text": "Check this out", "media": {"media_ids": [media_id]}}
|
|
153
|
-
)
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Rate Limits
|
|
157
|
-
|
|
158
|
-
X API rate limits vary by endpoint, auth method, and account tier, and they change over time. Always:
|
|
159
|
-
- Check the current X developer docs before hardcoding assumptions
|
|
160
|
-
- Read `x-rate-limit-remaining` and `x-rate-limit-reset` headers at runtime
|
|
161
|
-
- Back off automatically instead of relying on static tables in code
|
|
162
|
-
|
|
163
|
-
```python
|
|
164
|
-
import time
|
|
165
|
-
|
|
166
|
-
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
|
|
167
|
-
if remaining < 5:
|
|
168
|
-
reset = int(resp.headers.get("x-rate-limit-reset", 0))
|
|
169
|
-
wait = max(0, reset - int(time.time()))
|
|
170
|
-
print(f"Rate limit approaching. Resets in {wait}s")
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## Error Handling
|
|
174
|
-
|
|
175
|
-
```python
|
|
176
|
-
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
|
|
177
|
-
if resp.status_code == 201:
|
|
178
|
-
return resp.json()["data"]["id"]
|
|
179
|
-
elif resp.status_code == 429:
|
|
180
|
-
reset = int(resp.headers["x-rate-limit-reset"])
|
|
181
|
-
raise Exception(f"Rate limited. Resets at {reset}")
|
|
182
|
-
elif resp.status_code == 403:
|
|
183
|
-
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
|
|
184
|
-
else:
|
|
185
|
-
raise Exception(f"X API error {resp.status_code}: {resp.text}")
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Security
|
|
189
|
-
|
|
190
|
-
- **Never hardcode tokens.** Use environment variables or `.env` files.
|
|
191
|
-
- **Never commit `.env` files.** Add to `.gitignore`.
|
|
192
|
-
- **Rotate tokens** if exposed. Regenerate at developer.x.com.
|
|
193
|
-
- **Use read-only tokens** when write access is not needed.
|
|
194
|
-
- **Store OAuth secrets securely** — not in source code or logs.
|
|
195
|
-
|
|
196
|
-
## Integration with Content Engine
|
|
197
|
-
|
|
198
|
-
Use `content-engine` skill to generate platform-native content, then post via X API:
|
|
199
|
-
1. Generate content with content-engine (X platform format)
|
|
200
|
-
2. Validate length (280 chars for single tweet)
|
|
201
|
-
3. Post via X API using patterns above
|
|
202
|
-
4. Track engagement via public_metrics
|
|
203
|
-
|
|
204
|
-
## Related Skills
|
|
205
|
-
|
|
206
|
-
- `content-engine` — Generate platform-native content for X
|
|
207
|
-
- `crosspost` — Distribute content across X, LinkedIn, and other platforms
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Core dependencies
|
|
2
|
-
google-genai>=0.2.0
|
|
3
|
-
python-dotenv>=1.0.0
|
|
4
|
-
|
|
5
|
-
# Image processing
|
|
6
|
-
pillow>=10.0.0
|
|
7
|
-
|
|
8
|
-
# PDF processing
|
|
9
|
-
pypdf>=3.0.0
|
|
10
|
-
|
|
11
|
-
# Document conversion
|
|
12
|
-
markdown>=3.5
|
|
13
|
-
|
|
14
|
-
# Testing
|
|
15
|
-
pytest>=7.4.0
|
|
16
|
-
pytest-cov>=4.1.0
|
|
17
|
-
pytest-mock>=3.12.0
|
|
18
|
-
|
|
19
|
-
# Optional dependencies for full functionality
|
|
20
|
-
# ffmpeg-python>=0.2.0 # For media optimization (requires ffmpeg installed)
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for document_converter.py
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
import sys
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from unittest.mock import Mock, patch, MagicMock, mock_open
|
|
9
|
-
|
|
10
|
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
11
|
-
|
|
12
|
-
import document_converter as dc
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestAPIKeyFinder:
|
|
16
|
-
"""Test API key finding logic."""
|
|
17
|
-
|
|
18
|
-
@patch.dict('os.environ', {'GEMINI_API_KEY': 'test-key-from-env'})
|
|
19
|
-
def test_find_api_key_from_env(self):
|
|
20
|
-
"""Test finding API key from environment."""
|
|
21
|
-
api_key = dc.find_api_key()
|
|
22
|
-
assert api_key == 'test-key-from-env'
|
|
23
|
-
|
|
24
|
-
@patch.dict('os.environ', {}, clear=True)
|
|
25
|
-
@patch('document_converter.load_dotenv', None)
|
|
26
|
-
def test_find_api_key_no_key(self):
|
|
27
|
-
"""Test when no API key is available."""
|
|
28
|
-
api_key = dc.find_api_key()
|
|
29
|
-
assert api_key is None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class TestProjectRoot:
|
|
33
|
-
"""Test project root finding."""
|
|
34
|
-
|
|
35
|
-
@patch('pathlib.Path.exists')
|
|
36
|
-
def test_find_project_root_with_git(self, mock_exists):
|
|
37
|
-
"""Test finding project root with .git directory."""
|
|
38
|
-
root = dc.find_project_root()
|
|
39
|
-
assert isinstance(root, Path)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class TestMimeType:
|
|
43
|
-
"""Test MIME type detection."""
|
|
44
|
-
|
|
45
|
-
def test_pdf_mime_type(self):
|
|
46
|
-
"""Test PDF MIME type."""
|
|
47
|
-
assert dc.get_mime_type('document.pdf') == 'application/pdf'
|
|
48
|
-
|
|
49
|
-
def test_image_mime_types(self):
|
|
50
|
-
"""Test image MIME types."""
|
|
51
|
-
assert dc.get_mime_type('image.jpg') == 'image/jpeg'
|
|
52
|
-
assert dc.get_mime_type('image.png') == 'image/png'
|
|
53
|
-
|
|
54
|
-
def test_unknown_mime_type(self):
|
|
55
|
-
"""Test unknown file extension."""
|
|
56
|
-
assert dc.get_mime_type('file.unknown') == 'application/octet-stream'
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class TestIntegration:
|
|
60
|
-
"""Integration tests."""
|
|
61
|
-
|
|
62
|
-
def test_mime_type_integration(self):
|
|
63
|
-
"""Test MIME type detection with various extensions."""
|
|
64
|
-
test_cases = [
|
|
65
|
-
('document.pdf', 'application/pdf'),
|
|
66
|
-
('image.jpg', 'image/jpeg'),
|
|
67
|
-
('unknown.xyz', 'application/octet-stream'),
|
|
68
|
-
]
|
|
69
|
-
for file_path, expected_mime in test_cases:
|
|
70
|
-
assert dc.get_mime_type(file_path) == expected_mime
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if __name__ == '__main__':
|
|
74
|
-
pytest.main([__file__, '-v', '--cov=document_converter', '--cov-report=term-missing'])
|