Add social media automation kit for daily Threads posting - #4
Merged
Conversation
- 30 posts prontos para Twitter/X, LinkedIn e Instagram
- Calendário de conteúdo com 4 semanas temáticas (apresentação,
features, workflows, comunidade)
- Banco de hashtags por plataforma
- Script schedule.sh com agendamento cron às 18h diário:
schedule.sh install → cron job 18h todo dia
schedule.sh today → post do dia
schedule.sh day N → post do dia N
schedule.sh list → lista todos os posts
- Ciclo automático de 30 dias com log de publicações
https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
- poster.py: publicador cross-platform (Windows/macOS/Linux)
- 'today', 'day N', 'list', 'setup', 'schedule install/uninstall/status'
- Ciclo automático de 30 dias baseado na data de início
- Log JSONL de cada publicação
- dry-run mode para visualizar sem publicar
- threads_client.py: dois modos de publicação
- Modo 'api': API Graph oficial do Threads (sem browser)
- Modo 'browser': Playwright abre Chrome com perfil do usuário,
detecta sessão logada, posta via automação, screenshot de confirmação
- Modo 'auto': tenta API primeiro, cai para browser como fallback
- setup-windows.ps1: setup interativo para Windows
- Verifica Python 3.11+ e Google Chrome
- Instala dependências + playwright install chrome
- Auto-detecta caminho do perfil Chrome do usuário
- Instala Task Scheduler (18h diário) via Register-ScheduledTask
- windows-task.xml: XML de importação manual para Task Scheduler
- requirements-social.txt: httpx, playwright, python-dotenv, mistune
- .env.example: template de configuração comentado
- .gitignore: exclui .env, .start-date, screenshots, logs
https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
Skill /kambo-social:
- Calcula o dia do ciclo de 30 dias (lê .start-date, cria se não existir)
- Carrega o post correspondente de social-media/posts/
- Extrai o texto do primeiro bloco de código do markdown
- Usa ferramentas playwright_* do browser MCP para:
→ navegar para threads.net
→ verificar login (para se não estiver logado)
→ abrir o composer, injetar texto via JS (preserva emojis)
→ clicar em Publicar
→ tirar screenshot de confirmação
- Registra cada publicação em social-media/post-log.jsonl
- Suporta 'publica o dia N' para forçar um dia específico
Requer @playwright/mcp configurado no .mcp.json (ver README)
https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
Remove referência ao @playwright/mcp externo. A skill agora usa as ferramentas de browser integradas ao Claude Code, disponíveis quando iniciado com 'claude --chrome'. Também adiciona /kambo-social à tabela de roteamento do CLAUDE.md. https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
Reviewer's GuideAdds a cross-platform social media automation kit under social-media/ to publish a 30‑day rotating series of Kambo marketing posts to Threads, including a Python poster CLI with scheduling, a dual-mode Threads client (API or browser via Playwright/Chrome), OS-specific schedulers and setup scripts, pre-written post content, documentation, and a new Claude Code skill wiring this flow into the /kambo-social trigger. Sequence diagram for posting a daily Kambo message to ThreadssequenceDiagram
actor User
participant PosterCLI as poster.py
participant ThreadsClient as publish_to_threads
participant ThreadsAPI as ThreadsAPIClient
participant BrowserMode as post_via_browser
User->>PosterCLI: main(today | day N)
PosterCLI->>PosterCLI: get_cycle_day / POST_MAP lookup
PosterCLI->>PosterCLI: extract_post_text
PosterCLI->>ThreadsClient: publish_to_threads(text)
alt POSTER_MODE=api or auto with API creds
ThreadsClient->>ThreadsAPI: post(text)
ThreadsAPI-->>ThreadsClient: {success, post_id, url, mode=api}
else POSTER_MODE=browser or API unavailable
ThreadsClient->>BrowserMode: post_via_browser(text)
BrowserMode-->>ThreadsClient: {success, url=threads.net, mode=browser, screenshot}
end
ThreadsClient-->>PosterCLI: result
PosterCLI->>PosterCLI: log_post(day, post_path, result)
PosterCLI-->>User: print status + URL/screenshot
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Regression validator
No regressions or failures detected. |
There was a problem hiding this comment.
Hey - I've found 1 security issue, 11 other issues, and left some high level feedback:
Security issues:
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
General comments:
- The 30‑day post mapping is currently duplicated in
poster.py,schedule.shand.claude/skills/kambo-social/SKILL.md, which will easily drift over time; consider centralizing this mapping (e.g. in a shared JSON/YAML file) and loading it in each consumer instead of hard‑coding it three times. - In
schedule.sh, the date fallback usesdate -dwhich is not available on macOS (BSDdate), so the cycle‑day calculation will break on that platform; it would be safer to rely on a single, portable implementation (e.g. always callingpython3or using a small helper script) instead of shell date arithmetic.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The 30‑day post mapping is currently duplicated in `poster.py`, `schedule.sh` and `.claude/skills/kambo-social/SKILL.md`, which will easily drift over time; consider centralizing this mapping (e.g. in a shared JSON/YAML file) and loading it in each consumer instead of hard‑coding it three times.
- In `schedule.sh`, the date fallback uses `date -d` which is not available on macOS (BSD `date`), so the cycle‑day calculation will break on that platform; it would be safer to rely on a single, portable implementation (e.g. always calling `python3` or using a small helper script) instead of shell date arithmetic.
## Individual Comments
### Comment 1
<location path="social-media/poster.py" line_range="121" />
<code_context>
+ content = markdown_path.read_text(encoding="utf-8")
+
+ # Procura blocos de código: ```\n...\n```
+ pattern = re.compile(r"```\n(.*?)\n```", re.DOTALL)
+ matches = pattern.findall(content)
+
</code_context>
<issue_to_address>
**issue:** Markdown code fence extraction is brittle for language-tagged or CRLF code blocks.
The regex only matches fences of the form ```\n...\n``` and will miss common variants like ```txt or CRLF-terminated lines, causing `extract_post_text` to fail on valid Markdown. Please make the pattern more flexible (e.g., allow an optional language tag after ``` and support `\r?\n`), or consider using a Markdown parser to get the first fenced code block instead of relying on a brittle regex.
</issue_to_address>
### Comment 2
<location path="social-media/poster.py" line_range="382-384" />
<code_context>
+ print("ℹ️ Nenhum crontab encontrado.")
+ return
+
+ lines = [
+ l for l in result.stdout.splitlines()
+ if "KamboSocialPoster" not in l and "poster.py" not in l
+ ]
+ new_crontab = "\n".join(lines) + "\n"
</code_context>
<issue_to_address>
**issue (bug_risk):** Cron uninstall will delete any crontab line mentioning `poster.py`, not just this tool's job.
In `schedule_unix_uninstall`, this filter removes any crontab line containing `poster.py`, which could delete unrelated jobs that happen to call a poster script. Since the installed entry is already tagged with `# KamboSocialPoster`, please match on that marker (and ideally the full command) instead of the generic filename.
</issue_to_address>
### Comment 3
<location path="social-media/threads_client.py" line_range="244" />
<code_context>
+
+ if not text_area:
+ # Screenshot para debug
+ screenshot_path = Path(__file__).parent / "debug-screenshot.png"
+ await page.screenshot(path=str(screenshot_path))
+ raise RuntimeError(
</code_context>
<issue_to_address>
**suggestion:** Debug screenshots are always written to the same filename, potentially losing prior diagnostics.
This file is overwritten on each failure, which can make diagnosing intermittent issues difficult across runs. Consider including a timestamp or UUID in the filename (as you do for success screenshots) so multiple failure snapshots are preserved.
Suggested implementation:
```python
from pathlib import Path
from datetime import datetime
```
```python
if not text_area:
# Screenshot para debug (usar nome único para não sobrescrever)
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
screenshot_path = Path(__file__).parent / f"debug-screenshot-{timestamp}.png"
await page.screenshot(path=str(screenshot_path))
raise RuntimeError(
```
If `from pathlib import Path` is not present in this file, you will also need to add it near the other imports.
Optionally, you may want to include `str(screenshot_path)` in the `RuntimeError` message body so the caller can easily locate the saved debug screenshot.
</issue_to_address>
### Comment 4
<location path="social-media/schedule.sh" line_range="84-93" />
<code_context>
+ # Se o arquivo .start-date existir, usa como referência
+ if [[ -f "$SCRIPT_DIR/.start-date" ]]; then
+ START_DATE=$(cat "$SCRIPT_DIR/.start-date")
+ else
+ # Primeira execução: salva a data de início
+ echo "$START_DATE" > "$SCRIPT_DIR/.start-date"
+ fi
+
+ TODAY=$(date +%Y-%m-%d)
+
+ # Calcula diferença em dias
+ if command -v python3 &>/dev/null; then
+ DIFF=$(python3 -c "from datetime import date; print((date.fromisoformat('$TODAY') - date.fromisoformat('$START_DATE')).days)")
+ else
+ # Fallback: usa date aritmética do sistema
+ DIFF=$(( ($(date -d "$TODAY" +%s) - $(date -d "$START_DATE" +%s)) / 86400 ))
+ fi
+
</code_context>
<issue_to_address>
**issue (bug_risk):** GNU `date -d` is not portable to macOS, which can break the day calculation.
This fallback still relies on GNU `date -d`, so it will fail on macOS’s BSD `date`. Since this is meant to run on Linux/macOS, consider a more portable approach: for example, always use `python3` when available and otherwise either hard-fail with a clear message or add a macOS-specific branch using `date -j -f`. As is, macOS users without `python3` will hit a runtime error here.
</issue_to_address>
### Comment 5
<location path="social-media/setup-windows.ps1" line_range="132-135" />
<code_context>
+ Write-Host "✅ Perfil encontrado: $chromeProfileDir" -ForegroundColor Green
+
+ # Atualiza .env com o caminho do perfil
+ $envContent = Get-Content $envFile -Raw
+ if ($envContent -match "CHROME_PROFILE_DIR=`$") {
+ $envContent = $envContent -replace "CHROME_PROFILE_DIR=", "CHROME_PROFILE_DIR=$chromeProfileDir"
+ Set-Content $envFile $envContent -NoNewline
+ Write-Host " Caminho adicionado ao .env automaticamente." -ForegroundColor Green
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** `.env` patching for `CHROME_PROFILE_DIR` is fragile and may not match the actual template format.
The pattern only matches a line ending with `CHROME_PROFILE_DIR=` and then performs a blind `-replace` across the whole file, which risks altering other occurrences unintentionally. `Set-Content -NoNewline` also removes the trailing newline, which can break some tools. Consider processing the file line-by-line so only the `CHROME_PROFILE_DIR` entry is updated, appending it if absent, and keeping the final newline intact.
</issue_to_address>
### Comment 6
<location path="social-media/posts/linkedin/dia-18-selfimprove.md" line_range="20" />
<code_context>
+• Precisão dos findings
+• Padrão de uso ao longo do tempo
+
+Isso cria um histórico objetividade de desempenho por ferramenta.
+
+━━━━━━━━━━━━━━━━━━━━━━━━
</code_context>
<issue_to_address>
**issue (typo):** Ajustar para "histórico objetivo" para corrigir a construção gramatical.
Use "Isso cria um histórico objetivo de desempenho por ferramenta.", pois "histórico objetividade" é uma construção gramaticalmente incorreta em português.
</issue_to_address>
### Comment 7
<location path="social-media/posts/twitter/dia-11-evidence.md" line_range="51" />
<code_context>
+```
+
+```
+O model de confiança é configurável.
+
+Você pode calibrar os thresholds via /kambo-calibrate baseado no seu histórico de reports aceitos/rejeitados.
</code_context>
<issue_to_address>
**issue (typo):** Trocar "model" por "modelo" para manter o português correto.
Como o restante do parágrafo está em português, manter "modelo" aqui preserva a consistência do texto.
```suggestion
O modelo de confiança é configurável.
```
</issue_to_address>
### Comment 8
<location path="social-media/posts/twitter/dia-02-problema.md" line_range="40" />
<code_context>
+2. "faça um bug bounty completo"
+3. ☕ beba seu café
+
+O Claude executa as 5 fases, gradu os findings e gera o template HackerOne/Bugcrowd automaticamente.
+```
+
</code_context>
<issue_to_address>
**issue (typo):** Corrigir "gradu" para "gradua" no verbo.
Faltou uma letra no verbo: use "gradua" em vez de "gradu" para manter a frase gramaticalmente correta.
Suggested implementation:
```
O Claude executa as 5 fases, gradua os findings e gera o template HackerOne/Bugcrowd automaticamente.
```
Recomendo fazer uma busca rápida por "gradu " no restante do repositório para garantir que não existam outras ocorrências com a mesma grafia incorreta que também precisem ser corrigidas.
</issue_to_address>
### Comment 9
<location path="social-media/posts/linkedin/dia-12-api.md" line_range="37" />
<code_context>
+
+🔗 github.com/ktfth/kambo
+
+#APISecuriy #OWASP #BugBounty #CyberSecurity #BOLA #BFLA #PenetrationTesting #WebSecurity #EthicalHacking
+```
</code_context>
<issue_to_address>
**issue (typo):** Corrigir a hashtag para "#APISecurity".
Isso evita o erro de digitação atual (#APISecuriy) e ajuda na descoberta por buscas relacionadas a API Security.
```suggestion
#APISecurity #OWASP #BugBounty #CyberSecurity #BOLA #BFLA #PenetrationTesting #WebSecurity #EthicalHacking
```
</issue_to_address>
### Comment 10
<location path="social-media/posts/twitter/dia-16-scope.md" line_range="47" />
<code_context>
+```
+
+```
+Bonus: o log de auditoria em SQLite registra:
+• Cada ferramenta executada
+• O target testado
</code_context>
<issue_to_address>
**suggestion (typo):** Adicionar acento em "Bônus" para corrigir a ortografia em português.
Para manter a consistência com o restante do texto, sugiro usar "Bônus: o log de auditoria em SQLite registra:" em vez de "Bonus".
```suggestion
Bônus: o log de auditoria em SQLite registra:
```
</issue_to_address>
### Comment 11
<location path="social-media/threads_client.py" line_range="188" />
<code_context>
+ print("✏️ Abrindo formulário de novo post...")
+
+ # Tenta encontrar o botão de composição (vários seletores possíveis)
+ compose_selectors = [
+ 'a[href="/compose"]',
+ '[aria-label="New thread"]',
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared helpers for selector lookup, text truncation, and container polling to remove repetition and make the Threads publishing flow easier to follow.
You can reduce quite a bit of cognitive load here with small helpers, without changing behavior.
### 1. Deduplicate selector probing logic
The loops for `compose_selectors`, `text_area_selectors` e `publish_selectors` are structurally identical. A small helper keeps all retry behavior but removes repetition:
```python
# helper (near other browser helpers)
async def first_visible(page, selectors, timeout_per_selector=3000):
from playwright.async_api import TimeoutError as PWTimeout
for selector in selectors:
try:
el = await page.wait_for_selector(
selector, timeout=timeout_per_selector, state="visible"
)
if el:
return el, selector
except PWTimeout:
continue
return None, None
```
Then inside `post_via_browser`:
```python
# compose button
compose_btn, used = await first_visible(page, compose_selectors)
if compose_btn:
print(f" Encontrado: {used}")
await compose_btn.click()
await page.wait_for_timeout(1500)
else:
print(" Tentando via URL /compose...")
await page.goto(f"{THREADS_URL}/compose", wait_until="networkidle")
await page.wait_for_timeout(2000)
# text area
text_area, used = await first_visible(page, text_area_selectors, timeout_per_selector=5000)
if not text_area:
screenshot_path = Path(__file__).parent / "debug-screenshot.png"
await page.screenshot(path=str(screenshot_path))
raise RuntimeError(
f"❌ Campo de texto não encontrado.\n"
f" Screenshot salvo em: {screenshot_path}\n"
f" A interface do Threads pode ter mudado."
)
print(f" Campo encontrado: {used}")
# publish button
publish_btn, used = await first_visible(page, publish_selectors)
if not publish_btn or not await publish_btn.is_enabled():
screenshot_path = Path(__file__).parent / "debug-screenshot-publish.png"
await page.screenshot(path=str(screenshot_path))
raise RuntimeError(
f"❌ Botão 'Publicar' não encontrado ou desabilitado.\n"
f" Screenshot salvo em: {screenshot_path}"
)
print(f" Botão encontrado: {used}")
await publish_btn.click()
```
This keeps the same semantics but flattens three similar code blocks into a single, tested pattern.
---
### 2. Centralize truncation logic
The max-length truncation is duplicated between API and browser modes. A tiny helper makes it consistent and easier to change later:
```python
def truncate_for_threads(text: str) -> tuple[str, bool]:
if len(text) <= MAX_CHARS:
return text, False
return text[: MAX_CHARS - 3] + "...", True
```
Usage in `ThreadsAPIClient.post`:
```python
async def post(self, text: str) -> dict:
text, was_truncated = truncate_for_threads(text)
if was_truncated:
print(f"⚠️ Texto tem {len(text)} chars (limite: {MAX_CHARS}). Truncando...")
async with httpx.AsyncClient(timeout=30) as client:
...
```
And in `post_via_browser`:
```python
async def post_via_browser(...):
text, was_truncated = truncate_for_threads(text)
if was_truncated:
print(f"⚠️ Texto tem {len(text)} chars (limite: {MAX_CHARS}). Truncando...")
...
```
This removes duplication and guarantees both modes stay in sync on any future change to truncation rules.
---
### 3. Extract container polling from API client
The polling loop inside `ThreadsAPIClient.post` mixes control flow, HTTP calls, and error handling. Extracting it makes `post()` read as a simple 3-step flow:
```python
async def _wait_for_container_ready(self, client: httpx.AsyncClient, container_id: str) -> None:
print("⏳ Aguardando processamento...")
for attempt in range(10):
await asyncio.sleep(3)
status_resp = await client.get(
f"{THREADS_API_BASE}/{container_id}",
params={
"fields": "status,error_message",
"access_token": self.access_token,
},
)
status_resp.raise_for_status()
data = status_resp.json()
status = data.get("status", "")
if status == "FINISHED":
return
if status == "ERROR":
error_msg = data.get("error_message", "Erro desconhecido")
raise RuntimeError(f"Container com erro: {error_msg}")
print(f" Status: {status} (tentativa {attempt + 1}/10)")
raise TimeoutError("Container não ficou pronto em 30 segundos.")
```
Then `post()` becomes:
```python
async def post(self, text: str) -> dict:
text, was_truncated = truncate_for_threads(text)
if was_truncated:
print(f"⚠️ Texto tem {len(text)} chars (limite: {MAX_CHARS}). Truncando...")
async with httpx.AsyncClient(timeout=30) as client:
print("📤 Criando container no Threads...")
resp = await client.post(
f"{THREADS_API_BASE}/{self.user_id}/threads",
params={
"media_type": "TEXT",
"text": text,
"access_token": self.access_token,
},
)
resp.raise_for_status()
container_id = resp.json()["id"]
print(f"✅ Container criado: {container_id}")
await self._wait_for_container_ready(client, container_id)
print("🚀 Publicando...")
pub_resp = await client.post(
f"{THREADS_API_BASE}/{self.user_id}/threads_publish",
params={"creation_id": container_id, "access_token": self.access_token},
)
pub_resp.raise_for_status()
post_id = pub_resp.json()["id"]
return {
"success": True,
"post_id": post_id,
"url": f"https://www.threads.net/post/{post_id}",
"mode": "api",
}
```
This preserves all behavior and logs, but the main method is much easier to scan and reason about.
</issue_to_address>
### Comment 12
<location path="social-media/poster.py" line_range="320" />
<code_context>
result = subprocess.run(cmd, capture_output=True, text=True)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Architectural: - Centralize 30-day post mapping in posts-map.json (single source of truth) poster.py, schedule.sh and SKILL.md all load from this file — no more drift - schedule.sh: remove GNU date -d fallback (not portable on macOS BSD date); require python3 and hard-fail with a clear message if absent poster.py: - extract_post_text: fix regex to accept language-tagged fences (e.g. ```txt) and CRLF line-endings: r'```[^\r\n]*\r?\n(.*?)\r?\n```' - schedule_unix_uninstall: match only '# KamboSocialPoster' marker and skip the next line (the cron entry); no longer touches unrelated poster.py jobs - schedule_windows_install: add comment noting /TR is built from sys.executable and Path(__file__) — trusted system paths, not user-controlled input threads_client.py: - Extract truncate_for_threads(text) -> (str, bool): single truncation path shared by API and browser modes - Extract first_visible(page, selectors, timeout_ms) helper: eliminates three identical selector-probing loops (compose / textarea / publish button) - Extract ThreadsAPIClient._wait_for_container_ready(): separates polling from the main post() flow - Debug screenshots: use _debug_screenshot_path(label) which appends a full timestamp (%Y%m%d-%H%M%S-%f) so failure snapshots are never overwritten setup-windows.ps1: - Patch CHROME_PROFILE_DIR in .env line-by-line (not regex on whole file); append entry if key is absent; use Set-Content default (preserves newline) Typo fixes in posts: - dia-18-selfimprove.md: 'histórico objetividade' -> 'histórico objetivo' - dia-11-evidence.md: 'model' -> 'modelo' - dia-02-problema.md: 'gradu' -> 'gradua' - dia-12-api.md: '#APISecuriy' -> '#APISecurity' - dia-16-scope.md: 'Bonus:' -> 'Bônus:' https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
Addresses opengrep python.lang.security.audit.dangerous-subprocess-use-audit. schedule_windows_install: replace hand-built f-string /TR value with subprocess.list2cmdline([python, script, 'today']) which applies correct Windows cmd quoting for paths containing spaces or special characters. schedule_unix_install: wrap each path component (BASE_DIR, python, script, LOG_FILE) with shlex.quote() so the cron entry is safe for POSIX shells regardless of install directory names. Also adds 'import shlex' to the module imports. https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
…install Both `python` (sys.executable) and `script` (Path(__file__).resolve()) are resolved from the Python runtime itself — not from user-controlled input. list2cmdline is used to correctly quote paths with spaces for the Windows Task Scheduler /TR argument. Adding nosec B603/B607 annotations to silence the static analyzer false positives. https://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a complete cross-platform social media automation kit for publishing daily Kambo posts to Threads. Includes Python-based poster with dual publishing modes (browser automation and official API), scheduling support for Windows/Linux/macOS, and 30 days of pre-written content across Twitter, LinkedIn, and Instagram.
Key Changes
poster.py— Main automation script with:today,day N,list,setup,schedule install/uninstallthreads_client.py— Threads publishing client with:ThreadsAPIClientfor official Threads Graph API (text posts, container management, polling)post_via_browser()for Chrome automation via Playwright (no API key required, uses existing session)schedule.sh— Bash scheduler for Linux/macOS with:setup-windows.ps1— Windows setup automation:.envconfiguration30 days of pre-written content (
posts/directory):Configuration & Documentation:
.env.examplewith all configurable options (POSTER_MODE, CHROME_PROFILE_DIR, THREADS credentials, KAMBO_START_DATE)README.mdwith setup instructions for all platforms, mode explanations, and usage examplescalendar-30days.mdwith content calendar and themeshashtags.mdwith platform-specific hashtag bankswindows-task.xmlfor manual Task Scheduler importrequirements-social.txtwith Python dependenciesClaude Code Integration (
.claude/skills/kambo-social/SKILL.md):/kambo-socialtriggerProject Integration:
.gitignoreto exclude.start-date,.env,post-log.jsonl, and debug screenshotsCLAUDE.mdto reference the new social media skillNotable Implementation Details
.start-datefile orKAMBO_START_DATEenv var to track cycle start; calculates current day as(today - start_date).days % 30 + 1THREADS_USER_IDandTHREADS_ACCESS_TOKENhttps://claude.ai/code/session_01NLNppLCEKST4ZgS7ZbmLNA
Summary by Sourcery
Add a cross-platform social media automation kit for posting daily Kambo content to Threads, including scheduling, posting clients, and pre-written multi-platform content.
New Features:
/kambo-social) to generate and publish the daily Kambo post through browser automation.Enhancements:
Documentation:
Chores: