Compare commits
6 Commits
49770b351f
...
631ddddb37
| Author | SHA1 | Date |
|---|---|---|
|
|
631ddddb37 | |
|
|
de9930c4c2 | |
|
|
5f8d3330f7 | |
|
|
62ec508b07 | |
|
|
d4436483a9 | |
|
|
0f4f823f2a |
|
|
@ -24,27 +24,42 @@ Do NOT use this skill for:
|
|||
|
||||
## Setup overview
|
||||
|
||||
- **Launcher script**: `scripts/launch-chrome.sh` — starts Chrome with `--remote-debugging-port=9222` against the local profile copy at `.chrome/Default`.
|
||||
- **Launcher script**: `scripts/launch-chrome.sh` — starts Chrome with `--remote-debugging-port=9222` against the local profile copy at `.chrome/`. The script is idempotent: if Chrome is already running it exits immediately.
|
||||
- **Profile copy**: `.chrome/` is a fork of Oleg's `Profile 1` (the `usulpro@gmail.com` profile). Logged-in cookies live here. The directory is gitignored.
|
||||
- **MCP server**: `chrome-devtools` is configured in `.mcp.json` with `--browserUrl=http://127.0.0.1:9222`, i.e. it attaches to the already-running Chrome rather than launching its own.
|
||||
|
||||
You do not launch Chrome yourself — Oleg starts it manually via the script. Your job is to detect whether it's running and to use the MCP tools to drive it.
|
||||
|
||||
## Standard workflow
|
||||
|
||||
### 1. Verify Chrome is running and attached
|
||||
### 1. Ensure Chrome is running and attached
|
||||
|
||||
Before any browser action, confirm the debug port is live:
|
||||
Before any browser action, confirm the debug port is live, and launch Chrome automatically if it isn't:
|
||||
|
||||
```bash
|
||||
curl -sf http://127.0.0.1:9222/json/version >/dev/null && echo OK || echo DOWN
|
||||
```
|
||||
|
||||
If `DOWN`, tell Oleg:
|
||||
**If `DOWN`** — launch the script in the background and poll until Chrome is ready:
|
||||
|
||||
> Chrome не запущен с debug-портом. Запусти `./scripts/launch-chrome.sh` и скажи когда готово.
|
||||
```bash
|
||||
# Launch Chrome in background (script uses exec, so backgrounding the script is correct)
|
||||
nohup /projects/my-projects/cv-2026/scripts/launch-chrome.sh >/tmp/chrome-launch.log 2>&1 &
|
||||
|
||||
Do NOT try to launch Chrome yourself — the script blocks the terminal and the user is the one who controls the browser window.
|
||||
# Poll up to 15 seconds
|
||||
for i in $(seq 1 15); do
|
||||
sleep 1
|
||||
curl -sf http://127.0.0.1:9222/json/version >/dev/null && echo "Chrome up after ${i}s" && break
|
||||
echo "Waiting... ${i}s"
|
||||
done
|
||||
curl -sf http://127.0.0.1:9222/json/version >/dev/null || echo "FAILED"
|
||||
```
|
||||
|
||||
If Chrome does not respond after 15 seconds, stop and report:
|
||||
|
||||
> Chrome не запускается. Посмотри `/tmp/chrome-launch.log` и проверь, что `.chrome/` существует.
|
||||
|
||||
If the launcher prints `Error: .chrome/ does not exist`, stop immediately — there is no profile to use and you cannot create one.
|
||||
|
||||
Do NOT try to sign Oleg in, copy profiles, or work around a missing `.chrome/` directory.
|
||||
|
||||
### 2. Open / select the right page
|
||||
|
||||
|
|
@ -100,7 +115,7 @@ When the user asks "what fields does this application form want?", use `take_sna
|
|||
|
||||
| Symptom | Likely cause | Action |
|
||||
|---------|--------------|--------|
|
||||
| `curl http://127.0.0.1:9222/json/version` fails | Chrome not launched with debug port | Ask Oleg to run `./scripts/launch-chrome.sh` |
|
||||
| `curl http://127.0.0.1:9222/json/version` fails | Chrome not launched with debug port | Run `./scripts/launch-chrome.sh` in background and poll (see step 1) |
|
||||
| `new_page` times out but the tab appears in `list_pages` | Slow site / heavy SPA | Continue — check the final URL via `list_pages` |
|
||||
| Final URL is `/login`, `/checkpoint`, `/signin` | Not signed in | **Stop. Tell Oleg.** Don't try to log in for him. |
|
||||
| Feed/list shows only the first few items | Lazy loading hasn't fired | Scroll the inner container, not `window` |
|
||||
|
|
@ -109,7 +124,7 @@ When the user asks "what fields does this application form want?", use `take_sna
|
|||
|
||||
## Don'ts
|
||||
|
||||
- **Don't launch Chrome yourself** (the launcher blocks the terminal; Oleg owns the window).
|
||||
- **Don't launch Chrome interactively** — always use `nohup ... &` so it detaches from the terminal.
|
||||
- **Don't try to sign Oleg in** when you hit a login wall — just report and wait.
|
||||
- **Don't submit forms, click "Apply", send messages, or post anything** unless explicitly instructed.
|
||||
- **Don't loop on bot-detection pages** — one detection means stop.
|
||||
|
|
@ -118,7 +133,7 @@ When the user asks "what fields does this application form want?", use `take_sna
|
|||
|
||||
## Example: "read first N LinkedIn posts"
|
||||
|
||||
1. `curl -sf http://127.0.0.1:9222/json/version` → confirm Chrome up.
|
||||
1. `curl -sf http://127.0.0.1:9222/json/version` → if DOWN, launch via `nohup ./scripts/launch-chrome.sh &` and poll until up.
|
||||
2. `list_pages` — is LinkedIn already open? If not, `new_page(https://www.linkedin.com/feed/)`.
|
||||
3. Verify final URL contains `/feed/` and not `/login`.
|
||||
4. `evaluate_script`: find `<main>`, step `scrollTop += 1200` with 800ms waits until enough `"Feed post"` h2s have appeared.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,251 @@
|
|||
---
|
||||
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**: all p1, p2, p3 go through Haiku first pass. Main session reviews only APPLY and VERIFY verdicts from Haiku. SKIP verdicts from Haiku accepted without re-review.
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
---
|
||||
name: triage-jobs
|
||||
description: Triage one or more job vacancies using Oleg's canonical apply/skip playbook (vacancy-filter-and-triage-2026.md). Applies 4 hard gates, classifies the CV track, runs the research protocol, checks for prior applications, and outputs a consolidated verdict. For batch runs from telegram_inbox.json, stratifies by priority and delegates p2/p3 bulk to a Haiku subagent to protect the main session.
|
||||
---
|
||||
|
||||
# triage-jobs
|
||||
|
||||
Operational implementation of `base/reference/vacancy-filter-and-triage-2026.md`. That file is the authoritative playbook — read it before running this skill if you don't have it in context.
|
||||
|
||||
Posture: **trying-friendly, not filtering-friendly.** Hard skip is rare and requires a concrete objective blocker. On every ambiguous case, default to "подаём".
|
||||
|
||||
---
|
||||
|
||||
## When to use
|
||||
|
||||
- Oleg provides a job posting URL, raw JD text, or says "разбери вакансию".
|
||||
- After running `scripts/fetch_telegram_jobs.py` — triage `tracking/telegram_inbox.json`.
|
||||
- Any "should we apply?" question about a specific role.
|
||||
|
||||
---
|
||||
|
||||
## Input modes
|
||||
|
||||
| Mode | What Oleg provides |
|
||||
|---|---|
|
||||
| **Single URL** | One job posting link — fetch, triage, output verdict |
|
||||
| **Raw JD** | Pasted text — triage directly |
|
||||
| **Batch (inbox)** | No specific job — triage all entries in `tracking/telegram_inbox.json` |
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Read the playbook (if needed)
|
||||
|
||||
If `vacancy-filter-and-triage-2026.md` is not already in context:
|
||||
|
||||
```
|
||||
Read: base/reference/vacancy-filter-and-triage-2026.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Fetch the JD
|
||||
|
||||
For URL input:
|
||||
1. Try `WebFetch` first.
|
||||
2. If blocked (403, bot wall, login wall, JS-only, empty body) → switch to `chrome-session` skill immediately, no confirmation needed.
|
||||
3. If neither works → halt and report. **Never reconstruct a JD from memory.**
|
||||
|
||||
For Greenhouse / Lever / Ashby / Freshteam ATS: SSR usually works; confirm the "Apply" button is active (not a JS overlay saying "not accepting applications").
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Extract fast facts (< 1 min)
|
||||
|
||||
Pull from the JD:
|
||||
|
||||
- Role title and seniority signals
|
||||
- Must-have stack (vs. nice-to-have)
|
||||
- AI involvement: **must-have** / nice-to-have / not mentioned
|
||||
- Location & remote policy
|
||||
- Timezone / overlap requirements (explicit or inferable from location)
|
||||
- Compensation signals (range, equity framing, "scrappy" language, funding stage)
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Run 4 hard gates
|
||||
|
||||
Evaluate gates in order. First failure → `SKIP` with one objective reason. All four pass → continue.
|
||||
|
||||
### Gate 1 — Geography / hiring
|
||||
|
||||
Passes if the role can hire Oleg via **any** of:
|
||||
- Global remote (non-US/UK/EU-restricted)
|
||||
- Contractor via Deel or equivalent EOR covering Thailand
|
||||
- B2B contract for remote work
|
||||
- Russian-speaking team / Russian-speaking founders, hiring remotely (excluding RU/BY companies explicitly requiring presence)
|
||||
- Russian companies hiring remotely (presence in RU not required)
|
||||
- Relocation offered **AND** comp ≥ $10k/month
|
||||
- Temporary contract fully geography-agnostic (Mira, Algery, similar aggregators)
|
||||
|
||||
Skip if: location-locked / US-only / UK-only / EOR doesn't cover Thailand and no contractor option.
|
||||
|
||||
### Gate 2 — Timezone (comp-scaled, not binary)
|
||||
|
||||
Oleg's flexible window: **9:00–22:00 GMT+7**. Comfortable day: 11:00–20:00.
|
||||
|
||||
| Required overlap | Pass condition |
|
||||
|---|---|
|
||||
| Fits within 9:00–22:00 GMT+7 | Passes at any comp ≥ floor |
|
||||
| Partially outside the window | Passes if comp > $9k |
|
||||
| Full US-day / PST core hours / night work | Passes if comp > $10k |
|
||||
|
||||
Skip only if: comp ≤ $9k **AND** required overlap falls outside 9:00–22:00 GMT+7.
|
||||
|
||||
### Gate 3 — Compensation
|
||||
|
||||
| Tier | Range | Treatment |
|
||||
|---|---|---|
|
||||
| Target | $8–12k/month | Main funnel |
|
||||
| Floor | $6–8k/month | Normal lower bound |
|
||||
| Fallback | $4.5–6k/month | Temporary bridge only — штучно и осознанно |
|
||||
| Skip | < $4.5k/month | Hard skip |
|
||||
|
||||
No explicit comp? Read signals: pre-seed + equity-heavy framing + "scrappy team" → likely below floor. Founding-engineer at Series A–B AI startup can stretch to $15k+ if profile matches.
|
||||
|
||||
### Gate 4 — Seniority
|
||||
|
||||
Passes for: Senior / Staff / Tech Lead / experienced middle+.
|
||||
|
||||
- **Skip** if: explicitly Junior or mid with no growth ceiling.
|
||||
- **Do NOT skip** if: role title is Lead / Principal / Head of — treat as "they don't have that person internally, try it."
|
||||
- **Do NOT skip** if: role looks like middle but comp fits floor/target AND the role offers interesting new experience (AI, client-facing, English immersion).
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Anti-pattern self-check
|
||||
|
||||
Before marking skip, run through the false-blocker list. If you're about to skip on any of these, escalate to the next tier instead:
|
||||
|
||||
1. Role looks senior-heavy (Lead/Principal/Head) → that's a try signal.
|
||||
2. One missing skill → ask Oleg first (see Step 6), not skip.
|
||||
3. "Not an AI-first company" → Track B or Hybrid, not skip.
|
||||
4. Superficial domain mismatch → look at actual technical tasks (orchestration, LLM workflows, structured extraction).
|
||||
5. "Not a Russian-speaking scale-up" → neutral, not negative.
|
||||
6. Timeline outside 8-week window → affects sequencing, not eligibility.
|
||||
7. Comp below $8k but in floor/fallback → consider, especially strategic or interesting roles.
|
||||
8. Tech stack mismatch with fast ramp → lean toward apply, mention ramp honestly.
|
||||
9. Doesn't fully pass triage but ≥50% match + low application effort (LinkedIn Easy Apply, simple form, DM) → **quick-apply** mode: note it explicitly as "fast/training apply."
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Research protocol
|
||||
|
||||
Run for all roles that cleared the gates (and for borderline cases to resolve ambiguity).
|
||||
|
||||
### 5.0 — Check for prior application
|
||||
|
||||
Before deep research, check whether Oleg already applied to this role or company:
|
||||
|
||||
- **Trello** (`mcp__trello__set_active_board` → board `6a1a9a5af082cb0526b22704`, then search cards or scan the "Applyed" list).
|
||||
- **`tracking/applications.md`** — grep for company name.
|
||||
- **Telegram** — search in job-tracking chats if applicable.
|
||||
- **Gmail** — search for company name if accessible.
|
||||
|
||||
If already applied → note it in the output, don't re-triage as new.
|
||||
|
||||
### 5.1 — Research the company
|
||||
|
||||
What the company does, product, stage, funding, team size, recent news, reputation (Glassdoor / Blind if available). Goal: context + ghost/red-flag filter.
|
||||
|
||||
### 5.2 — Cross-platform posting check
|
||||
|
||||
Find the same vacancy on: company careers page, LinkedIn, ATS link (Greenhouse/Lever/Ashby/etc.), job boards, aggregators. Consolidate:
|
||||
|
||||
- **Posting date** — earliest appearance. 90+ days without update = ghost signal.
|
||||
- **Comp range** — different platforms often show different bands. Collect all.
|
||||
- **Geo restrictions** — any platform saying US Only / UK Only / location-locked / eligible countries list?
|
||||
|
||||
### 5.3 — Restriction priority rule
|
||||
|
||||
**Restrictions are authoritative.** If one source says "remote" and another says "US only" → US only wins → Gate 1 skip. Less restrictive wording wins **only** if it explicitly and clearly overrides/removes the restriction.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Missing skills
|
||||
|
||||
One missing skill is **not** a skip if it isn't flagged as explicit MUST HAVE.
|
||||
|
||||
Action: **ask Oleg before applying** — "JD asks for X, I don't see it in your materials — did you have any experience with it?" Many skills exist but were trimmed from CV (examples: Redux Toolkit/RTK Query, Vitest, Playwright, Jest, eCommerce/CMS work).
|
||||
|
||||
Real gaps — can name without checking: Go, Rust, Java, C++, Ruby, PHP, heavy DevOps (K8s orchestration at scale, SRE), native mobile (Swift, Kotlin). React Native and Python are bridgeable, not gaps.
|
||||
|
||||
**Never flag a gap explicitly in a cover letter** — don't write "one thing to flag" or "I notice a gap here."
|
||||
|
||||
---
|
||||
|
||||
## Step 7 — Classify track (for roles that cleared gates)
|
||||
|
||||
| JD condition | Track | CV file |
|
||||
|---|---|---|
|
||||
| AI in **must-have**: LLM / agents / MCP / RAG / orchestration / embeddings in core requirements | **Track A** | `base/cv-2026-05-base.md` |
|
||||
| Pure Node.js / backend, AI as nice-to-have or "significant plus" | **Backend-first** | `base/cv-backend-variant-hostinger-2026-05.md` |
|
||||
| Senior / full-stack / frontend, no AI in must-have | **Track B** | `base/oleg_proskurin_fullstack_techlead_cv.md` |
|
||||
| Senior + AI-tooling fluency as requirement (build with AI assistance, not AI product) | **Hybrid** | CV-B with AI moved up 2–3 positions in Skills |
|
||||
|
||||
Russian-speaking scale-ups (Manychat, inDrive, Wheely, Joom, Adapty, similar) → **Track B by default**, even if AI not mentioned.
|
||||
|
||||
Tiebreakers:
|
||||
- Track B vs. skip → Track B wins.
|
||||
- Track A vs. Track B for hybrid JD → AI in must-have → A; otherwise → B.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 — Output format
|
||||
|
||||
One consolidated block per vacancy. No separate research section — merge everything.
|
||||
|
||||
```
|
||||
## [Company] — [Role Title]
|
||||
|
||||
**Verdict:** APPLY (Track A) | SKIP | QUICK-APPLY
|
||||
|
||||
**Gate check:**
|
||||
- Gate 1 (Geo): ✅ global remote / ❌ US-only
|
||||
- Gate 2 (TZ): ✅ EMEA overlap fits / ⚠️ partial, comp > $9k ok / ❌
|
||||
- Gate 3 (Comp): ✅ $X–Yk (target) / ⚠️ floor / ❌
|
||||
- Gate 4 (Level): ✅ Senior / ❌ Junior
|
||||
|
||||
**Track:** A / B / Backend-first / Hybrid — CV: [filename]
|
||||
|
||||
**Strong matches:** [2–4 bullets: specific tech/scope from JD that map directly to Oleg's background]
|
||||
|
||||
**Open questions:** [missing skills to ask Oleg about; 1–2 items max]
|
||||
|
||||
**Research notes:**
|
||||
- Company: [stage, product, funding, red flags if any]
|
||||
- Posting date: [earliest found] — [fresh / stale signal]
|
||||
- Comp across platforms: [consolidate bands found]
|
||||
- Geo/restrictions: [what each platform says; which is authoritative]
|
||||
|
||||
**Prior application:** none found / ⚠️ already applied on [date], see Trello card [name]
|
||||
|
||||
**Proof-points for letter:** [2–3 concrete bullets that directly answer the JD's key asks]
|
||||
```
|
||||
|
||||
For SKIP: one-liner after gate check explaining the objective blocker. No research section needed unless it was needed to determine the gate outcome.
|
||||
|
||||
---
|
||||
|
||||
## Batch mode — stratified triage from 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`).
|
||||
2. **All p1, p2, and p3 entries**: delegate to a Haiku subagent first pass.
|
||||
- Pass the raw entries, the playbook summary, and the `known_applied` list to the subagent.
|
||||
- Subagent returns a structured verdict list (APPLY / SKIP / QUICK-APPLY / VERIFY).
|
||||
- SKIP verdicts from Haiku are accepted without re-review.
|
||||
3. **Main session reviews only** APPLY and VERIFY verdicts from Haiku — runs full Step 1–8 per entry.
|
||||
4. 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 VERIFY entries: run a fast WebSearch `"{company} remote hiring countries"` to resolve geo; then apply the gate.
|
||||
|
||||
**Why Haiku-first for everything:** p1 channels can have 50–100+ messages per run. Sending them straight to the main session risks context overflow and losing genuine APPLY roles in the flood. Haiku filters down to a manageable APPLY+VERIFY set; main session then does quality deep-dive on only those. A few extra false VERIFYs from Haiku are fine — main session catches them. Missing a real APPLY because main session ran out of context is not fine.
|
||||
|
||||
### Haiku subagent prompt template
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
Gate 1 (Geo): Must allow: global remote non-US/UK/EU, contractor via Deel/EOR from Thailand, B2B remote, Russian-speaking teams hiring remotely (not RU/BY presence-required), relocation with comp ≥$10k, geography-agnostic temp contracts.
|
||||
Gate 2 (TZ): Oleg's window 9:00–22:00 GMT+7. Outside window: ok if comp > $9k. Night/PST core: ok if comp > $10k. Skip only if comp ≤ $9k AND overlap is outside window.
|
||||
Gate 3 (Comp): Skip if < $4.5k/month. Floor $6–8k, target $8–12k.
|
||||
Gate 4 (Level): Skip if explicitly Junior or mid with no growth ceiling.
|
||||
|
||||
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").
|
||||
|
||||
Geo rules (Gate 1):
|
||||
- Message explicitly says "global remote" / "worldwide" / "anywhere" / "любой город" / contractor-friendly → Gate 1 passes, proceed.
|
||||
- Message says "United States" / "US only" / "United Kingdom only" / specific country list excluding Thailand → SKIP Gate 1.
|
||||
- Message says "Remote" with no geo qualifier → VERIFY geo (cannot confirm Thailand eligibility without JD).
|
||||
- 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:
|
||||
[paste entries from telegram_inbox.json]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## After triage — update tracking
|
||||
|
||||
For every APPLY verdict:
|
||||
- Add a row to `tracking/applications.md` with status `todo` (not yet applied) or `applied` if Oleg confirms.
|
||||
- Create a Trello card in the TODO column (`6a1aa59555aab72a261c42aa`) on board `6a1a9a5af082cb0526b22704` with: company, role, track, verdict summary, link to JD.
|
||||
|
||||
For SKIP: no tracking entry needed unless Oleg says to keep it for reference.
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
---
|
||||
name: write-as-oleg
|
||||
description: Write any English text artifact (cover letter, essay, form answer, outreach message, CV tailoring) in Oleg Proskurin's voice. Consolidates voice profile, writing process, and paired examples. Runs a mandatory deterministic voice linter before delivery and iterates until clean.
|
||||
---
|
||||
|
||||
# write-as-oleg
|
||||
|
||||
Produces English text artifacts under Oleg's name: cover letters, application essays, form answers, outreach DMs, screening answers, CV bullets. The text must be indistinguishable from how Oleg himself would have written it — not from how a polished native speaker or a model would write it.
|
||||
|
||||
**Replaces and consolidates:**
|
||||
- `base/reference/writing-on-behalf-of-oleg.md`
|
||||
- `base/reference/voice-profile-oleg.md`
|
||||
- `base/reference/voice-dictionary-oleg-paired.md`
|
||||
|
||||
---
|
||||
|
||||
## When to use
|
||||
|
||||
- Oleg asks to write, draft, or refine any text that will go out under his name.
|
||||
- Tailoring a CV or rewriting CV bullets for a specific role.
|
||||
- Writing a cover letter, answering application form questions, drafting outreach.
|
||||
- Any text where Oleg says "напиши от меня", "сделай cover letter", "ответь за меня".
|
||||
|
||||
---
|
||||
|
||||
## Mandatory lint loop — run before every delivery
|
||||
|
||||
Every generated text **must** pass through the voice linter before it reaches Oleg.
|
||||
|
||||
```bash
|
||||
node .claude/skills/write-as-oleg/lint-voice.mjs "full text here"
|
||||
# or via stdin:
|
||||
echo "full text" | node .claude/skills/write-as-oleg/lint-voice.mjs
|
||||
```
|
||||
|
||||
**Loop protocol:**
|
||||
|
||||
1. Generate draft (Phase 1 below).
|
||||
2. Run linter. Read findings.
|
||||
3. Fix all ERRORs in the text. Do NOT auto-insert linter output into the text.
|
||||
4. Run linter again. Repeat until exit 0 (no ERRORs).
|
||||
5. Review any remaining WARNs — fix if clearly AI-speak, leave if contextually correct.
|
||||
6. Output final text to Oleg.
|
||||
|
||||
The agent fixes the text and delivers it. The linter only finds issues — it does not rewrite.
|
||||
|
||||
---
|
||||
|
||||
## Writing process — four phases
|
||||
|
||||
Do not skip Phase 0. Do not merge Phases 1 and 2.
|
||||
|
||||
### Phase 0 — Seed and strategy (before writing)
|
||||
|
||||
Output **ultra-briefly** before drafting:
|
||||
|
||||
- **Seed** — the one idea that must reach the reader.
|
||||
- **Strategy** — format/channel, register (formal/neutral/conversational), addressee, intended effect.
|
||||
- **Slant** — if clear from the request: what is primary, what is secondary. If unclear, write neutrally and flag it.
|
||||
|
||||
Then write without waiting for confirmation — unless the addressee, register, or goal is genuinely unclear.
|
||||
|
||||
### Phase 1 — Draft
|
||||
|
||||
Write the text while avoiding all Category A artifacts (list below) from the start. Do not add fake liveliness in their place. Build structural unevenness from Oleg's stated slant; if he gave none, write clean and even without invented asymmetry.
|
||||
|
||||
### Phase 2 — Cleanup
|
||||
|
||||
Do exactly three things:
|
||||
|
||||
1. **Silently clean** any remaining Category A artifacts.
|
||||
2. **Ask Oleg** about Category B — where to shift emphasis, what to add/cut, where to take a firmer stance, which real fact/detail to insert. Do not invent Category B yourself.
|
||||
3. With his answers, **rewrite**. Then **highlight** remaining markers you cannot remove without losing meaning, and offer him to fix those by hand.
|
||||
|
||||
### Phase 3 — Iteration
|
||||
|
||||
Loop: either edit per Oleg's specific notes, or he sends back his edited version and you work from it (clean Category A, highlight Category B). Repeat until he says done.
|
||||
|
||||
---
|
||||
|
||||
## Category A — remove always, no input needed
|
||||
|
||||
These are deletions and clean replacements. Meaning is preserved. The linter catches the detectable subset; catch the rest by eye.
|
||||
|
||||
- **Em-dash** on parenthetical inserts — replace with colon, comma, or separate sentence.
|
||||
- **"not X, but Y" / "it's not just …, it's …"** — rephrase as direct statement.
|
||||
- **Rule of three** (exactly 3 items in enumeration) — break into 2 or 4.
|
||||
- **Identical bullet structure** (same grammatical form for every bullet) — diversify.
|
||||
- **Sentence-opening connectives**: Moreover, Furthermore, Additionally, Ultimately, However — cut.
|
||||
- **Metadiscourse** ("first … then … finally …") — cut.
|
||||
- **Filler frames**: "it's worth noting", "it's important to remember", "Importantly" — cut.
|
||||
- **Rhetorical question + immediate answer** — turn into statement.
|
||||
- **Inclusive catch-alls**: "whether you're … or …", "from X to Y" — cut.
|
||||
- **Empty closing**: "In conclusion", "Overall", repacked summary — cut.
|
||||
- **Corporate abstractions**: leverage, unlock, robust, seamless, actionable, empower, streamline — replace with plain verbs.
|
||||
- **Forced punchy rhythm** ("No fluff. Just results.") — remove.
|
||||
- **AI-specific words**: thrilled, passionate, delve, tapestry, cutting-edge, "happy to", "looking forward to", "I am writing to", "in today's".
|
||||
- **Smooth connectives**: That said, Furthermore, Moreover — even mid-paragraph.
|
||||
|
||||
## Category B — needs Oleg's input; do not invent
|
||||
|
||||
Ask or highlight; do not fabricate:
|
||||
|
||||
- **Even structure** (equal-length paragraphs, equal coverage). Ask: what is primary, what is filler. Do not overshoot into chaos.
|
||||
- **Hedging / no stance** ("both are valid", "it depends"). Ask: his actual position. Do not invent confidence he doesn't hold.
|
||||
- **Flat tone / no specifics**. Ask: a real fact, number, project, failure, audience detail. Never fabricate stories.
|
||||
- **Noisiness / over-explaining**. Ask: reader's level, what can be left implicit. Do not over-cut.
|
||||
|
||||
---
|
||||
|
||||
## Voice profile
|
||||
|
||||
### Register
|
||||
|
||||
- Direct, by-the-point, no runway and no warm-up.
|
||||
- Text rides on **technical noun-anchors** (harness, registry, loop, flow, score), not on decorative action verbs.
|
||||
- Confident but not self-promotional. Facts and system names do the work.
|
||||
- Rhythm is uneven: short sentences sit next to long comma chains. Do not level it into monotony.
|
||||
|
||||
### Syntax — reproduce these patterns
|
||||
|
||||
**Long comma chains without semicolons.**
|
||||
Several verb blocks strung together via comma — Oleg's natural rhythm. Do not split into short sentences. Do not insert `;`.
|
||||
> *Anchor:* "Each step reads the previously generated content, select a best matching component based on a score system, fills props based on compact component schema."
|
||||
|
||||
**Postpositional "from" and "based on" instead of compression.**
|
||||
Justification and source go at the end of the phrase. Double `from … from …` in a row is fine.
|
||||
> *Anchor:* "selecting components from a registry of 200+ from a scored, filtered candidate pool"
|
||||
|
||||
**Pointer words (such / these / that) instead of repeating the noun.**
|
||||
> *Anchor:* "I've implemented such scenarios", "a harness around these steps"
|
||||
|
||||
**Headline phrase + colon + content.**
|
||||
Colon is the main thought divider. NOT em-dash.
|
||||
> *Anchor:* "I am applying with some relevant experience:"
|
||||
> *Anchor:* "My day-to-day stack: [list]"
|
||||
|
||||
**Parentheses for secondary detail.**
|
||||
> *Anchor:* "(see primeui.com)", "(on Mastra + Vercel AI SDK)", "(e.g. ...)"
|
||||
|
||||
**Closings are short and dry.**
|
||||
> *Anchor:* "Open to chat if it's a fit."
|
||||
> NOT: "Happy to dig into specifics on a call" / "Looking forward to hearing from you"
|
||||
|
||||
### Lexicon — Oleg's anchor words
|
||||
|
||||
Use these when they fit the meaning:
|
||||
|
||||
- **harness** — infrastructure/wrapper around a core ("a harness around these steps with validation and retry")
|
||||
- **orchestrates / orchestration** — ("it orchestrates around 20 agent tools")
|
||||
- **registry** — component registry, registry of 200+
|
||||
- **flow** — generation flow, publishing flow, quiz flows
|
||||
- **loop** — agent loop, "the loop runs until..."
|
||||
- **score / scored / scoring** — score system, scored candidate pool
|
||||
- **from scratch** — "I've been working on that project from scratch"
|
||||
- **assist / assists** — "the chatbot that assist users"
|
||||
- **etc** (not "and so on"), **based on**, **such / these / that**
|
||||
|
||||
Default to system names and technical nouns, not decorative action verbs.
|
||||
|
||||
### Grammar signatures — preserve dosed, do NOT "fix"
|
||||
|
||||
These are hand-markers of a Russian-speaking senior. Keep dosed to avoid sounding like a polished native. Do NOT preserve to the point of broken meaning.
|
||||
|
||||
| Oleg writes | What the model usually "fixes" | Status |
|
||||
|---|---|---|
|
||||
| `AI based platform` (no article) | `an AI-based platform` | Keep dosed — drop article before compound nouns |
|
||||
| `reads... select... fills` (floating -s) | `reads... selects... fills` | Keep dosed — agreement floats within a clause |
|
||||
| `during more than one year` | `for over a year` | Keep in informal channels |
|
||||
| `assist users` (no -s) | `helps users` | Keep — his anchor word |
|
||||
| `I see the relevant experience in what I've been doing` | `My relevant experience comes from` | Keep — deliberate Russian calque for RU-team signal |
|
||||
|
||||
**Calibration:** more unevenness in informal channels (Telegram, followup DMs); cleaner in cover letters / essays — but still not a polished native.
|
||||
|
||||
### Clean errors — always fix, not voice
|
||||
|
||||
These he accepted without pushback. They are typos, not signatures:
|
||||
|
||||
| Oleg wrote | Correct |
|
||||
|---|---|
|
||||
| `splitted to:` | `split into:` |
|
||||
| `serveral` / `scenarious` | `several` / `scenarios` |
|
||||
| `exelent` | `excellent` |
|
||||
| `clients communication` | `client communication` |
|
||||
| `regulator approvements was` | `regulator approval was a critical step` |
|
||||
| `tested for a compatibility` | `tested for compatibility` |
|
||||
| `Senior AI Engineer .` (space before dot) | `Senior AI Engineer.` |
|
||||
|
||||
---
|
||||
|
||||
## Voice dictionary — key paired examples
|
||||
|
||||
These are real edits: model's version (left) vs. what Oleg wrote or kept (right). The right column is the target voice.
|
||||
|
||||
### Verbs — do not upgrade to "native" verbs
|
||||
|
||||
| Model wrote | Oleg wrote | What changes |
|
||||
|---|---|---|
|
||||
| I've built this pattern in several places | I've implemented such scenarios in several places | `implemented` + pointer `such scenarios` |
|
||||
| picks the best-matching component by score | select a best matching component based on a score system | postposition `based on a score system` instead of compression |
|
||||
| fills its props from a compact component schema | fills props based on compact component schema | drops article; `based on` |
|
||||
| I built a harness around these steps | I also created a harness around these steps | `created` not `built`; leading `also` |
|
||||
| the chatbot that helps users create and modify | the chatbot that assist users in creating/modifying | `assist`, `in creating/modifying` |
|
||||
|
||||
### Self-characterisation — colon + facts, not claims
|
||||
|
||||
| Model wrote | Oleg wrote/kept | What changes |
|
||||
|---|---|---|
|
||||
| I'm a direct match | My day-to-day stack: [list] | Does not claim fit — gives facts via colon-header |
|
||||
| My day-to-day stack matches yours | [specific years-on-stack per technology] | "too casual" — replaced with per-tech year claims |
|
||||
| The core of the role is the work I've been doing | [opens directly with own work, no JD recap] | Does NOT recap the JD back to the employer |
|
||||
|
||||
### Vague → short concrete sentences
|
||||
|
||||
| Model wrote | Oleg wrote/kept | What changes |
|
||||
|---|---|---|
|
||||
| Multi-tenant and white-label is not new to me | [short concrete sentences: mobile app, web portal, casino team, betting widgets as consumers of one backend] | "too poetic" — replaced with named entities |
|
||||
| Built the AI layer containing some independent AI flows | [named 4 flows: content writing, page generation, chatbot, sitemap generation] | specific count + names |
|
||||
| Happy to dig into specifics on a call | Open to chat if it's a fit. | shorter, drier, his register |
|
||||
|
||||
### Connectives — cut, don't glue
|
||||
|
||||
| Model wrote | Oleg wrote | What changes |
|
||||
|---|---|---|
|
||||
| So, the content team ran experiments... | Content team ran experiments... | dropped leading `So,` |
|
||||
| Furthermore / Moreover / That said | [new sentence or colon, no connective] | connection from content, not from a word |
|
||||
| — (em-dash on insert) | `:` or `,` or separate sentence | em-dash on parenthetical = not his |
|
||||
|
||||
### Calibration — one full pair
|
||||
|
||||
**Model version (left, polished native):**
|
||||
> PrimeUI is an AI-based platform that generates production-level codebases for client websites, with a strong focus on the design quality of components and pages — I've worked on it from scratch for over a year. The core of page generation is a planner agent that iterates section by section across a page, autonomously selecting components from a registry of 200+ via a scored, filtered candidate pool. Each step reads the previously generated content, picks the best-matching component by score, and fills its props from a compact component schema. I built a harness around these steps with validation and a retry system.
|
||||
|
||||
**Oleg's version (right, target voice):**
|
||||
> PrimeUI is AI based platform for generation production level codebases for clients websites with a focus on hi design quality of components and pages. I've been working on that project from scratch during more than one year. The core of pages generation is a planner agent that iterates section by section across a page, autonomously selecting components from a registry of 200+ from a scored, filtered candidate pool. Each step reads the previously generated content, select a best matching component based on a score system, fills props based on compact component schema. I also created a harness around these steps with validation and retry system.
|
||||
|
||||
Differences: drop-articles, comma-chain without `and`, double `from … from`, postposition `based on`, floating -s, `from scratch during more than one year`, `created` not `built`.
|
||||
|
||||
---
|
||||
|
||||
## "Yes." rule
|
||||
|
||||
Bare "Yes." as opener — **only** in followup DMs where:
|
||||
- There is a concrete closed yes/no question.
|
||||
- Format "yes/no → then elaborate" is appropriate.
|
||||
|
||||
> *OK:* "Do you have a production use case where an LLM operated in a loop?" → "Yes. On primeui.com I've built this pattern..."
|
||||
|
||||
**Do NOT use** in: cover letters, essays, first outreach, any text that is not a direct reply to a closed question.
|
||||
|
||||
---
|
||||
|
||||
## Stop-list — not Oleg's voice (linter catches the detectable subset)
|
||||
|
||||
If any of these appear — rewrite:
|
||||
|
||||
- `—` em-dash on parenthetical inserts
|
||||
- Perfectly parallel bullet structure (identical grammar form on every bullet)
|
||||
- GPT words: **leverage, robust, seamless, passionate, thrilled, delve, tapestry, navigate** (metaphorical), **cutting-edge, actionable, empower, streamline, unlock**
|
||||
- Smooth openers: **Furthermore, Moreover, That said, Additionally, Ultimately, However, Importantly**
|
||||
- "looking forward to", "happy to", "excited to", "I am writing to", "in today's"
|
||||
- "it's worth noting", "it's important to remember", "In conclusion", "Overall,"
|
||||
- Even, monotone rhythm across all sentences
|
||||
- "honest framing / one thing to flag / I want to be honest about one thing" — not his pattern (gaps he names in his own words, without this template)
|
||||
- "I'm a great fit" / "I'm a direct match" / "my stack matches yours" — self-claims instead of facts
|
||||
|
||||
---
|
||||
|
||||
## Pre-delivery checklist
|
||||
|
||||
Run after the lint loop exits 0:
|
||||
|
||||
1. No em-dash on inserts? → replaced with `:` / `,` / separate sentence.
|
||||
2. Lint script exits 0? → required.
|
||||
3. Rhythm still uneven? → short and long sentences alternate, no monotone flow.
|
||||
4. Anchor nouns present (harness, registry, loop, flow, score, etc.) where they fit?
|
||||
5. No JD recap in the opening of a cover letter?
|
||||
6. Colon-headers used where the format is "phrase + content"?
|
||||
7. Closing is short and dry? (e.g. "Open to chat if it's a fit.")
|
||||
8. "Yes." opener only if this is a direct reply to a closed question?
|
||||
9. No "one thing to flag / honest framing" construction?
|
||||
10. Grammar signatures dosed correctly for the channel (more in DMs, less in cover letters)?
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* lint-voice.mjs — deterministic voice linter for Oleg's texts.
|
||||
*
|
||||
* Checks for AI/corporate artifacts, wrong typography, and banned patterns
|
||||
* that make text sound like a model rather than Oleg.
|
||||
*
|
||||
* Usage:
|
||||
* node lint-voice.mjs "text to check"
|
||||
* echo "text" | node lint-voice.mjs
|
||||
* node lint-voice.mjs < input.txt
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — clean (no ERRORs; WARNs alone do not fail)
|
||||
* 1 — ERRORs found (agent must fix and re-run)
|
||||
* 2 — usage error
|
||||
*/
|
||||
|
||||
// [level, id, pattern, message]
|
||||
// level: "ERROR" = must fix before delivery; "WARN" = check in context, may be OK
|
||||
const CHECKS = [
|
||||
// ── Typography ─────────────────────────────────────────────────────────────
|
||||
["ERROR", "EM_DASH",
|
||||
/—/g,
|
||||
"Em-dash (—). Replace with colon, comma, or rephrase as separate sentence."],
|
||||
|
||||
["ERROR", "SMART_QUOTES",
|
||||
/[""„«»]/g,
|
||||
'Smart/typographic double quote. Use straight " or rephrase.'],
|
||||
|
||||
["WARN", "SMART_APOSTROPHE",
|
||||
/[''`]/g,
|
||||
"Smart/curly apostrophe or backtick. Use straight apostrophe '."],
|
||||
|
||||
// ── AI / corporate words ───────────────────────────────────────────────────
|
||||
["ERROR", "AI_LEVERAGE",
|
||||
/\bleverag(?:e|es|ed|ing)\b/gi,
|
||||
'"leverage" — AI/corporate word. Replace with plain verb (use, apply, build on).'],
|
||||
|
||||
["ERROR", "AI_ROBUST",
|
||||
/\brobust\b/gi,
|
||||
'"robust" — AI/corporate word. Replace with specific description.'],
|
||||
|
||||
["ERROR", "AI_SEAMLESS",
|
||||
/\bseamless(?:ly)?\b/gi,
|
||||
'"seamless" — AI/corporate word. Cut or replace with plain description.'],
|
||||
|
||||
["ERROR", "AI_PASSIONATE",
|
||||
/\bpassionat(?:e|ely)\b|\bmy passion\b/gi,
|
||||
'"passionate/passion" — AI/corporate word. Replace with direct statement.'],
|
||||
|
||||
["ERROR", "AI_THRILLED",
|
||||
/\bthrilled\b/gi,
|
||||
'"thrilled" — AI word. Replace with direct statement.'],
|
||||
|
||||
["ERROR", "AI_DELVE",
|
||||
/\bdelv(?:e|ing|ed)\b/gi,
|
||||
'"delve" — AI word. Replace with explore / look into / dig.'],
|
||||
|
||||
["ERROR", "AI_TAPESTRY",
|
||||
/\btapestry\b/gi,
|
||||
'"tapestry" — AI word. Remove or replace.'],
|
||||
|
||||
["ERROR", "AI_EMPOWER",
|
||||
/\bempower(?:s|ed|ing)?\b/gi,
|
||||
'"empower" — AI/corporate word. Replace with plain verb.'],
|
||||
|
||||
["ERROR", "AI_STREAMLINE",
|
||||
/\bstreamline[ds]?\b|\bstreamlining\b/gi,
|
||||
'"streamline" — AI/corporate word. Replace with plain verb.'],
|
||||
|
||||
["ERROR", "AI_ACTIONABLE",
|
||||
/\bactionable\b/gi,
|
||||
'"actionable" — AI/corporate word. Replace with plain description.'],
|
||||
|
||||
["ERROR", "AI_UNLOCK",
|
||||
/\bunlock(?:s|ed|ing)?\b/gi,
|
||||
'"unlock" — AI/corporate word. Replace with plain verb.'],
|
||||
|
||||
["ERROR", "AI_CUTTING_EDGE",
|
||||
/cutting[- ]edge/gi,
|
||||
'"cutting-edge" — AI/corporate phrase. Replace with specific description.'],
|
||||
|
||||
["WARN", "AI_INNOVATIVE",
|
||||
/\binnovativ(?:e|ely)\b|\binnovation\b/gi,
|
||||
'"innovative/innovation" — often AI/corporate filler. Replace with specific if used generically.'],
|
||||
|
||||
// ── Sentence openers (capitalised opener form) ────────────────────────────
|
||||
// Matches word at line start OR after terminal punctuation + whitespace
|
||||
["ERROR", "OPENER_FURTHERMORE",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Furthermore[,\s]/gm,
|
||||
'"Furthermore" opener. Cut — connection should come from content, not connective.'],
|
||||
|
||||
["ERROR", "OPENER_MOREOVER",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Moreover[,\s]/gm,
|
||||
'"Moreover" opener. Cut — restructure.'],
|
||||
|
||||
["ERROR", "OPENER_ADDITIONALLY",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Additionally[,\s]/gm,
|
||||
'"Additionally" opener. Cut — restructure without connective.'],
|
||||
|
||||
["ERROR", "OPENER_ULTIMATELY",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Ultimately[,\s]/gm,
|
||||
'"Ultimately" opener. Cut — restructure.'],
|
||||
|
||||
["ERROR", "OPENER_HOWEVER",
|
||||
/(?:^|(?<=[.!?])[ \t]+)However[,\s]/gm,
|
||||
'"However" opener. Cut — use contrast from content, not a gluing connective.'],
|
||||
|
||||
["ERROR", "OPENER_IMPORTANTLY",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Importantly[,\s]/gm,
|
||||
'"Importantly" opener — filler frame. Cut.'],
|
||||
|
||||
["ERROR", "OPENER_THAT_SAID",
|
||||
/(?:^|(?<=[.!?])[ \t]+)That said[,\s]/gm,
|
||||
'"That said" opener. Cut — restructure.'],
|
||||
|
||||
["ERROR", "OPENER_IN_CONCLUSION",
|
||||
/in conclusion[,\s]/gi,
|
||||
'"In conclusion" — empty closing summary. Cut entirely.'],
|
||||
|
||||
["ERROR", "OPENER_OVERALL",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Overall[,\s]/gm,
|
||||
'"Overall" opener — empty summary signal. Cut.'],
|
||||
|
||||
["WARN", "OPENER_SO_COMMA",
|
||||
/(?:^|(?<=[.!?])[ \t]+)So[,\s]/gm,
|
||||
'"So," opener. Oleg cuts these — connection should come from content.'],
|
||||
|
||||
// ── Filler frames ──────────────────────────────────────────────────────────
|
||||
["ERROR", "FILLER_WORTH_NOTING",
|
||||
/it'?s worth noting/gi,
|
||||
'"It\'s worth noting" — filler frame. Cut, say the thing directly.'],
|
||||
|
||||
["ERROR", "FILLER_IMPORTANT_REMEMBER",
|
||||
/it'?s important to remember/gi,
|
||||
'"It\'s important to remember" — filler frame. Cut.'],
|
||||
|
||||
["ERROR", "FILLER_I_AM_WRITING",
|
||||
/\bI am writing to\b/gi,
|
||||
'"I am writing to" — generic opener. Replace with direct lead.'],
|
||||
|
||||
["ERROR", "FILLER_IN_TODAYS",
|
||||
/in today'?s\b/gi,
|
||||
'"In today\'s..." — AI/generic opener. Cut.'],
|
||||
|
||||
// ── AI structural patterns ─────────────────────────────────────────────────
|
||||
["ERROR", "PATTERN_ITS_NOT_JUST",
|
||||
/it'?s not just/gi,
|
||||
'"it\'s not just X, it\'s Y" construction. Rephrase as direct statement.'],
|
||||
|
||||
["WARN", "PATTERN_NOT_X_BUT",
|
||||
/\bnot \w+(?:\s+\w+){0,3},?\s+but\b/gi,
|
||||
'"not X, but Y" — often AI phrasing. Rephrase if rhetorical.'],
|
||||
|
||||
["ERROR", "PATTERN_HONEST_FRAMING",
|
||||
/honest framing|one thing to flag|I want to be honest about/gi,
|
||||
'"honest framing / one thing to flag" template. Not Oleg\'s pattern — remove.'],
|
||||
|
||||
// ── AI closings ────────────────────────────────────────────────────────────
|
||||
["ERROR", "CLOSING_LOOKING_FORWARD",
|
||||
/looking forward to/gi,
|
||||
'"Looking forward to" closing. Replace e.g. "Open to chat if it\'s a fit."'],
|
||||
|
||||
["ERROR", "CLOSING_HAPPY_TO",
|
||||
/\bhappy to\b/gi,
|
||||
'"happy to" — AI phrasing. Replace with plain statement.'],
|
||||
|
||||
// ── Warn-only borderline cases ─────────────────────────────────────────────
|
||||
["WARN", "WARN_NAVIGATE",
|
||||
/\bnavigat(?:e|ing|ed)\b/gi,
|
||||
'"navigate" — check if metaphorical AI-speak ("navigate challenges"). OK if literal.'],
|
||||
|
||||
["WARN", "WARN_LANDSCAPE",
|
||||
/\blandscape\b/gi,
|
||||
'"landscape" — check if metaphorical AI-speak. OK if literal/technical.'],
|
||||
];
|
||||
|
||||
function lineOf(text, pos) {
|
||||
return text.slice(0, pos).split("\n").length;
|
||||
}
|
||||
|
||||
function getContext(text, start, end, window = 60) {
|
||||
const from = Math.max(0, start - window);
|
||||
const to = Math.min(text.length, end + window);
|
||||
let snippet = text.slice(from, to).replace(/\s+/g, " ").trim();
|
||||
if (from > 0) snippet = "…" + snippet;
|
||||
if (to < text.length) snippet = snippet + "…";
|
||||
return snippet;
|
||||
}
|
||||
|
||||
function runChecks(text) {
|
||||
const findings = [];
|
||||
for (const [level, id, pattern, message] of CHECKS) {
|
||||
// Reset lastIndex before each use (patterns are defined with /g)
|
||||
pattern.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = pattern.exec(text)) !== null) {
|
||||
findings.push({
|
||||
level,
|
||||
id,
|
||||
line: lineOf(text, m.index),
|
||||
context: getContext(text, m.index, m.index + m[0].length),
|
||||
message,
|
||||
});
|
||||
// Prevent infinite loops on zero-width matches
|
||||
if (m[0].length === 0) pattern.lastIndex++;
|
||||
}
|
||||
}
|
||||
findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id));
|
||||
return findings;
|
||||
}
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) chunks.push(chunk);
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let text;
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
text = process.argv.slice(2).join(" ");
|
||||
} else if (!process.stdin.isTTY) {
|
||||
text = await readStdin();
|
||||
} else {
|
||||
process.stderr.write('Usage: node lint-voice.mjs "text" OR pipe text via stdin\n');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (!text.trim()) {
|
||||
process.stdout.write("✓ CLEAN — empty input.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const findings = runChecks(text);
|
||||
const errors = findings.filter(f => f.level === "ERROR");
|
||||
const warns = findings.filter(f => f.level === "WARN");
|
||||
|
||||
if (findings.length === 0) {
|
||||
process.stdout.write("✓ CLEAN — no issues found.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.stdout.write(`Found ${findings.length} issue(s): ${errors.length} ERROR, ${warns.length} WARN\n\n`);
|
||||
|
||||
for (const f of findings) {
|
||||
const action = f.level === "ERROR" ? "Fix" : "Note";
|
||||
process.stdout.write(`[${f.level}] ${f.id} — line ${f.line}\n`);
|
||||
process.stdout.write(` Context: "${f.context}"\n`);
|
||||
process.stdout.write(` ${action}: ${f.message}\n\n`);
|
||||
}
|
||||
|
||||
process.stdout.write("---\n");
|
||||
if (errors.length > 0) {
|
||||
process.stdout.write(`EXIT 1 — ${errors.length} error(s) present. Fix and re-run.\n`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.stdout.write("EXIT 0 — no errors (warnings only). Review WARNs if applicable.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
29
.mcp.json
29
.mcp.json
|
|
@ -9,11 +9,30 @@
|
|||
]
|
||||
},
|
||||
"trello": {
|
||||
"command": "sh",
|
||||
"args": [
|
||||
"-c",
|
||||
"set -a; [ -f .env ] && . ./.env; set +a; exec bunx @delorenj/mcp-server-trello"
|
||||
]
|
||||
"command": "bunx",
|
||||
"args": ["@delorenj/mcp-server-trello"],
|
||||
"env": {
|
||||
"TRELLO_API_KEY": "fc7a66f0f39374418fc53ef6b2681c67",
|
||||
"TRELLO_TOKEN": "ATTAa06242e59289e714f350c5d97bc6fa949bef6c2cdd7510d293cc5f3ebc46a9f674A18E58"
|
||||
}
|
||||
},
|
||||
"telegram-usulpro": {
|
||||
"command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"TELEGRAM_API_ID": "30708025",
|
||||
"TELEGRAM_API_HASH": "1de1cfbec7f43f460b4400f72b3093c6",
|
||||
"TELEGRAM_SESSION_STRING": "1ApWapzMBuy6zzXtKHk9qipYPfDHEZOowyn2NukLpKpbRyNggosUMunnjUA7EJTHuwCcf2EWn4Wk6ezjYw8zfczt-nGo4nbQay4a3IfnjHj1byv8sIwIbLqyJzeWlvOU2eO3d9l-5Ys-MY3DbpHC4QaU6MrdizJsxW52hghF8bLUyQ5xw15Ih91X03xG-5XMCeFscwStp2Be8bZgQV1JhiyImufneFt6Z1DjibTnn8U4uYXVG2jd1AmrFna7seGpFBy7vbV4QPjxcYiaOWO_qXH0TSxTHTHmLPK6FAHfkq1L-q5MXlEHIrB4nNeUKKX48gdxW662aYPsH1Audb6-oB8TkycRZbls="
|
||||
}
|
||||
},
|
||||
"telegram-helper": {
|
||||
"command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"TELEGRAM_API_ID": "30708025",
|
||||
"TELEGRAM_API_HASH": "1de1cfbec7f43f460b4400f72b3093c6",
|
||||
"TELEGRAM_SESSION_STRING": "1BVtsOJsBu0I61cjJwTKtmzz5BJOz53W5nvAo_aZJvpCl86hUC1zx-CVnYA4QOZ7ZULL3mNG3xg9uvZD7htcSP8QvH7U9VvkZspLT8ygEAX4pu9KJ-shhjYFCWQBDOsOujlOcGBVaVEBDBLMSIEmU7zfg-zhyTOlZ4M3OOfOQqs8nV9BlELaOTsaHwzU3OscKSwBGjewHcTvqCksp9JXFLOda9gMOBVV_Z65dCg9ovwKKOKSUNiIc3zC3f2UkZTq82hkObrhLewjI2Mue4fIEdCY0ziBkjBVTiPi40-wvv5eWopOcnf112Q5ansYoba3rY5ItQZhn7UDKfZk8-EDhnin8tUFSahs="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
CLAUDE.md
56
CLAUDE.md
|
|
@ -10,6 +10,12 @@ The visual format of every generated PDF must match `base/reference/Oleg_Proskur
|
|||
|
||||
---
|
||||
|
||||
## Writing texts as Oleg
|
||||
|
||||
Any English text that goes out under Oleg's name — cover letter, application essay, form answer, outreach DM, CV bullet — must be written with the **`write-as-oleg` skill** (`.claude/skills/write-as-oleg/`). The skill consolidates his voice profile, writing process, and a paired example dictionary, and runs a mandatory deterministic linter (`lint-voice.mjs`) before delivery. Do not write such texts without loading this skill first.
|
||||
|
||||
---
|
||||
|
||||
## Browser access for logged-in sites
|
||||
|
||||
This project ships a Chrome session bound to Oleg's signed-in profile. Use it via the **`chrome-session` skill** (`.claude/skills/chrome-session/`). It is the right tool for: reading LinkedIn (feed, profiles, jobs), inspecting job descriptions and application forms on HR platforms (Greenhouse, Lever, Workable, Ashby, Workday, etc.), searching jobs, and any page where Oleg is already logged in.
|
||||
|
|
@ -45,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
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -57,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
|
||||
- Terminating sessions, blocking users, removing contacts
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
|
@ -79,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.
|
||||
|
||||
Quick orientation:
|
||||
- **Source of subscriptions**: Telegram folder "Jobs" (id=6), curated manually by Oleg. Never mirror its membership to a repo file.
|
||||
- **Pipeline**: `scripts/list_telegram_channels.py` → `scripts/fetch_telegram_jobs.py` (chainable via stdin).
|
||||
- **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). 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`).
|
||||
- **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.
|
||||
|
|
@ -276,6 +323,7 @@ The CSS in `templates/cv-style.css` already encodes these — when generating HT
|
|||
- **Language**: every artifact (HTML, MD, PDF, tracking rows, card notes) is in English. Always.
|
||||
- **Filenames**: snake_case for base CVs (`oleg_proskurin_<role>_cv.md`), kebab-case for tailored slugs. Tailored CV: `cv.md` inside `tailored/<slug>/`.
|
||||
- **Dates**: absolute (`2026-05-24`), never "last Thursday".
|
||||
- **Scripts**: all scripts in this project must be written in **Node.js** or **bash** — no Python. Use Node.js for anything with logic (parsing, transformation, linting, generation); use bash for simple shell glue (piping, file ops, invoking other tools).
|
||||
- **Don't edit the reference PDF.** It is the immutable visual anchor.
|
||||
- **Don't auto-regenerate PDFs** unless Oleg asks — show diffs first when content changes.
|
||||
- **Verify before claiming "done"**: after `pnpm pdf`, confirm the PDF exists and looks correct (open it, check page count). If you can't visually verify, say so.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# Telegram job-каналы — добавить
|
||||
|
||||
| Канал | id |
|
||||
|---|---|
|
||||
| ⚡ JavaScript Jobs \| React, Node.js & Web3 | `-1003759240721` |
|
||||
| Node.js jobs — NotificaJobs | `-1001519468289` |
|
||||
| LLM Jobs \| Вакансии по AI | `-1003091463924` |
|
||||
| JavaScript Вакансии Чат (Angular React Vue Node) | `-1001549843798` |
|
||||
| Serbia IT Jobs [RU/UA/ENG] | `-1001706774915` |
|
||||
| Javascript jobs — вакансии по фронтенду | `-1001247420798` |
|
||||
| Frontend Вакансии \| React/Vue/Angular | `-1003701733995` |
|
||||
| AI Вакансии \| Разработчики, LLM, GenAI, ML | `-1003828027446` |
|
||||
| 🇨🇾 IT-Jobs Cyprus | `-1001774387570` |
|
||||
| DevsUnite: Remote USA/EU | `-1003926588250` |
|
||||
|
|
@ -22,8 +22,13 @@
|
|||
|
||||
### Gate 1 — География / найм
|
||||
Можно нанять одним из:
|
||||
- contractor через Deel / Remote.com из Таиланда, ИЛИ
|
||||
- remote-employee для компании, нанимающей non-US резидентов.
|
||||
- global remote. employee для компаний, нанимающих non-US, non-UK, non-EU и тп кандидатов
|
||||
- contractor через Deel или другой подобны EOR из Таиланда
|
||||
- b2b контракт для удаленной работы
|
||||
- Русскоязычные команды или компании с русскоговорящими фаундерами, нанимающие удаленно, кроме РФ и РБ
|
||||
- Российские компании, нанимающие удаленно (не обязательно находиться в РФ)
|
||||
- Компании предлагающие релокацию, ПРИ УСЛОВИИ, что зарплата вы $10K в месяц
|
||||
- Временная контрактная работа, не привязанная к географии вообще. Типа mira, algery и подобные аггрегаторы
|
||||
|
||||
**Skip если:** location-locked / US-only / UK-only / EOR не покрывает Таиланд и нет contractor-опции.
|
||||
|
||||
|
|
@ -53,6 +58,7 @@ Senior / Staff / Tech Lead / опытный middle+.
|
|||
|
||||
**Skip если:** явный junior / mid без потолка роста.
|
||||
**НЕ skip если:** роль выглядит выше (Lead / Principal / Head) — это сигнал, что внутри такого человека нет, стоит пробовать, особенно если подача быстрая.
|
||||
**НЕ skip если:** роль выглядит как мидл, но при этом укладывается в требования по компенсации и дает интересный новый опыт: AI, работа с клиентами, прокачка английского
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -77,7 +83,7 @@ Russian-speaking scale-ups (Manychat, inDrive, Wheely, Joom, Adapty и подо
|
|||
|
||||
## 4. Обработка отсутствующих навыков
|
||||
|
||||
Один отсутствующий навык (Go, конкретный фреймворк, named-инструмент) — **не skip**.
|
||||
Один отсутствующий навык (Go, конкретный фреймворк, named-инструмент) — **не skip** если он не указан явно как MUST HAVE.
|
||||
|
||||
Но:
|
||||
- **Не флагать явно** в письме. Не строить конструкцию «вот тут у меня гэп / one thing to flag».
|
||||
|
|
@ -90,6 +96,8 @@ Russian-speaking scale-ups (Manychat, inDrive, Wheely, Joom, Adapty и подо
|
|||
|
||||
## 5. Research-протокол при анализе вакансии
|
||||
|
||||
5.0 Прежде чем изучать вакансию - необходмо проверить, подавались ли мы до этого на нее или в эту компанию. Для этого необходимо изучить доступные подключенные источники данных: трелло, телеграм группы где мы трекаем подачи, мой gmail и тп. Исходить из того, что доступно на момент проверки.
|
||||
|
||||
Дополнительно к чтению самой JD — рекомендуется (особенно для ролей, проходящих gate):
|
||||
|
||||
### 5.1. Поресёрчить саму компанию
|
||||
|
|
@ -122,12 +130,13 @@ Russian-speaking scale-ups (Manychat, inDrive, Wheely, Joom, Adapty и подо
|
|||
6. **«Stage / timeline не вписывается в 8 недель»** → влияет на приоритизацию последовательности, не на отсев. Проверять скорость interview-процесса по сигналам.
|
||||
7. **Comp ниже $8k но в floor / fallback** → рассматриваем, особенно если компания стратегическая или роль интересная.
|
||||
8. **Tech-stack mismatch с быстрым ramp** → лиди к подаче, ramp упоминаем честно (но не выпячиваем гэп — см. раздел 4).
|
||||
10. Вакансия не проходит триаж, но на нее мало подавались и матч есть хотя бы на 50% - плюс технически подача проста и может быть автоматизированна (easy apply на Linkedin, простая форма без дополниельных эссе, подача через DM в телеге) - пробуем податься в "быстром" режиме, чтобы потренировать прохождение следующих этапов если пройдем дальше, получить опыт и фидбэк. Однако это надо явно указать.
|
||||
|
||||
---
|
||||
|
||||
## 7. Operational flow при получении JD
|
||||
|
||||
1. Открыть и прочитать JD (при недоступности URL — halt, не реконструировать из памяти).
|
||||
1. Открыть и прочитать JD (при недоступности URL — halt, не реконструировать из памяти). Использовать web-fetch или открыть через браузер если там стоит защита от ботов. используй chrome-session скилл для доступа через chrome.
|
||||
2. Извлечь за первую минуту: уровень роли, must-have stack, AI-наличие (must / nice / нет), location & timezone constraints, comp-сигналы.
|
||||
3. Прогнать 4 hard-gate (раздел 2). Любой провален → skip с одним объективным критерием.
|
||||
4. Anti-pattern self-check (раздел 6): отсеваю ли по subjective критерию? Да → повысить класс.
|
||||
|
|
@ -140,6 +149,9 @@ Russian-speaking scale-ups (Manychat, inDrive, Wheely, Joom, Adapty и подо
|
|||
|
||||
## 8. Когда обновлять документ
|
||||
|
||||
Позиции на которые подались, сохранять в нашей Trello доске. Затем обновлять при:
|
||||
|
||||
- После первого callback / interview / rejection из каждого трека → калибровка proof-point и порогов.
|
||||
- При изменении comp-floor, таймзонного окна или target-зоны Олегом → правка немедленно.
|
||||
- Изменении статуса, отказа и тп.
|
||||
- Раз в 4 недели baseline pass — рынок и позиционирование меняются быстро.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
# Voice Dictionary — парные формулировки (модель → Олег)
|
||||
|
||||
> **Назначение:** словарь для эмуляции голоса Олега. Слева — как написала бы модель (причёсанный натив или generic-дефолт). Справа — как написал/оставил Олег. Цель при генерации текста под его именем: узнавать левую колонку как «не его» и тянуться к правой.
|
||||
>
|
||||
> **Источник:** реальные парные правки из чатов проекта (Lucky Hunter / XPN Q1-Q2, Plata, Salmon, Miratech, Faye, Web Engineer/Robert, Ecommpay), плюс эталоны из voice-анализа. Каждая строка — фактическая пара, не выдуманная.
|
||||
>
|
||||
> **Как читать пометку «он принял» / «он откинул»:** «откинул» = я предложил левую, он вернул правую (чистейший сигнал голоса). «он принял» = он написал правую, я предложил левую, он согласился — но правая всё равно его естественная рука, и для эмуляции цель — она.
|
||||
>
|
||||
> Дополняет `voice-profile-oleg.md` (там правила), здесь — конкретные пары.
|
||||
|
||||
---
|
||||
|
||||
## 1. Глаголы действия: я тяну к «native verb», он — к простому/системному
|
||||
|
||||
| Модель (левая) | Олег (правая) | Что меняется |
|
||||
|---|---|---|
|
||||
| I've built this pattern in several places | I've implemented such scenarious in serveral places | он берёт `implemented` + указатель `such scenarious`, не «built this pattern» |
|
||||
| picks the best-matching component by score | select a best matching component based on a score system | `based on a score system` вместо `by score`; постпозиция вместо компрессии |
|
||||
| fills its props from a compact component schema | fills props based on compact component schema | дроп артикля (`props`, `compact component schema`) + `based on` |
|
||||
| I built a harness around these steps | I also created a harness around these steps | `created`, не `built`; вводное `also` |
|
||||
| the chatbot that helps users create and modify page nodes | the chatbot that assist users in creating/modifying page nodes | `assist` (его опорное слово), drop -s, `in creating/modifying` |
|
||||
|
||||
**Вывод:** не апгрейдить его глаголы до «сочного» нативного. `implemented / created / assist / orchestrates` — его база. Декоративные `picks / crafts / leverages` — чужие.
|
||||
|
||||
---
|
||||
|
||||
## 2. Постпозиция «from / based on» вместо компрессии
|
||||
|
||||
| Модель (левая) | Олег (правая) | Что меняется |
|
||||
|---|---|---|
|
||||
| autonomously selecting components via a scored, filtered candidate pool | autonomously selecting components from a registry of 200+ from a scored, filtered candidate pool | двойное `from ... from ...` подряд — он так пишет, я бы свернул |
|
||||
| My relevant experience comes from PrimeUI | I see the relevant experience in what I've been doing at PrimeUI | **он откинул мою.** Калька с русского, оставил сознательно (русскоязычная команда = signal «свой») |
|
||||
|
||||
**Вывод:** обоснование и источник он навешивает в хвост через `from` / `based on` / `in what I've been doing`. Не переворачивать в «X comes from Y».
|
||||
|
||||
---
|
||||
|
||||
## 3. Длинные цепочки через запятую, без точки с запятой
|
||||
|
||||
| Модель (левая) | Олег (правая) | Что меняется |
|
||||
|---|---|---|
|
||||
| Each step reads the previously generated content, picks the best-matching component by score, **and** fills its props from a compact component schema. | Each step reads the previously generated content, select a best matching component based on a score system, fills props based on compact component schema. | я добавляю `and` перед последним блоком и причёсываю согласование; он нанизывает три глагольных блока через запятую без `and`, с плавающим -s |
|
||||
| it orchestrates around 20 agent tools **that access** the DB, UX layer, component registry, **and so on** | it orchestrates around 20 agent tools accessing DB, UX, component registry etc | он: `accessing` (не `that access`), `etc` (не `and so on`), drop артиклей в списке |
|
||||
|
||||
**Вывод:** не разбивать его комма-цепочки на отдельные предложения и не вставлять `;`. `etc` он любит больше, чем `and so on`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Самохарактеристики → двоеточие-заголовок
|
||||
|
||||
| Модель (левая) | Олег (правая) | Что меняется |
|
||||
|---|---|---|
|
||||
| I'm a direct match | My day-to-day stack: [перечисление] | **он откинул мою.** Не заявляет «я подхожу», даёт фактам говорить через двоеточие-заголовок |
|
||||
| My day-to-day stack matches yours | [конкретные claims по годам на каждую технологию] | **он откинул мою** (Miratech): «слишком casual», заменил на годы-на-стек |
|
||||
| The core of the role is the work I've been doing | [открывашка сразу в свою работу, не пересказ JD] | **он откинул мою** (Miratech): я пересказал JD обратно работодателю — он ведёт сразу со своего |
|
||||
|
||||
**Вывод:** он не пишет «I'm a great fit / matches yours / the role is what I do». Вместо самооценки — `My day-to-day stack:` + факты. Письмо НЕ открывает пересказом вакансии.
|
||||
|
||||
---
|
||||
|
||||
## 5. «Поэтичное» / гладкое → короткое конкретное
|
||||
|
||||
| Модель (левая) | Олег (правая) | Что меняется |
|
||||
|---|---|---|
|
||||
| Multi-tenant and white-label is not new to me | [короткие конкретные предложения про Tipico: mobile app, web portal, casino team, betting widgets как consumers одного бэкенда] | **он откинул мою** (Miratech): «слишком поэтично», заменил на конкретику с named-сущностями |
|
||||
| Built the AI layer containing some independent AI flows | [назвать 4 потока: content writing, page generation, chatbot, sitemap generation] | vague `some ... flows` он сам потом раскрыл в число — но изначально склонен к vague, тут я подталкивал к конкретике |
|
||||
| Happy to dig into specifics on a call | Open to chat if it's a fit. | его закрывашка — короче и суше, без «happy to dig» |
|
||||
|
||||
**Вывод:** где я сглаживаю в обтекаемую фразу — он рубит на короткие предложения с именами систем. Закрывашка короткая: `Open to chat if it's a fit.`
|
||||
|
||||
---
|
||||
|
||||
## 6. Связки и переходы
|
||||
|
||||
| Модель (левая) | Олег (правая) | Что меняется |
|
||||
|---|---|---|
|
||||
| So, the content team ran experiments... | Content team ran experiments... | **он откинул `So`** в начале связки. Принцип: связь из содержания, не из слова |
|
||||
| Furthermore / Moreover / That said | [нет связки, новое предложение / двоеточие] | он не использует gluing-переходы в начале |
|
||||
| — (em-dash на вставке) | : или , или отдельное предложение | em-dash на parenthetical он не ставит, я ставлю |
|
||||
|
||||
---
|
||||
|
||||
## 7. Грамматические подписи (его рука — сохранять дозированно, НЕ «исправлять»)
|
||||
|
||||
Это правая колонка как есть — то, что я по привычке вычищаю, а для его голоса часть надо оставлять:
|
||||
|
||||
| Модель причёсывает в → | Олег пишет → | Статус |
|
||||
|---|---|---|
|
||||
| PrimeUI is **an** AI-based platform | PrimeUI is AI based platform | drop артикля перед составным сущ. — **оставлять дозированно** |
|
||||
| with a focus on **high** design quality | with a focus on hi design quality | `hi`→high — фонетика на скорости; в followup ок, **в cover letter чистим** |
|
||||
| I've worked on it from scratch **for over a year** | I've been working on that project from scratch during more than one year | `during more than one year` — калька, его ритм; **оставлять в неформальном** |
|
||||
| picks the best-matching component (3 л. -s ровно) | reads... select... fills (плавающий -s) | согласование плавает внутри фразы — **дозированно ок** |
|
||||
|
||||
---
|
||||
|
||||
## 8. Чистые ошибки (правлю всегда — это НЕ голос, это опечатки)
|
||||
|
||||
Эти правки он принимал без споров. Они отделяют «голос» (сохранять) от «брак» (чинить):
|
||||
|
||||
| Олег написал | Правка | Тип |
|
||||
|---|---|---|
|
||||
| splitted to: | split into: | irregular verb, грамматика |
|
||||
| serveral / scenarious | several / scenarios | опечатка |
|
||||
| exelent | excellent | опечатка |
|
||||
| clients communication | client communication | мн.ч. в атрибутиве |
|
||||
| regulator approvements was | regulator approval was a critical step | несуществующее слово + согласование |
|
||||
| has signed Deel account | have a Deel account (W-8BEN on file) | глагол/артикль |
|
||||
| tested for a compatibility | tested for compatibility | лишний артикль |
|
||||
| Senior AI Engineer . (пробел перед точкой) | Senior AI Engineer. | типографика |
|
||||
|
||||
**Граница:** `splitted`, `exelent`, `approvements` — чинить (брак). `AI based platform`, `assist users`, `I see the relevant experience` — НЕ трогать (голос).
|
||||
|
||||
---
|
||||
|
||||
## 9. Опорные слова правой колонки (тянуться к ним)
|
||||
|
||||
Из его текстов, не из моих: **harness, orchestrates, registry, flow, loop, score / scored, from scratch, assist, such / these / that** (указатели), **etc**, **based on**.
|
||||
|
||||
Дефолты модели, которых он не использует: leverage, robust, seamless, crafts, dig into, a direct match, So/Furthermore/Moreover в начале, em-dash на вставке.
|
||||
|
||||
---
|
||||
|
||||
## 10. Эталон целиком — одна пара для калибровки ритма
|
||||
|
||||
**Моя версия (left, причёсанный натив):**
|
||||
> PrimeUI is an AI-based platform that generates production-level codebases for client websites, with a strong focus on the design quality of components and pages — I've worked on it from scratch for over a year. The core of page generation is a planner agent that iterates section by section across a page, autonomously selecting components from a registry of 200+ via a scored, filtered candidate pool. Each step reads the previously generated content, picks the best-matching component by score, and fills its props from a compact component schema. I built a harness around these steps with validation and a retry system.
|
||||
|
||||
**Его версия (right, target voice):**
|
||||
> PrimeUI is AI based platform for generation production level codebases for clients websites with a focus on hi design quality of components and pages. I've been working on that project from scratch during more than one year. The core of pages generation is a planner agent that iterates section by section across a page, autonomously selecting components from a registry of 200+ from a scored, filtered candidate pool. Each step reads the previously generated content, select a best matching component based on a score system, fills props based on compact component schema. I also created a harness around these steps with validation and retry system.
|
||||
|
||||
Разница на одном абзаце: я ставлю артикли, em-dash, `and` перед хвостом, апгрейжу `created→built` и `assist→helps`, сворачиваю двойное `from`. Он держит drop-артикли, комма-цепочку, постпозицию `based on`, плавающий -s, `from scratch during more than one year`.
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
# 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:
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
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_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`.
|
||||
|
|
@ -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
|
||||
set -a && source .env && set +a && \
|
||||
/projects/my-utils/telegram/.venv/bin/telegram-mcp-generate-session
|
||||
```
|
||||
|
||||
### 3a. usulsu — main account
|
||||
|
||||
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).
|
||||
- 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
|
||||
# from the cv-2026 project root
|
||||
set -a && source .env && set +a && \
|
||||
npx -y add-mcp "/projects/my-utils/telegram/.venv/bin/telegram-mcp" \
|
||||
-n telegram \
|
||||
--env 'TELEGRAM_API_ID=${TELEGRAM_API_ID}' \
|
||||
--env 'TELEGRAM_API_HASH=${TELEGRAM_API_HASH}' \
|
||||
--env 'TELEGRAM_SESSION_STRING=${TELEGRAM_SESSION_STRING}' \
|
||||
-a claude-code -a claude-desktop \
|
||||
-g -y
|
||||
|
||||
# fix the literal-placeholder issue — substitute actual values into both configs
|
||||
set -a && source .env && set +a && \
|
||||
for CFG in ~/.config/Claude/claude_desktop_config.json ~/.claude.json; do
|
||||
jq --arg id "$TELEGRAM_API_ID" --arg hash "$TELEGRAM_API_HASH" --arg sess "$TELEGRAM_SESSION_STRING" \
|
||||
'.mcpServers.telegram.env.TELEGRAM_API_ID = $id
|
||||
| .mcpServers.telegram.env.TELEGRAM_API_HASH = $hash
|
||||
| .mcpServers.telegram.env.TELEGRAM_SESSION_STRING = $sess' \
|
||||
"$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
|
||||
done
|
||||
```json
|
||||
"telegram-usulpro": {
|
||||
"command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"TELEGRAM_API_ID": "<id>",
|
||||
"TELEGRAM_API_HASH": "<hash>",
|
||||
"TELEGRAM_SESSION_STRING": "<usulsu session string>"
|
||||
}
|
||||
},
|
||||
"telegram-helper": {
|
||||
"command": "/projects/my-utils/telegram/.venv/bin/telegram-mcp",
|
||||
"args": [],
|
||||
"env": {
|
||||
"TELEGRAM_API_ID": "<id>",
|
||||
"TELEGRAM_API_HASH": "<hash>",
|
||||
"TELEGRAM_SESSION_STRING": "<samuishechka session string>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 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
|
||||
# usulsu
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -14,6 +14,7 @@ channels for keyword decisions.
|
|||
Inputs:
|
||||
- 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)
|
||||
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_channels.json — per-channel curated metadata (lang, priority)
|
||||
and filter (include/exclude). See tracking/CLAUDE.md.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ Output: JSON array of usernames (falling back to numeric id for private channels
|
|||
|
||||
Pipe directly into the fetch script:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
#!/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,
|
||||
# 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
|
||||
|
||||
ACCOUNT="${1:-}" # "helper" for samuishechka, empty for usulsu
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
TELEGRAM_DIR="/projects/my-utils/telegram"
|
||||
|
||||
|
|
@ -21,5 +31,14 @@ if [ -z "${TELEGRAM_API_ID:-}" ] || [ -z "${TELEGRAM_API_HASH:-}" ]; then
|
|||
exit 1
|
||||
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"
|
||||
exec .venv/bin/python session_string_generator.py --qr "$@"
|
||||
|
|
|
|||
|
|
@ -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 (`-`).
|
||||
|
||||
**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
|
||||
|
||||
- `DEFAULT_LOOKBACK_DAYS = 30` — first-time lookback window for new channels (no cursor yet).
|
||||
|
|
|
|||
|
|
@ -8,3 +8,6 @@ Statuses: `applied` → `screen` → `interview` → `offer` / `rejected` / `wit
|
|||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 2026-MM-DD | Example Co | Senior FE Engineer | LinkedIn / referral / direct | `tailored/example-co/cv.pdf` | applied | 2026-MM-DD | Link to JD, recruiter name |
|
||||
| 2026-05-30 | Hostinger (Horizons) | Backend Software Engineer \| Node.js \| Remote | direct (Ashby) | `tailored/hostinger-horizons-backend/oleg_proskurin_senior_frontend_backend_cv.pdf` | to apply | 2026-05-30 | JD: https://jobs.ashbyhq.com/hostinger/8e4d93c1-ce2a-4e2a-a3af-d477222ace0f · EM: Tadas Paplauskas · Open questions: location (Poland-only?) + comp range + UoP vs B2B |
|
||||
| 2026-06-07 | LI.FI | Senior Full Stack Engineer (Checkout) | TG @notificaJobs_nodeJS | TBD (CV-B) | todo | 2026-06-07 | JD: https://notificajobs.com/offers/6a120b521a5838ce8c7d74e7 · Track B · EOR/contractor available · Trello: https://trello.com/c/NJCwHda1 |
|
||||
| 2026-06-07 | Grafana Labs | Senior Software Engineer, Application Core Services | TG @notificaJobs_nodeJS | TBD (CV-A) | todo | 2026-06-07 | JD: notificajobs.com · Track A · 50+ countries remote-first; Thailand TBC · Trello: https://trello.com/c/veG07e3Q |
|
||||
| 2026-06-07 | G-P (Globalization Partners) | Senior Software Engineer | TG @notificaJobs_nodeJS | TBD (CV-B) | todo | 2026-06-07 | JD: globalization-partners.com/careers · Track B · they ARE the EOR — Thailand covered · Trello: https://trello.com/c/wf4G0Wjv |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
{
|
||||
"JS_RemoteJobs": {
|
||||
"lang": "en",
|
||||
"priority": "p2",
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", " rust ", " java "]
|
||||
},
|
||||
"notificaJobs_nodeJS": {
|
||||
"lang": "en",
|
||||
"priority": "p1",
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", " rust "]
|
||||
},
|
||||
|
||||
"jaabz_com": {
|
||||
"lang": "en",
|
||||
"priority": "p1",
|
||||
|
|
@ -38,6 +49,28 @@
|
|||
"include": [["typescript", "javascript", " react", "node.js", "nodejs", "fullstack", "full-stack", "ai engineer", "tech lead", "techlead"]],
|
||||
"exclude": ["mental health", "marketing", "sales", "designer", "manager", "recruit", "kafka", "golang", "kotlin", "android", "swift"]
|
||||
},
|
||||
"ai_dev_job": {
|
||||
"lang": "ru",
|
||||
"priority": "p2"
|
||||
},
|
||||
"jsdevjob": {
|
||||
"lang": "ru",
|
||||
"priority": "p2",
|
||||
"include": [["react", "javascript", "typescript", "frontend", "fullstack", "full-stack", " llm", " ai engineer"]],
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", "курс ", "вебинар", "конкурс", "#резюме", "#resume"]
|
||||
},
|
||||
"itdevjobs": {
|
||||
"lang": "mixed",
|
||||
"priority": "p2",
|
||||
"include": [["#remote"], ["#typescript", "#javascript", "#react", "#fullstack"]],
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", "#резюме", "#resume", "#cv"]
|
||||
},
|
||||
"serbia_jobs": {
|
||||
"lang": "en",
|
||||
"priority": "p2",
|
||||
"include": [["#vacancy", "#вакансия"], ["frontend", "fullstack", "full-stack", "javascript", "typescript", " react", "node.js", "nodejs", "ai engineer", " llm", "tech lead"]],
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", " java ", "#cv", "#resume", "#резюме", "#ищуработу", "sales manager", "marketing"]
|
||||
},
|
||||
"javascript_jobs_feed": {
|
||||
"lang": "ru",
|
||||
"priority": "p2"
|
||||
|
|
@ -79,6 +112,18 @@
|
|||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", "#резюме", "#resume", "#cv", "#ищуработу"]
|
||||
},
|
||||
|
||||
"devsunite": {
|
||||
"lang": "en",
|
||||
"priority": "p3",
|
||||
"include": [["javascript", "typescript", " react", "node.js", "nodejs", "fullstack", "full-stack"]],
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", " java ", " rust ", "php", "devops", "qa", "sales", "marketing", "designer", "cloud engineer", "aws engineer"]
|
||||
},
|
||||
"it_jobs_cyprus": {
|
||||
"lang": "ru",
|
||||
"priority": "p3",
|
||||
"include": [["#vacancy", "#вакансия"], ["javascript", "typescript", " react", "node.js", "nodejs", "fullstack", "full-stack", "frontend", "ai engineer", " llm"]],
|
||||
"exclude": ["kafka", "golang", "kotlin", "android", "swift", " java ", "#cv", "#resume", "#резюме", "#ищуработу", "igaming", "iGaming", "sales", "marketing", "devops", "manual test", "qa engineer"]
|
||||
},
|
||||
"devops_jobs": {
|
||||
"lang": "ru",
|
||||
"priority": "p3",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue