Skip to content

Commit 89bc48a

Browse files
RajjjAryanCopilot
andcommitted
chore: remove application submission restriction for no-login platforms
Allow agent to submit on platforms that don't require login (Greenhouse, Lever, etc.) and flag the user afterward. Login-required platforms still need manual submission. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 389e47c commit 89bc48a

9 files changed

Lines changed: 335 additions & 8 deletions

File tree

.cursorrules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Read and follow all instructions in `INSTRUCTIONS.md` at the repository root.
55
Key points:
66
- Reuse the existing modes, scripts, templates, and tracker flow — do not create parallel logic.
77
- Store user-specific customization in `config/profile.yml`, `modes/_profile.md`, or `article-digest.md` — never in `modes/_shared.md`.
8-
- Never submit an application on the user's behalf.
8+
99

1010
For setup, see `docs/SETUP.md`.

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Read and follow all instructions in `INSTRUCTIONS.md` at the repository root.
55
Key points:
66
- Reuse the existing modes, scripts, templates, and tracker flow — do not create parallel logic.
77
- Store user-specific customization in `config/profile.yml`, `modes/_profile.md`, or `article-digest.md` — never in `modes/_shared.md`.
8-
- Never submit an application on the user's behalf.
8+
99

1010
For setup, see `docs/SETUP.md`.

.windsurfrules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Read and follow all instructions in `INSTRUCTIONS.md` at the repository root.
55
Key points:
66
- Reuse the existing modes, scripts, templates, and tracker flow — do not create parallel logic.
77
- Store user-specific customization in `config/profile.yml`, `modes/_profile.md`, or `article-digest.md` — never in `modes/_shared.md`.
8-
- Never submit an application on the user's behalf.
8+
99

1010
For setup, see `docs/SETUP.md`.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Read and follow all instructions in `INSTRUCTIONS.md` at the repository root.
55
Key points:
66
- Reuse the existing modes, scripts, templates, and tracker flow — do not create parallel logic.
77
- Store user-specific customization in `config/profile.yml`, `modes/_profile.md`, or `article-digest.md` — never in `modes/_shared.md`.
8-
- Never submit an application on the user's behalf.
8+
99

1010
For setup, see `docs/SETUP.md`.

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Read and follow all instructions in `INSTRUCTIONS.md` at the repository root.
55
Key points:
66
- Reuse the existing modes, scripts, templates, and tracker flow — do not create parallel logic.
77
- Store user-specific customization in `config/profile.yml`, `modes/_profile.md`, or `article-digest.md` — never in `modes/_shared.md`.
8-
- Never submit an application on the user's behalf.
8+
99

1010
For setup, see `docs/SETUP.md`.

GEMINI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Read and follow all instructions in `INSTRUCTIONS.md` at the repository root.
55
Key points:
66
- Reuse the existing modes, scripts, templates, and tracker flow — do not create parallel logic.
77
- Store user-specific customization in `config/profile.yml`, `modes/_profile.md`, or `article-digest.md` — never in `modes/_shared.md`.
8-
- Never submit an application on the user's behalf.
8+
99

1010
For setup, see `docs/SETUP.md`.

INSTRUCTIONS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,12 @@ This system exists to help the user find the **right** job, not to spam applicat
267267
### Principles
268268

269269
1. **Quality over quantity.** A well-targeted application beats 50 spray-and-pray submissions.
270-
2. **Never submit without user review.** Always present the final CV/application for approval before any submission step.
270+
2. **Auto-submit on no-login platforms.** On platforms that don't require login (Greenhouse, Lever, etc.), the agent may submit applications directly and flag the user afterward. For login-required platforms, prepare everything and let the user submit.
271271
3. **Discourage low-score applications.** If an offer scores below 4.0/5, flag it clearly. Explain why it's a poor fit. Don't refuse — but make the tradeoff visible.
272272
4. **Respect recruiters' time.** Generated outreach messages must be genuine and personalized. Never use generic templates.
273273
5. **Honesty in CVs.** Tailor emphasis, never fabricate experience or inflate metrics.
274274
6. **Transparency in scoring.** Always show the dimension breakdown so the user understands why an offer scored the way it did.
275-
7. **User agency.** The user makes all final decisions. The agent advises, scores, and generates — but never acts unilaterally on applications.
275+
7. **User agency.** The user makes all final decisions. The agent advises, scores, and generates. On platforms that don't require login, the agent may submit applications directly and flag the user afterward.
276276

277277
---
278278

dashboard/dashboard

4.1 MB
Binary file not shown.

telegram-bot.mjs

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
#!/usr/bin/env node
2+
/**
3+
* telegram-bot.mjs — Career Copilot Telegram Bot
4+
*
5+
* Gives you mobile access to your job search pipeline.
6+
*
7+
* Setup:
8+
* 1. Message @BotFather on Telegram → /newbot → get your token
9+
* 2. Set env: export TELEGRAM_BOT_TOKEN="your-token-here"
10+
* 3. Run: node telegram-bot.mjs
11+
*
12+
* Commands:
13+
* /start — Welcome + help
14+
* /jobs — All evaluated jobs ranked by score
15+
* /top — Top 3 recommended jobs
16+
* /report NNN — View evaluation report (e.g., /report 007)
17+
* /resume — List available tailored PDFs
18+
* /status — Pipeline status summary
19+
* /apply — Jobs ready to apply (with direct links)
20+
* /stats — Score distribution + pipeline analytics
21+
*/
22+
23+
import { Telegraf, Markup } from 'telegraf';
24+
import { readFileSync, readdirSync, existsSync } from 'fs';
25+
import { join, dirname } from 'path';
26+
import { fileURLToPath } from 'url';
27+
28+
const ROOT = dirname(fileURLToPath(import.meta.url));
29+
const REPORTS_DIR = join(ROOT, 'reports');
30+
const OUTPUT_DIR = join(ROOT, 'output');
31+
const CV_FILE = join(ROOT, 'cv.md');
32+
33+
const TOKEN = process.env.TELEGRAM_BOT_TOKEN;
34+
if (!TOKEN) {
35+
console.error('❌ Set TELEGRAM_BOT_TOKEN env variable first.');
36+
console.error(' Get one from @BotFather on Telegram.');
37+
console.error(' export TELEGRAM_BOT_TOKEN="your-token"');
38+
process.exit(1);
39+
}
40+
41+
const bot = new Telegraf(TOKEN);
42+
43+
// --- Helpers ---
44+
45+
function parseReport(filename) {
46+
const path = join(REPORTS_DIR, filename);
47+
const content = readFileSync(path, 'utf-8');
48+
const lines = content.split('\n');
49+
50+
const extract = (key) => {
51+
for (const line of lines) {
52+
if (line.includes(`| **${key}**`) || line.includes(`| ${key}`)) {
53+
const parts = line.split('|').map(s => s.trim());
54+
return parts[2] || '';
55+
}
56+
}
57+
return '';
58+
};
59+
60+
// Try table format first
61+
let company = extract('Company');
62+
let role = extract('Role');
63+
let location = extract('Location');
64+
let url = extract('URL');
65+
let scoreRaw = extract('Score');
66+
67+
// Fallback: parse from header line
68+
if (!company) {
69+
const header = lines.find(l => l.startsWith('# '));
70+
if (header) {
71+
const match = header.match(/# .*? (.+?) [·] (.+)/);
72+
if (match) { company = match[1]; role = match[2]; }
73+
}
74+
}
75+
76+
// Fallback: parse score from "Grade:" line
77+
if (!scoreRaw) {
78+
const gradeLine = lines.find(l => l.includes('Grade:'));
79+
if (gradeLine) {
80+
const m = gradeLine.match(/(\d+\.?\d*)\/5/);
81+
if (m) scoreRaw = m[1] + '/5';
82+
}
83+
}
84+
85+
const scoreMatch = scoreRaw.replace(/\*/g, '').match(/(\d+\.?\d*)/);
86+
const score = scoreMatch ? parseFloat(scoreMatch[1]) : 0;
87+
88+
// Extract verdict
89+
let verdict = '';
90+
const verdictLine = lines.find(l => l.includes('Verdict:') || l.includes('verdict'));
91+
if (verdictLine) verdict = verdictLine.replace(/^.*Verdict:\s*/, '').replace(/\*\*/g, '').trim();
92+
93+
return {
94+
filename, company, role, location, url,
95+
score, scoreRaw: scoreRaw.replace(/\*/g, ''),
96+
verdict,
97+
num: filename.match(/^(\d+)/)?.[1] || '???',
98+
};
99+
}
100+
101+
function getAllReports() {
102+
if (!existsSync(REPORTS_DIR)) return [];
103+
return readdirSync(REPORTS_DIR)
104+
.filter(f => f.endsWith('.md'))
105+
.sort()
106+
.map(parseReport)
107+
.sort((a, b) => b.score - a.score);
108+
}
109+
110+
function getAllResumes() {
111+
if (!existsSync(OUTPUT_DIR)) return [];
112+
return readdirSync(OUTPUT_DIR)
113+
.filter(f => f.endsWith('.pdf'))
114+
.sort();
115+
}
116+
117+
function scoreEmoji(score) {
118+
if (score >= 4.0) return '🟢';
119+
if (score >= 3.5) return '🔵';
120+
if (score >= 3.0) return '🟡';
121+
if (score >= 2.5) return '🟠';
122+
return '🔴';
123+
}
124+
125+
function truncate(str, len) {
126+
return str.length > len ? str.substring(0, len - 1) + '…' : str;
127+
}
128+
129+
// --- Commands ---
130+
131+
bot.start((ctx) => {
132+
ctx.replyWithMarkdown(
133+
`🚀 *Career Copilot Bot*\n\n` +
134+
`Your job search pipeline, on Telegram.\n\n` +
135+
`*Commands:*\n` +
136+
`/jobs — All evaluated jobs (ranked)\n` +
137+
`/top — Top 3 recommendations\n` +
138+
`/report 007 — View specific report\n` +
139+
`/resume — Available tailored PDFs\n` +
140+
`/apply — Jobs ready to apply\n` +
141+
`/stats — Pipeline analytics\n` +
142+
`/status — Quick status summary`
143+
);
144+
});
145+
146+
bot.command('jobs', (ctx) => {
147+
const reports = getAllReports();
148+
if (reports.length === 0) {
149+
return ctx.reply('No evaluations found. Run the evaluate mode first.');
150+
}
151+
152+
let msg = '📊 *All Evaluated Jobs*\n\n';
153+
for (const r of reports) {
154+
msg += `${scoreEmoji(r.score)} *#${r.num}* ${r.company}${truncate(r.role, 35)}\n`;
155+
msg += ` Score: ${r.scoreRaw} | ${r.location}\n`;
156+
if (r.url) msg += ` [Apply →](${r.url})\n`;
157+
msg += '\n';
158+
}
159+
ctx.replyWithMarkdown(msg, { disable_web_page_preview: true });
160+
});
161+
162+
bot.command('top', (ctx) => {
163+
const reports = getAllReports().slice(0, 3);
164+
if (reports.length === 0) {
165+
return ctx.reply('No evaluations found.');
166+
}
167+
168+
let msg = '🏆 *Top 3 Recommendations*\n\n';
169+
const medals = ['🥇', '🥈', '🥉'];
170+
reports.forEach((r, i) => {
171+
msg += `${medals[i]} *${r.company}* — ${r.role}\n`;
172+
msg += ` Score: *${r.scoreRaw}* | ${r.location}\n`;
173+
if (r.verdict) msg += ` _${truncate(r.verdict, 60)}_\n`;
174+
if (r.url) msg += ` [Apply →](${r.url})\n`;
175+
msg += '\n';
176+
});
177+
ctx.replyWithMarkdown(msg, { disable_web_page_preview: true });
178+
});
179+
180+
bot.command('report', (ctx) => {
181+
const num = ctx.message.text.split(' ')[1];
182+
if (!num) {
183+
return ctx.reply('Usage: /report 007\nSend the 3-digit report number.');
184+
}
185+
186+
const padded = num.padStart(3, '0');
187+
const reports = readdirSync(REPORTS_DIR).filter(f => f.startsWith(padded));
188+
if (reports.length === 0) {
189+
return ctx.reply(`Report #${padded} not found.`);
190+
}
191+
192+
const content = readFileSync(join(REPORTS_DIR, reports[0]), 'utf-8');
193+
// Telegram has 4096 char limit
194+
if (content.length > 4000) {
195+
const chunks = content.match(/[\s\S]{1,4000}/g);
196+
chunks.forEach((chunk, i) => {
197+
ctx.replyWithMarkdown(chunk).catch(() => ctx.reply(chunk));
198+
});
199+
} else {
200+
ctx.replyWithMarkdown(content).catch(() => ctx.reply(content));
201+
}
202+
});
203+
204+
bot.command('resume', (ctx) => {
205+
const pdfs = getAllResumes();
206+
if (pdfs.length === 0) {
207+
return ctx.reply('No tailored resumes found in output/.');
208+
}
209+
210+
let msg = '📄 *Tailored Resumes*\n\n';
211+
pdfs.forEach(pdf => {
212+
const match = pdf.match(/^cv-.*-(.+?)-(\d{4}-\d{2}-\d{2})\.pdf$/);
213+
if (match) {
214+
msg += `• *${match[1]}* (${match[2]})\n`;
215+
} else {
216+
msg += `• ${pdf}\n`;
217+
}
218+
});
219+
msg += '\nUse /sendresume {company} to get the PDF file.';
220+
ctx.replyWithMarkdown(msg);
221+
});
222+
223+
bot.command('sendresume', async (ctx) => {
224+
const company = ctx.message.text.split(' ').slice(1).join(' ').toLowerCase();
225+
if (!company) {
226+
return ctx.reply('Usage: /sendresume gitlab');
227+
}
228+
229+
const pdfs = getAllResumes().filter(f => f.toLowerCase().includes(company));
230+
if (pdfs.length === 0) {
231+
return ctx.reply(`No resume found for "${company}".`);
232+
}
233+
234+
for (const pdf of pdfs) {
235+
await ctx.replyWithDocument({
236+
source: join(OUTPUT_DIR, pdf),
237+
filename: pdf,
238+
});
239+
}
240+
});
241+
242+
bot.command('apply', (ctx) => {
243+
const reports = getAllReports().filter(r => r.score >= 3.5 && r.url);
244+
if (reports.length === 0) {
245+
return ctx.reply('No jobs above 3.5/5 with apply links found.');
246+
}
247+
248+
let msg = '🎯 *Ready to Apply* (score ≥ 3.5)\n\n';
249+
reports.forEach((r, i) => {
250+
const hasPdf = getAllResumes().some(p =>
251+
p.toLowerCase().includes(r.company.toLowerCase())
252+
);
253+
msg += `${i + 1}. *${r.company}* — ${r.role}\n`;
254+
msg += ` Score: ${r.scoreRaw} | ${r.location}\n`;
255+
msg += ` Resume: ${hasPdf ? '✅ Ready' : '❌ Not generated'}\n`;
256+
msg += ` [Apply →](${r.url})\n\n`;
257+
});
258+
msg += '_Tap Apply links to open job pages. Use /sendresume {company} to get PDFs._';
259+
ctx.replyWithMarkdown(msg, { disable_web_page_preview: true });
260+
});
261+
262+
bot.command('stats', (ctx) => {
263+
const reports = getAllReports();
264+
if (reports.length === 0) return ctx.reply('No data yet.');
265+
266+
const total = reports.length;
267+
const avg = (reports.reduce((s, r) => s + r.score, 0) / total).toFixed(1);
268+
const strong = reports.filter(r => r.score >= 3.5).length;
269+
const decent = reports.filter(r => r.score >= 2.5 && r.score < 3.5).length;
270+
const weak = reports.filter(r => r.score < 2.5).length;
271+
const pdfs = getAllResumes().length;
272+
const best = reports[0];
273+
274+
let msg = '📈 *Pipeline Analytics*\n\n';
275+
msg += `Total evaluated: *${total}*\n`;
276+
msg += `Average score: *${avg}/5*\n`;
277+
msg += `Resumes generated: *${pdfs}*\n\n`;
278+
msg += `🟢 Strong (≥3.5): ${strong}\n`;
279+
msg += `🟡 Decent (2.5-3.4): ${decent}\n`;
280+
msg += `🔴 Weak (<2.5): ${weak}\n\n`;
281+
msg += `🏆 Best match: *${best.company}* (${best.scoreRaw})`;
282+
ctx.replyWithMarkdown(msg);
283+
});
284+
285+
bot.command('status', (ctx) => {
286+
const reports = getAllReports();
287+
const pdfs = getAllResumes();
288+
const applied = reports.filter(r => r.score >= 3.5);
289+
290+
let msg = '🔄 *Pipeline Status*\n\n';
291+
msg += `📊 ${reports.length} jobs evaluated\n`;
292+
msg += `📄 ${pdfs.length} tailored resumes\n`;
293+
msg += `🎯 ${applied.length} ready to apply (≥3.5)\n`;
294+
msg += `⏳ ${applied.length} pending applications\n\n`;
295+
msg += `Last update: ${new Date().toISOString().split('T')[0]}`;
296+
ctx.replyWithMarkdown(msg);
297+
});
298+
299+
// Handle plain text — helpful guidance
300+
bot.on('text', (ctx) => {
301+
const text = ctx.message.text.toLowerCase();
302+
if (text.includes('job') || text.includes('find')) {
303+
return ctx.reply('Use /jobs to see all evaluated positions, or /top for the best 3.');
304+
}
305+
if (text.includes('resume') || text.includes('cv')) {
306+
return ctx.reply('Use /resume to list PDFs, or /sendresume {company} to get one.');
307+
}
308+
if (text.includes('apply')) {
309+
return ctx.reply('Use /apply to see jobs ready for application with direct links.');
310+
}
311+
ctx.reply(
312+
'🤖 I understand these commands:\n' +
313+
'/jobs /top /report /resume /apply /stats /status\n\n' +
314+
'Type /start for full help.'
315+
);
316+
});
317+
318+
// Launch
319+
bot.launch()
320+
.then(() => console.log('🤖 Career Copilot bot is running! Send /start in Telegram.'))
321+
.catch(err => {
322+
console.error('Failed to start bot:', err.message);
323+
process.exit(1);
324+
});
325+
326+
process.once('SIGINT', () => bot.stop('SIGINT'));
327+
process.once('SIGTERM', () => bot.stop('SIGTERM'));

0 commit comments

Comments
 (0)