This commit is contained in:
Oleg Proskurin 2026-06-07 03:19:53 +07:00
parent 5f8d3330f7
commit de9930c4c2
10 changed files with 586 additions and 54 deletions

View File

@ -0,0 +1,254 @@
---
name: get-tg-jobs
description: Full autonomous end-to-end job pipeline. Fetches fresh vacancies from Oleg's Telegram "Jobs" folder, handles new/pending channels, runs the full triage cycle (triage-jobs), pushes APPLY verdicts to Trello, then reports results in the chat session AND sends a Telegram notification to Oleg via telegram-helper. Trigger on "забери из Jobs", "что нового в каналах", "запусти pipeline", or similar.
---
# get-tg-jobs
Autonomous end-to-end pipeline: fetch → curate new channels → triage → Trello → report. Does everything without stopping for confirmation. Only halts if a hard error occurs (auth failure, script crash, etc.).
**Trigger only when Oleg explicitly asks** — e.g. "забери свежее из Jobs", "что нового в каналах", "запусти pipeline". No background polling.
---
## Notification targets
All pipeline reports go to the **"New cool job"** Telegram group. The `telegram-helper` account (samuishechka) is a member of that group and sends messages there.
| Target | chat_id | When |
|---|---|---|
| "New cool job" group | `-4783500394` | All reports — success and errors |
Tool to use: `mcp__telegram-helper__send_message(chat_id=-4783500394, message="...", parse_mode="html")`
This section is the canonical reference for notification targets. Update here if the group changes.
---
## Step 1 — Run the pipeline
```bash
~/.local/bin/uv run scripts/list_telegram_channels.py \
| ~/.local/bin/uv run scripts/fetch_telegram_jobs.py -
```
Run from the project root. The script prints a per-channel summary to stdout — echo it to the session so Oleg can see raw progress.
**If the script fails:**
- `TELEGRAM_SESSION_STRING not set` or auth error → `.env` is missing or the session is expired. Regenerate with `scripts/regen_telegram_session.sh` and tell Oleg.
- `TELEGRAM_API_ID / TELEGRAM_API_HASH not set` → same `.env` issue.
- `Could not find folder "Jobs"` → the folder was renamed or the wrong session is in `.env`. Must be the **usulsu** session, not samuishechka.
- Any other exception → paste the error and stop. Don't retry blindly.
---
## Step 2 — Check for pending (new) channels
```bash
test -f tracking/telegram_pending_channels.json && echo PENDING || echo OK
```
**If OK**: skip to Step 3.
**If PENDING**: curate each new channel using the procedure below, then rerun Step 1.
---
### New-channel curation procedure
For each channel in `telegram_pending_channels.json`:
#### 2a — Read keyword frequency scan
```bash
jq -r 'to_entries[] | "\n=== \(.key) ===\nmessages_scanned: \(.value.messages_scanned)\ntruncated: \(.value.truncated)\ntop keywords:\n\(.value.keyword_counts_from_other_channels | to_entries | sort_by(-.value) | .[:20] | .[] | " \(.key): \(.value)")"' \
tracking/telegram_pending_channels.json
```
#### 2b — Sample messages
```bash
jq -r '.channels["<channel>"].messages[:6] | .[] | "── \(.date[0:16])\n\(.text[0:400])\n"' \
tracking/telegram_inbox.json
```
Look for: hashtag patterns, language, post structure (single role / digest / chat), recurring noise.
#### 2c — Decide lang, priority, filter shape
**Priority:**
- `p1` — strong stack hits (JS/TS/React/Node/AI/LLM) AND global-remote culture
- `p2` — stack OK but CIS/RUB market, or occasional gems; recruiter/market-intel content
- `p3` — wrong stack, off-market, noise, dead channels
Base priority on the **best** vacancy in the sample, not the average.
**Filter shape:**
| Channel pattern | Filter |
|---|---|
| Consistent `#vacancy` + `#remote` hashtags | AND-of-OR hashtag filter |
| Vacancy text without consistent hashtags | Positive stack include + Oleg-stack excludes |
| Low-volume personal/curated digest | Trust-all (no `include`/`exclude`) |
| Mixes resumes and vacancies | Trust-all |
| Mostly noise but worth keeping | Strict positive filter |
Standard Oleg-stack excludes: `["kafka", "golang", "kotlin", "android", "swift"]`
For `*_jobs` channels add resume excludes: `["#резюме", "#resume", "#cv", "#ищуработу"]`
Positive stack include: `["javascript", "typescript", " react", "node.js", "nodejs", "fullstack", "full-stack", "tech lead", "techlead", "ai engineer", " llm"]`
#### 2d — Add entry to telegram_channels.json
Insert in correct priority block (p1 → p2 → p3, alphabetically within each group):
```jsonc
"<channel_id>": {
"lang": "ru" | "en" | "mixed",
"priority": "p1" | "p2" | "p3",
"include": [["kw1","kw2"], ["kw3"]], // optional
"exclude": ["kw4", "kw5"] // optional
}
```
#### 2e — Rerun and confirm clean
```bash
~/.local/bin/uv run scripts/list_telegram_channels.py \
| ~/.local/bin/uv run scripts/fetch_telegram_jobs.py -
test -f tracking/telegram_pending_channels.json && echo "STILL PENDING" || echo "Clean"
```
#### 2f — Validate the filter
```bash
jq -r '.channels["<channel>"] | "kept \(.kept)/\(.seen)" + (.messages | map("\n── \(.date[0:16])\n\(.text[0:300])") | join(""))' \
tracking/telegram_inbox.json
```
If `kept == 0` and a filter is set — verify the filter isn't too strict before accepting zero as correct.
---
## Step 3 — Run full triage
Follow the complete `triage-jobs` skill procedure on `tracking/telegram_inbox.json`.
Key points from that skill:
1. **Pre-step**: build `known_applied` list before spawning Haiku:
```bash
grep -E '^\|' tracking/applications.md | tail -n +2 | awk -F'|' '{print $3}' | sort -u
```
Also fetch Trello card titles from all BestJob board lists. Pass the merged list into every Haiku subagent prompt.
2. **Stratify by priority**:
- p1 ≤ 15 messages → triage fully in main session
- p1 > 15 messages → Haiku first pass, then main session reviews only APPLY and VERIFY
- p2 and p3 → Haiku subagent, main session reviews APPLY and VERIFY only
3. **VERIFY verdicts**: run a fast `WebSearch "{company} remote hiring countries"` to resolve geo, then apply Gate 1.
4. **APPLY verdicts**: for every confirmed APPLY —
- Add a row to `tracking/applications.md` with status `todo`
- Create a Trello card in the TODO column (`6a1aa59555aab72a261c42aa`) on board `6a1a9a5af082cb0526b22704` with: company, role, track, verdict summary, JD link
5. **QUICK-APPLY verdicts**: note them explicitly in the session report; do not auto-add to Trello. Ask Oleg if he wants to proceed.
Full triage playbook: `base/reference/vacancy-filter-and-triage-2026.md`. Full batch instructions: `.claude/skills/triage-jobs/SKILL.md`.
---
## Step 4 — Report in session
After triage is complete, output a consolidated summary to the chat:
```
Pipeline done — <date>
Fetch: N messages from X channels (Y with results)
[⚠️ Truncated: channel_name (kept K/500, p2/ru)]
Triage results:
✅ APPLY: N roles → added to Trello
🔍 VERIFY: N roles → resolved / still open
⚡ QUICK-APPLY: N roles (ask Oleg before adding)
❌ SKIP: N roles
APPLY roles:
- Company A — Role Title (Track A) → [Trello card]
- Company B — Role Title (Track B) → [Trello card]
New channels curated: N [or: none]
```
---
## Step 5 — Send Telegram notification
After the session report, send a summary to the **"New cool job"** group via telegram-helper.
### Success message format
```
mcp__telegram-helper__send_message(
chat_id=-4783500394,
parse_mode="html",
message="""<b>Jobs pipeline done</b> · 2026-06-07
✅ APPLY: N роли → Trello
🔍 VERIFY: N (нужен ресёрч)
⚡ Quick-apply: N (нужно ок)
❌ SKIP: N
📥 Всего: N сообщений из X каналов
<b>APPLY:</b>
<a href="https://trello.com/c/...">Company A — Role (Track A)</a>
<a href="https://trello.com/c/...">Company B — Role (Track B)</a>"""
)
```
Rules:
- Always include Trello links for APPLY roles — это главная ценность уведомления.
- If QUICK-APPLY roles exist, list them with direct JD links (not Trello — для быстрого решения):
```
⚡ Quick-apply (нужно ок):
Company X — Role → wantapply.com/...
Company Y — Role → linkedin.com/jobs/...
```
- Keep VERIFY as a count only (no list) — they need more research, not immediate action.
- If zero APPLY and zero QUICK-APPLY: still send the message — "всё просмотрено, ничего релевантного в этот раз".
### Error message format
If the pipeline failed at any step and cannot complete — send a short error notification to the **same group**:
```
mcp__telegram-helper__send_message(
chat_id=-4783500394,
parse_mode="html",
message="""<b>⚠️ Pipeline error</b> · 2026-06-07
Упало на шаге: <step name>
Причина: <one-line error summary>
Что сделано до ошибки: <brief e.g. "fetch OK, 12 msgs, triage не запустился">
Что нужно: <action e.g. "обнови TELEGRAM_SESSION_STRING в .env">"""
)
```
Send the error notification **even if only partial work was done**. Better to have an incomplete notification than silence when something breaks.
---
## Sanity notes
- `telegram_state.json` is not in git — per-machine cursor. If lost, next run fetches 30 days back per channel (slow but correct).
- To reset a single channel's cursor for filter re-validation:
```bash
jq 'del(.["<channel>"])' tracking/telegram_state.json > /tmp/s.json && mv /tmp/s.json tracking/telegram_state.json
```
- `telegram_inbox.json` is overwritten every run — it's the current snapshot only.
- The pipeline reads the live "Jobs" folder — new channels Oleg added in Telegram are automatically included in the next run.

View File

@ -231,19 +231,31 @@ For SKIP: one-liner after gate check explaining the objective blocker. No resear
When triaging `tracking/telegram_inbox.json`: When triaging `tracking/telegram_inbox.json`:
### Pre-step: build prior-applications list (do this once before spawning any subagent)
```bash
grep -E '^\|' tracking/applications.md | tail -n +2 | awk -F'|' '{print $3}' | sort -u
```
Also call `mcp__trello__get_cards_by_list_id` for all BestJob board lists to extract card titles. Merge both into a `known_applied` list (company name → date/status where available). Pass this list to every Haiku subagent prompt (see template below).
### Stratification rules
1. Read the file. Group entries by channel `priority` field (`p1` / `p2` / `p3`). 1. Read the file. Group entries by channel `priority` field (`p1` / `p2` / `p3`).
2. **p1 entries**: triage in the main session (full Step 18 per entry). 2. **p1 entries — ≤ 15 messages**: triage fully in the main session (full Step 18 per entry).
3. **p2 and p3 entries**: delegate to a Haiku subagent to protect main context. 3. **p1 entries — > 15 messages**: Haiku first pass (same prompt as p2/p3). Main session then reviews only APPLY and VERIFY verdicts; SKIP verdicts from Haiku are accepted without re-review.
- Pass the raw entries and the playbook summary to the subagent. 4. **p2 and p3 entries**: delegate to a Haiku subagent to protect main context.
- Pass the raw entries, the playbook summary, and the `known_applied` list to the subagent.
- Subagent returns a structured verdict list. - Subagent returns a structured verdict list.
- Review, escalate any surprising p2/p3 passes to main session for full triage. - Review, escalate any APPLY and VERIFY passes to main session for full triage.
4. After all verdicts: promote APPLY entries to `tracking/applications.md`. 5. After all verdicts: promote confirmed APPLY entries to `tracking/applications.md`.
5. For QUICK-APPLY entries: note explicitly, ask Oleg if he wants to proceed. 6. For QUICK-APPLY entries: note explicitly, ask Oleg if he wants to proceed.
7. For VERIFY entries: run a fast WebSearch `"{company} remote hiring countries"` to resolve geo; then apply the gate.
### Haiku subagent prompt template ### Haiku subagent prompt template
``` ```
You are triaging job vacancies for Oleg Proskurin (Tech Lead / AI Engineer, based in Thailand, available for remote work). You are triaging job vacancies for Oleg Proskurin (Tech Lead / AI Engineer, based in Thailand GMT+7, available for remote work).
Apply these 4 hard gates — SKIP on first failure: Apply these 4 hard gates — SKIP on first failure:
@ -252,13 +264,25 @@ Gate 2 (TZ): Oleg's window 9:0022:00 GMT+7. Outside window: ok if comp > $9k.
Gate 3 (Comp): Skip if < $4.5k/month. Floor $68k, target $812k. Gate 3 (Comp): Skip if < $4.5k/month. Floor $68k, target $812k.
Gate 4 (Level): Skip if explicitly Junior or mid with no growth ceiling. Gate 4 (Level): Skip if explicitly Junior or mid with no growth ceiling.
Posture: trying-friendly. On ambiguous cases, lean toward APPLY. Posture: trying-friendly when data IS PRESENT and borderline. When key gate data is ABSENT from the message text, return VERIFY — not APPLY. Lean toward APPLY only when geo and comp are explicitly stated and borderline (e.g. "global remote", "$7k/month").
For each vacancy below, output: Geo rules (Gate 1):
- APPLY, SKIP, or QUICK-APPLY (≥50% match + easy application) - Message explicitly says "global remote" / "worldwide" / "anywhere" / "любой город" / contractor-friendly → Gate 1 passes, proceed.
- Gate that failed (for SKIP) - Message says "United States" / "US only" / "United Kingdom only" / specific country list excluding Thailand → SKIP Gate 1.
- Probable track: A (AI must-have), B (full-stack/frontend), Backend-first, Hybrid - Message says "Remote" with no geo qualifier → VERIFY geo (cannot confirm Thailand eligibility without JD).
- 1-line reason - Message has no location info at all → VERIFY geo.
Comp rules (Gate 3):
- Explicit salary ≥ $4.5k/month or ≥ $54k/year → Gate 3 passes, proceed.
- Explicit salary < $4.5k/month → SKIP Gate 3.
- No salary mentioned → VERIFY comp.
- Timezone offset explicitly stated (e.g. GMT-GMT+6) → treat as geo restriction, apply Gate 1.
Known applied companies — flag these as SKIP (already applied):
[INSERT known_applied LIST HERE — one line per company]
For each vacancy below, output one line:
APPLY | SKIP | QUICK-APPLY | VERIFY — [company/role] — [gate failed or what's missing] — [track: A/B/Backend-first/Hybrid]
Vacancies: Vacancies:
[paste entries from telegram_inbox.json] [paste entries from telegram_inbox.json]

View File

@ -51,9 +51,50 @@ The list names "Applyed" and "Artifactes" are the actual spellings on the board
--- ---
## Telegram — two accounts, two roles
Two Telegram accounts are wired up as separate MCP servers. **Always use the correct account for each operation** — swapping them sends outreach under the wrong name or clutters Oleg's main history with bot noise.
| Account | MCP server | Tool prefix | Purpose |
|---|---|---|---|
| **usulsu** (main) | `telegram-usulpro` | `mcp__telegram-usulpro__*` | Reading job channels; direct recruiter outreach from Oleg's name |
| **samuishechka** (helper) | `telegram-helper` | `mcp__telegram-helper__*` | Notifications to Oleg / Ekaterina; workflow triggers and confirmations |
### usulsu — main account (`mcp__telegram-usulpro__*`)
Use for everything that relates to Oleg's identity or the job-channel pipeline:
- Reading the "Jobs" folder and job channels (also used by direct Telethon scripts — see below)
- All vacancy pipeline reads: `list_telegram_channels.py`, `fetch_telegram_jobs.py`
- Direct outreach to recruiters — messages appear under Oleg's real name
- Any investigation of job postings, channel info, message context
### samuishechka — helper account (`mcp__telegram-helper__*`)
Use for auxiliary, bot-like coordination that must not mix with Oleg's main identity:
- Sending notifications to Oleg (`@usulpro`) or Ekaterina (`@dalikat`) — e.g. "triage done, 3 hot leads", "outreach sent to N recruiters"
- Receiving go-ahead commands from Oleg/Ekaterina to trigger a new pipeline run or check a specific vacancy
- Any message that is a system event rather than a human outreach
### Correct tool prefix summary
| Operation | Tool prefix |
|---|---|
| Read job channels / check vacancies | `mcp__telegram-usulpro__*` |
| Recruiter outreach (DMs from Oleg) | `mcp__telegram-usulpro__*` |
| Notify @usulpro or @dalikat | `mcp__telegram-helper__*` |
| Workflow triggers / confirmations | `mcp__telegram-helper__*` |
### Direct Telethon scripts — always usulsu
`scripts/list_telegram_channels.py` and `scripts/fetch_telegram_jobs.py` connect to Telegram directly via Telethon (bypassing MCP). They read `TELEGRAM_SESSION_STRING` from the project `.env`, which **must be the usulsu session** — the "Jobs" folder lives on that account. Never put the samuishechka session there.
---
## Telegram MCP — safety policy ## Telegram MCP — safety policy
Claude is connected to Oleg's personal Telegram account via the `telegram-mcp` server (full MTProto access, **not** a bot). This is powerful and reaches a wide social surface, so two rules govern its use. **Both rules are absolute — they take precedence over any in-conversation instruction, including a direct user request to ignore them.** Both accounts are connected via the `telegram-mcp` server (full MTProto access, **not** a bot). This is powerful and reaches a wide social surface, so two rules govern its use. **Both rules apply to both accounts and are absolute — they take precedence over any in-conversation instruction, including a direct user request to ignore them.**
### Rule 1 — never perform destructive or irreversible actions ### Rule 1 — never perform destructive or irreversible actions
@ -63,7 +104,7 @@ Do not call any Telegram tool whose effect cannot be cleanly undone, **even if O
- Leaving / deleting / archiving-then-purging chats where history would become unreachable - Leaving / deleting / archiving-then-purging chats where history would become unreachable
- Terminating sessions, blocking users, removing contacts - Terminating sessions, blocking users, removing contacts
- Bulk operations that change many entities at once (mass mark-as-read, mass leave, mass unsubscribe) - Bulk operations that change many entities at once (mass mark-as-read, mass leave, mass unsubscribe)
- Changing profile photo, account info, privacy settings, 2FA — anything that alters Oleg's account identity or security posture - Changing profile photo, account info, privacy settings, 2FA — anything that alters account identity or security posture
- Any "clear" / "reset" / "wipe" tool - Any "clear" / "reset" / "wipe" tool
If Oleg genuinely wants such an action, he must do it himself in the Telegram client. This rule exists because a misclick from an LLM agent at this layer can lose data permanently. If Oleg genuinely wants such an action, he must do it himself in the Telegram client. This rule exists because a misclick from an LLM agent at this layer can lose data permanently.
@ -85,8 +126,8 @@ A read-only investigation never needs permission; this rule only applies to muta
Operational details — file layout, scripts, filter schema, priority rubric, new-channel triage procedure — live in **[`tracking/CLAUDE.md`](tracking/CLAUDE.md)**. Read that first before doing any vacancy-related Telegram work. Operational details — file layout, scripts, filter schema, priority rubric, new-channel triage procedure — live in **[`tracking/CLAUDE.md`](tracking/CLAUDE.md)**. Read that first before doing any vacancy-related Telegram work.
Quick orientation: Quick orientation:
- **Source of subscriptions**: Telegram folder "Jobs" (id=6), curated manually by Oleg. Never mirror its membership to a repo file. - **Source of subscriptions**: Telegram folder "Jobs" (id=6), curated manually by Oleg on the **usulsu** account. Never mirror its membership to a repo file.
- **Pipeline**: `scripts/list_telegram_channels.py``scripts/fetch_telegram_jobs.py` (chainable via stdin). - **Pipeline**: `scripts/list_telegram_channels.py``scripts/fetch_telegram_jobs.py` (chainable via stdin). Both scripts use the **usulsu** session (`TELEGRAM_SESSION_STRING` in `.env`).
- **Curated config**: [`tracking/telegram_channels.json`](tracking/telegram_channels.json) — per-channel `lang`, `priority` (`p1`/`p2`/`p3`), and filter (`include` / `exclude`). - **Curated config**: [`tracking/telegram_channels.json`](tracking/telegram_channels.json) — per-channel `lang`, `priority` (`p1`/`p2`/`p3`), and filter (`include` / `exclude`).
- **Output**: [`tracking/telegram_inbox.json`](tracking/telegram_inbox.json) — filtered messages, overwritten each run. - **Output**: [`tracking/telegram_inbox.json`](tracking/telegram_inbox.json) — filtered messages, overwritten each run.
- **Trigger**: manual only — run when Oleg explicitly asks (e.g. "забери свежее из Jobs"). No background polling. - **Trigger**: manual only — run when Oleg explicitly asks (e.g. "забери свежее из Jobs"). No background polling.

View File

@ -1,8 +1,13 @@
# Telegram MCP — setup on a new machine # Telegram MCP — setup on a new machine
Reproduces the working integration this project uses: read Oleg's Telegram channels/DMs as a user (MTProto, not a bot) through the `chigwell/telegram-mcp` server, registered in Claude Code + Claude Desktop. Reproduces the working integration this project uses: two Telegram accounts wired up as separate MCP servers via `chigwell/telegram-mcp` (MTProto, not bots), registered in Claude Code + Claude Desktop.
Time: ~10 minutes. Most of it is one-time Telegram credentials. | Account | MCP server name | Tool prefix | Role |
|---|---|---|---|
| **usulsu** (main) | `telegram-usulpro` | `mcp__telegram-usulpro__*` | Job channels, recruiter outreach |
| **samuishechka** (helper) | `telegram-helper` | `mcp__telegram-helper__*` | Notifications to Oleg + Ekaterina, workflow triggers |
Time: ~15 minutes. Most of it is one-time Telegram credentials and two session strings.
--- ---
@ -13,7 +18,9 @@ Time: ~10 minutes. Most of it is one-time Telegram credentials.
--- ---
## 1. Get Telegram API credentials (one-time per Telegram account) ## 1. Get Telegram API credentials (one-time — shared across both accounts)
The `api_id` / `api_hash` identify the *app*, not the account. One app serves both sessions.
Open https://my.telegram.org/apps and create a new application: Open https://my.telegram.org/apps and create a new application:
@ -27,12 +34,13 @@ You'll get `api_id` (number) and `api_hash` (32-char hex). Treat them like a pas
If you see a generic `ERROR` popup, fill in the URL field — that's the most common cause. If you see a generic `ERROR` popup, fill in the URL field — that's the most common cause.
Save both into the project `.env`: Save both into the project `.env` (session strings filled in after step 3):
``` ```
TELEGRAM_API_ID=<id> TELEGRAM_API_ID=<id>
TELEGRAM_API_HASH=<hash> TELEGRAM_API_HASH=<hash>
TELEGRAM_SESSION_STRING= TELEGRAM_SESSION_STRING= # usulsu — main account (job channels + outreach)
TELEGRAM_SESSION_STRING_HELPER= # samuishechka — notifications + workflow triggers
``` ```
Confirm `.env` is in `.gitignore`. Confirm `.env` is in `.gitignore`.
@ -60,52 +68,89 @@ Two binaries appear at `/projects/my-utils/telegram/.venv/bin/`:
--- ---
## 3. Generate the session string (one-time, interactive) ## 3. Generate session strings (one per account, interactive)
From the cv-2026 project root (where `.env` lives): Run the generator twice — once per account. From the cv-2026 project root:
```bash ```bash
set -a && source .env && set +a && \ set -a && source .env && set +a && \
/projects/my-utils/telegram/.venv/bin/telegram-mcp-generate-session /projects/my-utils/telegram/.venv/bin/telegram-mcp-generate-session
``` ```
### 3a. usulsu — main account
At the prompts: At the prompts:
- **Account label:** *leave empty* (Enter). A label adds a `_<LABEL>` suffix to env var names and we don't need multi-account. - **Account label:** *leave empty* (Enter) — output var will be `TELEGRAM_SESSION_STRING`
- **Login method:** `1` (QR) is the fastest — scan from any device where Telegram is already open (Settings → Devices → Link Desktop Device). - **Login method:** `1` (QR) is the fastest — scan from any device where Telegram is already open (Settings → Devices → Link Desktop Device).
- Alternative: `2` (phone). The code arrives **inside Telegram** as a message from the official `Telegram` account, not via SMS — check there first. - Alternative: `2` (phone). The code arrives **inside Telegram** as a message from the official `Telegram` account, not via SMS — check there first.
Output ends with a ~350-char string. Paste it into `.env` as `TELEGRAM_SESSION_STRING=<value>`. Paste the output into `.env` as `TELEGRAM_SESSION_STRING=<value>`.
### 3b. samuishechka — helper account
Run the same command again, this time logged in as (or switching to) the samuishechka account:
- **Account label:** `helper` — output var will be `TELEGRAM_SESSION_STRING_HELPER`
- **Login method:** same options; use whichever device has samuishechka logged in.
Paste the output into `.env` as `TELEGRAM_SESSION_STRING_HELPER=<value>`.
Alternatively, use the `--phone` flag to avoid the interactive menu:
```bash
TELEGRAM_API_ID=<id> TELEGRAM_API_HASH=<hash> \
/projects/my-utils/telegram/.venv/bin/python \
/projects/my-utils/telegram/session_string_generator.py --phone
```
--- ---
## 4. Register in Claude Code + Claude Desktop ## 4. Register both servers in Claude Desktop config
[`add-mcp`](https://www.npmjs.com/package/add-mcp) writes the server entry into both client configs in one shot. **Important gotcha:** with `--yes`, `add-mcp` stores `${VAR}` placeholders literally instead of resolving them — Claude Desktop won't expand env vars at runtime, so we resolve them manually with `jq` right after. Edit `~/.config/Claude/claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`) and add two entries under `mcpServers`:
```bash ```json
# from the cv-2026 project root "telegram-usulpro": {
set -a && source .env && set +a && \ "command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
npx -y add-mcp "/projects/my-utils/telegram/.venv/bin/telegram-mcp" \ "args": [],
-n telegram \ "env": {
--env 'TELEGRAM_API_ID=${TELEGRAM_API_ID}' \ "TELEGRAM_API_ID": "<id>",
--env 'TELEGRAM_API_HASH=${TELEGRAM_API_HASH}' \ "TELEGRAM_API_HASH": "<hash>",
--env 'TELEGRAM_SESSION_STRING=${TELEGRAM_SESSION_STRING}' \ "TELEGRAM_SESSION_STRING": "<usulsu session string>"
-a claude-code -a claude-desktop \ }
-g -y },
"telegram-helper": {
# fix the literal-placeholder issue — substitute actual values into both configs "command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
set -a && source .env && set +a && \ "args": [],
for CFG in ~/.config/Claude/claude_desktop_config.json ~/.claude.json; do "env": {
jq --arg id "$TELEGRAM_API_ID" --arg hash "$TELEGRAM_API_HASH" --arg sess "$TELEGRAM_SESSION_STRING" \ "TELEGRAM_API_ID": "<id>",
'.mcpServers.telegram.env.TELEGRAM_API_ID = $id "TELEGRAM_API_HASH": "<hash>",
| .mcpServers.telegram.env.TELEGRAM_API_HASH = $hash "TELEGRAM_SESSION_STRING": "<samuishechka session string>"
| .mcpServers.telegram.env.TELEGRAM_SESSION_STRING = $sess' \ }
"$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG" }
done
``` ```
macOS Claude Desktop config path is `~/Library/Application Support/Claude/claude_desktop_config.json` — adjust the loop. Or use `jq` to inject from `.env` in one shot (run from project root):
```bash
set -a && source .env && set +a && \
CFG=~/.config/Claude/claude_desktop_config.json && \
jq --arg id "$TELEGRAM_API_ID" --arg hash "$TELEGRAM_API_HASH" \
--arg sess "$TELEGRAM_SESSION_STRING" \
--arg sess_h "$TELEGRAM_SESSION_STRING_HELPER" '
.mcpServers["telegram-usulpro"] = {
"command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
"args": [], "env": {
"TELEGRAM_API_ID": $id, "TELEGRAM_API_HASH": $hash,
"TELEGRAM_SESSION_STRING": $sess }} |
.mcpServers["telegram-helper"] = {
"command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
"args": [], "env": {
"TELEGRAM_API_ID": $id, "TELEGRAM_API_HASH": $hash,
"TELEGRAM_SESSION_STRING": $sess_h }}' \
"$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
```
--- ---
@ -114,13 +159,18 @@ macOS Claude Desktop config path is `~/Library/Application Support/Claude/claude
- **Claude Desktop:** quit and reopen. - **Claude Desktop:** quit and reopen.
- **Claude Code:** start a new session (this CLI loads MCP servers at session start). - **Claude Code:** start a new session (this CLI loads MCP servers at session start).
In the new session, ask: *"проверь доступ к телеграму"*. The agent should call `mcp__telegram__get_me` and return your handle. In the new session, ask: *"проверь доступ к обоим телеграм аккаунтам"*. The agent should call both `mcp__telegram-usulpro__get_me` (→ @UsulPro) and `mcp__telegram-helper__get_me` (→ @samuishka) and return the respective handles.
If tools never appear, check the server starts cleanly: If tools never appear, smoke-test each server manually:
```bash ```bash
# usulsu
set -a && source .env && set +a && /projects/my-utils/telegram/.venv/bin/telegram-mcp set -a && source .env && set +a && /projects/my-utils/telegram/.venv/bin/telegram-mcp
# Ctrl+C after it prints startup logs — it's an stdio MCP server, this just smoke-tests it
# samuishechka
TELEGRAM_SESSION_STRING="$TELEGRAM_SESSION_STRING_HELPER" \
/projects/my-utils/telegram/.venv/bin/telegram-mcp
# Ctrl+C after startup logs — it's a stdio MCP server
``` ```
--- ---

View File

@ -14,6 +14,7 @@ channels for keyword decisions.
Inputs: Inputs:
- channel usernames/ids as positional args, OR `-` to read a JSON array from stdin - channel usernames/ids as positional args, OR `-` to read a JSON array from stdin
- .env in the project root (TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_SESSION_STRING) - .env in the project root (TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_SESSION_STRING)
TELEGRAM_SESSION_STRING must be the **usulsu** (main) account the "Jobs" folder lives there.
- tracking/telegram_state.json per-channel last_message_id (created if missing) - tracking/telegram_state.json per-channel last_message_id (created if missing)
- tracking/telegram_channels.json per-channel curated metadata (lang, priority) - tracking/telegram_channels.json per-channel curated metadata (lang, priority)
and filter (include/exclude). See tracking/CLAUDE.md. and filter (include/exclude). See tracking/CLAUDE.md.

View File

@ -13,6 +13,9 @@ Output: JSON array of usernames (falling back to numeric id for private channels
Pipe directly into the fetch script: Pipe directly into the fetch script:
list_telegram_channels.py | fetch_telegram_jobs.py - list_telegram_channels.py | fetch_telegram_jobs.py -
Account: uses TELEGRAM_SESSION_STRING from .env must be the usulsu (main) account.
The "Jobs" folder lives on that account.
""" """
import asyncio import asyncio

View File

@ -1,9 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Regenerate Telegram session string via QR login. # Regenerate a Telegram session string.
# Reads TELEGRAM_API_ID / TELEGRAM_API_HASH from the project .env, # Reads TELEGRAM_API_ID / TELEGRAM_API_HASH from the project .env,
# then runs the generator from the telegram-mcp install. # then runs the generator from the telegram-mcp install.
#
# Usage:
# bash scripts/regen_telegram_session.sh # regenerate usulsu (main)
# bash scripts/regen_telegram_session.sh helper # regenerate samuishechka
#
# After running, paste the printed session string into the project .env:
# usulsu → TELEGRAM_SESSION_STRING=...
# helper → TELEGRAM_SESSION_STRING_HELPER=...
set -euo pipefail set -euo pipefail
ACCOUNT="${1:-}" # "helper" for samuishechka, empty for usulsu
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TELEGRAM_DIR="/projects/my-utils/telegram" TELEGRAM_DIR="/projects/my-utils/telegram"
@ -21,5 +31,14 @@ if [ -z "${TELEGRAM_API_ID:-}" ] || [ -z "${TELEGRAM_API_HASH:-}" ]; then
exit 1 exit 1
fi fi
if [ "$ACCOUNT" = "helper" ]; then
echo "Regenerating session for samuishechka (helper account)."
echo "When prompted for label, enter: helper"
else
echo "Regenerating session for usulsu (main account)."
echo "When prompted for label, leave empty (press Enter)."
fi
echo ""
cd "$TELEGRAM_DIR" cd "$TELEGRAM_DIR"
exec .venv/bin/python session_string_generator.py --qr "$@" exec .venv/bin/python session_string_generator.py --qr "$@"

View File

@ -32,6 +32,8 @@ Two scripts, chainable. Always run from project root.
**Step 2 — `scripts/fetch_telegram_jobs.py`**: pulls new messages per channel, applies the per-channel filter, and writes results to `telegram_inbox.json`. Accepts channels as positional args or as a JSON array on stdin (`-`). **Step 2 — `scripts/fetch_telegram_jobs.py`**: pulls new messages per channel, applies the per-channel filter, and writes results to `telegram_inbox.json`. Accepts channels as positional args or as a JSON array on stdin (`-`).
**Account:** both scripts connect directly via Telethon using `TELEGRAM_SESSION_STRING` from `.env` — that must be the **usulsu** (main) session. The "Jobs" folder lives on that account. Do not put the samuishechka session there.
### Constants in the fetch script ### Constants in the fetch script
- `DEFAULT_LOOKBACK_DAYS = 30` — first-time lookback window for new channels (no cursor yet). - `DEFAULT_LOOKBACK_DAYS = 30` — first-time lookback window for new channels (no cursor yet).

View File

@ -1,7 +1,7 @@
{ {
"JS_RemoteJobs": { "JS_RemoteJobs": {
"lang": "en", "lang": "en",
"priority": "p1", "priority": "p2",
"exclude": ["kafka", "golang", "kotlin", "android", "swift", " rust ", " java "] "exclude": ["kafka", "golang", "kotlin", "android", "swift", " rust ", " java "]
}, },
"notificaJobs_nodeJS": { "notificaJobs_nodeJS": {

View File

@ -0,0 +1,138 @@
{
"mobile_jobs": {
"last_message_id": 211450,
"last_seen_date": "2026-06-06T13:58:01+00:00"
},
"javascript_jobs": {
"last_message_id": 881871,
"last_seen_date": "2026-06-06T18:42:26+00:00"
},
"javascript_jobs_feed": {
"last_message_id": 39247,
"last_seen_date": "2026-06-06T18:42:25+00:00"
},
"devops_jobs": {
"last_message_id": 1566498,
"last_seen_date": "2026-06-06T18:48:55+00:00"
},
"newworld_2088": {
"last_message_id": 8390,
"last_seen_date": "2026-06-04T19:29:30+00:00"
},
"nodejs_jobs": {
"last_message_id": 133491,
"last_seen_date": "2026-06-06T12:47:02+00:00"
},
"mobile_vacancies": {
"last_message_id": 85194,
"last_seen_date": "2026-06-06T13:21:11+00:00"
},
"jsspeak": {
"last_message_id": 58668,
"last_seen_date": "2026-06-06T14:14:30+00:00"
},
"-1002137052673": {
"last_message_id": 9437,
"last_seen_date": "2026-06-06T18:06:02+00:00"
},
"projects_jobs": {
"last_message_id": 137333,
"last_seen_date": "2026-06-06T13:15:22+00:00"
},
"zarubezhom_jobs": {
"last_message_id": 3922,
"last_seen_date": "2026-06-06T18:14:12+00:00"
},
"bez_investorov": {
"last_message_id": 34,
"last_seen_date": "2026-06-06T14:02:03+00:00"
},
"budny_lucky_hunter": {
"last_message_id": 2069,
"last_seen_date": "2026-06-05T11:36:05+00:00"
},
"remotejobss": {
"last_message_id": 12948,
"last_seen_date": "2026-06-06T10:00:06+00:00"
},
"huntermikevolkov": {
"last_message_id": 308,
"last_seen_date": "2026-06-05T06:45:07+00:00"
},
"devitjobs_nl": {
"last_message_id": 1636,
"last_seen_date": "2026-06-05T09:03:47+00:00"
},
"Jobs_global_startups": {
"last_message_id": 711,
"last_seen_date": "2026-06-04T07:20:45+00:00"
},
"gogetajob": {
"last_message_id": 145083,
"last_seen_date": "2026-06-06T11:39:16+00:00"
},
"agile_jobs": {
"last_message_id": 61712,
"last_seen_date": "2026-06-04T13:49:42+00:00"
},
"techjobsworld": {
"last_message_id": 1614,
"last_seen_date": "2026-06-02T17:44:08+00:00"
},
"javascriptjobjs": {
"last_message_id": 424,
"last_seen_date": "2026-05-12T16:25:08+00:00"
},
"remote_jobs_today": {
"last_message_id": 103,
"last_seen_date": "2025-04-06T07:34:01+00:00"
},
"RemotiveJobs_All_Others": {
"last_message_id": 514,
"last_seen_date": "2024-12-30T03:13:11+00:00"
},
"Remote_Software_Developer_Jobs": {
"last_message_id": 1073,
"last_seen_date": "2024-12-24T15:13:05+00:00"
},
"serbia_jobs": {
"last_message_id": 48345,
"last_seen_date": "2026-06-06T16:05:27+00:00"
},
"jaabz_com": {
"last_message_id": 10793,
"last_seen_date": "2026-06-06T19:00:07+00:00"
},
"ai_dev_job": {
"last_message_id": 81,
"last_seen_date": "2026-06-06T15:00:41+00:00"
},
"devsunite": {
"last_message_id": 46,
"last_seen_date": "2026-06-06T18:02:25+00:00"
},
"notificaJobs_nodeJS": {
"last_message_id": 3877,
"last_seen_date": "2026-06-06T12:49:44+00:00"
},
"jsdevjob": {
"last_message_id": 2384,
"last_seen_date": "2026-06-05T18:03:38+00:00"
},
"itdevjobs": {
"last_message_id": 703,
"last_seen_date": "2026-06-05T17:00:11+00:00"
},
"dev_connectablejobs": {
"last_message_id": 2123,
"last_seen_date": "2026-06-05T15:02:24+00:00"
},
"it_jobs_cyprus": {
"last_message_id": 7624,
"last_seen_date": "2026-06-06T19:20:59+00:00"
},
"JS_RemoteJobs": {
"last_message_id": 336,
"last_seen_date": "2026-06-05T11:00:37+00:00"
}
}