toolsProvider.js
"use strict";
/**
* Job Search Plugin β toolsProvider (23 tools)
*
* Tools:
* Search Β· search_jobs(channel), fetch_job_page
* ATS Portals Β· scan_portals(mode)
* Applications Β· manage_application(action), list_applications
* Insights Β· application_insights(mode)
* Salary Β· salary(mode)
* Export Β· export_applications(format)
* Network Β· manage_network(action)
* Saved Search Β· manage_saved_search(action)
* Settings Β· job_settings(action)
* Analysis Β· analyze_job_description, match_resume_to_job, verify_company_legitimacy
* Resume Β· read_resume
* Generation Β· generate_cover_letter, generate_resume_bullets, prepare_interview_questions
* Visa Β· check_work_permit
* Briefing Β· daily_briefing
* Comparison Β· compare_jobs
* Batch Β· batch_update_applications
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.toolsProvider = void 0;
const sdk_1 = require("@lmstudio/sdk");
const promises_1 = require("fs/promises");
const pdf_parse_1 = __importDefault(require("pdf-parse"));
const path_1 = require("path");
const os_1 = require("os");
const search_1 = require("./search");
const portals_1 = require("./portals");
const zod_1 = require("zod");
const config_1 = require("./config");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function json(obj) {
return JSON.stringify(obj, null, 2);
}
function safe_impl(name, fn) {
return async (params, ctx) => {
if (ctx.signal.aborted) {
return JSON.stringify({ tool_error: true, tool: name, error: "cancelled" });
}
try {
return await fn(params, ctx);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return JSON.stringify({
tool_error: true,
tool: name,
error: msg,
hint: "Read the error above, fix the parameter causing the issue, and retry the tool call.",
}, null, 2);
}
};
}
function stripHtml(html) {
return html
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/ /g, " ").replace(/"/g, '"').replace(/'/g, "'")
.replace(/[ \t]+/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function getDataDir(configPath) {
return expandPath(configPath.trim() || (0, path_1.join)((0, os_1.homedir)(), "job-search-data"));
}
// Expand leading ~/ to the user's home directory (Node's readFile doesn't do this)
function expandPath(p) {
if (p.startsWith("~/"))
return (0, path_1.join)((0, os_1.homedir)(), p.slice(2));
if (p === "~")
return (0, os_1.homedir)();
return p;
}
// Read a resume file β supports plain text (.txt, .md) and PDF
async function readResumeFile(filePath) {
const buf = await (0, promises_1.readFile)(filePath);
if (filePath.toLowerCase().endsWith(".pdf")) {
const parsed = await (0, pdf_parse_1.default)(buf);
return parsed.text;
}
return buf.toString("utf8");
}
function dbPath(dataDir) {
return (0, path_1.join)(dataDir, "applications.json");
}
async function loadDB(dataDir) {
try {
const raw = await (0, promises_1.readFile)(dbPath(dataDir), "utf8");
const db = JSON.parse(raw);
if (!db.applications)
db.applications = [];
if (!db.savedSearches)
db.savedSearches = [];
if (!db.network)
db.network = [];
// Hydrate fields added after initial release so old records don't crash
for (const app of db.applications) {
app.postedDate ??= "";
app.followUpDate ??= "";
app.totalComp ??= "";
// Migrate old string[] contacts to new structured format
if (Array.isArray(app.contacts) && typeof app.contacts[0] === "string") {
app.contacts = app.contacts.map((c) => ({
name: c, role: "", email: "", linkedIn: "", notes: "",
}));
}
app.contacts ??= [];
app.country ??= "";
app.isInternational ??= false;
app.workPermitStatus ??= "unknown";
app.workPermitNotes ??= "";
app.legitimacyScore ??= -1;
app.legitimacyFlags ??= [];
app.interviewNotes ??= [];
app.rejectionReason ??= "";
app.statusHistory ??= [];
}
return db;
}
catch (err) {
if (err.code === "ENOENT") {
return { applications: [], savedSearches: [], network: [] };
}
throw err;
}
}
const BACKUP_RETENTION = 3;
async function saveDB(dataDir, db) {
await (0, promises_1.mkdir)(dataDir, { recursive: true });
const dbFile = dbPath(dataDir);
const dbName = dbFile.split("/").pop();
try {
await (0, promises_1.copyFile)(dbFile, `${dbFile}.bak.${Date.now()}`);
}
catch (e) {
if (e.code !== "ENOENT")
throw e;
}
await (0, promises_1.writeFile)(dbFile, JSON.stringify(db, null, 2), "utf8");
const entries = await (0, promises_1.readdir)(dataDir);
const backups = entries
.filter((n) => n.startsWith(`${dbName}.bak.`))
.sort()
.reverse();
for (const stale of backups.slice(BACKUP_RETENTION)) {
await (0, promises_1.unlink)((0, path_1.join)(dataDir, stale));
}
}
function makeId() {
return crypto.randomUUID();
}
function todayStr() {
return new Date().toISOString().slice(0, 10);
}
/** Set appliedDate and followUpDate (+7 days) if not already set. Mutates in place. */
function stampAppliedDate(app) {
if (!app.appliedDate) {
app.appliedDate = todayStr();
}
if (!app.followUpDate) {
const fu = new Date();
fu.setDate(fu.getDate() + 7);
app.followUpDate = fu.toISOString().slice(0, 10);
}
}
/** Build sets for dedup filtering against already-tracked applications. */
function buildTrackedSets(apps) {
return {
urls: new Set(apps.map((a) => a.url.toLowerCase()).filter(Boolean)),
keys: new Set(apps.map((a) => `${a.company.toLowerCase().replace(/[^a-z0-9]/g, "")}::${a.role.toLowerCase().replace(/[^a-z0-9]/g, "")}`)),
};
}
/** Filter out search hits that match already-tracked applications by URL or fuzzy company+role. */
function filterTracked(hits, tracked, limit) {
const filtered = hits.filter((h) => {
if (tracked.urls.has(h.url.toLowerCase()))
return false;
const titleNorm = h.title.toLowerCase().replace(/[^a-z0-9]/g, "");
for (const key of tracked.keys) {
const [comp, role] = key.split("::");
if (comp.length >= 3 && role.length >= 3 && titleNorm.includes(comp) && titleNorm.includes(role))
return false;
}
return true;
});
return { results: filtered.slice(0, limit), skipped: hits.length - filtered.length };
}
// ---------------------------------------------------------------------------
// Tools Provider
// ---------------------------------------------------------------------------
const toolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(config_1.pluginConfigSchematics);
const dataDir = () => getDataDir(cfg.get("dataPath"));
const maxResults = () => cfg.get("maxSearchResults");
const resumePath = () => expandPath(cfg.get("resumePath").trim());
const preferredLocation = () => cfg.get("preferredLocation").trim() || "India";
const homeCountry = () => cfg.get("homeCountry").trim() || "India";
const citizenship = () => cfg.get("citizenship").trim() || "Indian";
const openToInternational = () => cfg.get("openToInternational");
// Convert a comma-separated domain list to a DuckDuckGo site: filter string
function toSiteFilter(domains, fallback) {
const parts = (domains.trim() || fallback)
.split(",")
.map((d) => `site:${d.trim()}`)
.filter((s) => s !== "site:");
return parts.join(" OR ");
}
const homeBoards = () => toSiteFilter(cfg.get("homeJobBoards"), "naukri.com,linkedin.com,foundit.in,instahyre.com,iimjobs.com,wellfound.com,shine.com");
const globalBoards = () => toSiteFilter(cfg.get("globalJobBoards"), "linkedin.com,indeed.com,glassdoor.com,lever.co,greenhouse.io,wellfound.com");
const searxng = () => cfg.get("searxngUrl").trim() || undefined;
const searchWindow = () => {
const v = cfg.get("searchRecencyWindow").trim().toLowerCase();
return (["day", "week", "month", "year"].includes(v) ? v : undefined);
};
const webSearch = (query, max, timeRange) => (0, search_1.webSearch)(query, max, 10_000, searxng(), timeRange ?? searchWindow());
// Returns true when the job country differs from the user's home country
function isInternationalJob(jobCountry) {
if (!jobCountry)
return false;
return jobCountry.toLowerCase() !== homeCountry().toLowerCase();
}
// Normalise a raw location string to a country name best-effort
function extractCountry(location) {
const l = location.toLowerCase();
if (/\bindia\b|bengaluru|bangalore|mumbai|delhi|hyderabad|pune|chennai|kolkata|noida|gurugram|gurgaon/.test(l))
return "India";
if (/\busa\b|\bunited states\b|\bamerican\b|new york|san francisco|seattle|boston|austin|chicago|remote us/.test(l))
return "USA";
if (/\buk\b|\bunited kingdom\b|london|manchester|edinburgh/.test(l))
return "UK";
if (/\bcanada\b|toronto|vancouver|montreal/.test(l))
return "Canada";
if (/\baustralia\b|sydney|melbourne|brisbane/.test(l))
return "Australia";
if (/\bgermany\b|berlin|munich|hamburg/.test(l))
return "Germany";
if (/\bsingapore\b/.test(l))
return "Singapore";
if (/\buae\b|dubai|abu dhabi/.test(l))
return "UAE";
if (/\bnetherlands\b|amsterdam/.test(l))
return "Netherlands";
if (/\bfrance\b|paris/.test(l))
return "France";
if (/\bjapan\b|tokyo/.test(l))
return "Japan";
if (/\bremote\b/.test(l))
return "Remote";
return location; // Return as-is if unrecognised
}
const tools = [
// =========================================================================
// SEARCH
// =========================================================================
(0, sdk_1.tool)({
name: "search_jobs",
description: (0, sdk_1.text) `
Search for job listings across all channels.
channel options:
boards β standard job boards (Naukri, LinkedIn, Indeed, Glassdoor etc.)
company β research a company: culture, reviews, funding, tech stack
hidden β unadvertised roles: LinkedIn/X hiring posts, HN Who's Hiring, Wellfound, YC, Reddit, regional boards
remote β verified remote boards via Remotive API + Remote OK API + WWR/Remote.co
funded β recently funded startups likely hiring (TechCrunch + Crunchbase)
government β government/public sector portals (India: NCS/UPSC/SSC/PSUs/Railways; USA: USAJobs; UK: Civil Service; AU/EU/CA)
academic β research and university boards (IIT/IISc/CSIR/DST for India; jobs.ac.uk; Chronicle; Nature Careers; EURAXESS)
`,
parameters: {
query: zod_1.z.string().describe("Role, skills, keywords, or company name"),
channel: zod_1.z.enum(["boards", "company", "hidden", "remote", "funded", "government", "academic"])
.default("boards").describe("Which market channel to search"),
location: zod_1.z.string().default("").describe("Location override. Leave blank to use plugin default."),
includeInternational: zod_1.z.coerce.boolean().optional()
.describe("Override international toggle for this search (boards channel only)"),
jobType: zod_1.z.enum(["any", "full_time", "part_time", "contract", "internship", "remote"]).default("any")
.describe("Employment type filter (boards channel only)"),
max: zod_1.z.coerce.number().int().min(1).max(20).optional().describe("Max results"),
strategy: zod_1.z.enum([
"all", "hiring_posts", "career_pages", "referral_network", "hn_whoishiring",
"wellfound", "yc_startups", "reddit", "product_hunt", "regional",
"tech_contract", "dev_community",
]).default("all").describe("Hidden market strategy (hidden channel only)"),
source: zod_1.z.enum(["all", "remotive", "remoteok", "boards"]).default("all")
.describe("Remote source (remote channel only)"),
stage: zod_1.z.enum(["seed", "series_a", "series_b", "series_c", "any"]).default("any")
.describe("Funding stage (funded channel only)"),
sector: zod_1.z.string().default("").describe("Sector/industry for funded/government/academic channels"),
country: zod_1.z.enum(["india", "usa", "uk", "australia", "eu", "canada", "all"]).default("india")
.describe("Country for government channel"),
field: zod_1.z.string().default("").describe("Research field (academic channel only)"),
region: zod_1.z.enum(["global", "india", "usa", "uk", "europe", "us", "eu", "remote"]).default("global")
.describe("Region (academic/remote/funded channels)"),
},
implementation: safe_impl("search_jobs", async (params, ctx) => {
const { channel } = params;
// ββ boards ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "boards") {
ctx.status(`Searching jobs: ${params.query}`);
const limit = params.max ?? maxResults();
const loc = params.location.trim() || preferredLocation();
const country = extractCountry(loc);
const international = isInternationalJob(country) && country !== "Remote";
const intlAllowed = params.includeInternational ?? openToInternational();
if (international && !intlAllowed) {
return json({
blocked: true,
reason: `International search (${country}) is disabled. Enable "Open to International Roles" in plugin settings, or pass includeInternational: true to override.`,
suggestion: `To search locally, try location="${preferredLocation()}" or leave it blank.`,
});
}
const typeTag = params.jobType !== "any" ? ` ${params.jobType.replace("_", " ")}` : "";
const locTag = loc ? ` ${loc}` : "";
const baseQuery = `${params.query}${typeTag}${locTag} job`;
const isHomeSearch = extractCountry(loc).toLowerCase() === homeCountry().toLowerCase();
const sites = isHomeSearch ? homeBoards() : globalBoards();
const fetchLimit = limit + 10;
let hits = await webSearch(`${baseQuery} ${sites}`, fetchLimit, searchWindow());
if (hits.length === 0)
hits = await webSearch(baseQuery, fetchLimit, searchWindow());
const db = await loadDB(dataDir());
const tracked = buildTrackedSets(db.applications);
const { results: filtered, skipped } = filterTracked(hits, tracked, limit);
return json({
query: params.query, location: loc, country, isInternational: international,
workPermitNote: international ? `International search (${country}). Run check_work_permit for visa requirements.` : null,
jobBoards: sites.split(" OR ").map((s) => s.replace("site:", "")),
results: filtered,
alreadyTracked: skipped > 0 ? `${skipped} result(s) hidden β already tracked.` : null,
});
}
// ββ company βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "company") {
ctx.status(`Researching company: ${params.query}`);
const q = params.sector
? `${params.query} company ${params.sector}`
: `${params.query} company culture reviews tech stack ${new Date().getFullYear()}`;
const hits = await webSearch(q, maxResults(), "year");
return json({ company: params.query, focus: params.sector || "general", results: hits });
}
// ββ hidden βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "hidden") {
ctx.status(`Finding hidden ${params.query} opportunitiesβ¦`);
const strategy = params.strategy;
const loc = params.location ? ` ${params.location}` : "";
const ind = params.sector ? ` ${params.sector}` : "";
const regionKey = (() => {
const l = params.location.toLowerCase();
if (/india|bangalore|mumbai|delhi|hyderabad|pune|chennai/.test(l))
return "india";
if (/europe|germany|france|uk|netherlands|spain|poland/.test(l))
return "europe";
if (/singapore|malaysia|philippines|thailand|indonesia|vietnam|japan|australia/.test(l))
return "sea";
if (/brazil|mexico|colombia|argentina|chile|peru/.test(l))
return "latam";
if (/uae|dubai|saudi|qatar|bahrain|kuwait|oman/.test(l))
return "me";
if (/nigeria|kenya|ghana|south africa|egypt/.test(l))
return "africa";
if (/remote/.test(l))
return "remote";
return "startup";
})();
const searches = [
{ key: "hiring_posts", query: `"we're hiring" OR "we are hiring" ${params.query}${ind}${loc} site:linkedin.com OR site:twitter.com OR site:x.com`, active: strategy === "all" || strategy === "hiring_posts" },
{ key: "career_pages", query: `${params.query}${ind}${loc} "careers" OR "jobs" -site:linkedin.com -site:indeed.com -site:glassdoor.com`, active: strategy === "all" || strategy === "career_pages" },
{ key: "hn_whoishiring", query: `site:news.ycombinator.com "Who is Hiring" ${params.query}${ind}`, active: strategy === "all" || strategy === "hn_whoishiring" },
{ key: "referral_network", query: `${params.query}${ind}${loc} "looking for" OR "open to referrals" OR "DM me" hiring`, active: strategy === "all" || strategy === "referral_network" },
{ key: "wellfound", query: `site:wellfound.com/jobs ${params.query}${ind}${loc}`, active: strategy === "all" || strategy === "wellfound" },
{ key: "yc_startups", query: `site:workatastartup.com ${params.query}${ind}`, active: strategy === "all" || strategy === "yc_startups" },
{ key: "reddit", query: `(site:reddit.com/r/forhire OR site:reddit.com/r/cscareerquestions) ${params.query} hiring`, active: strategy === "all" || strategy === "reddit" },
{ key: "product_hunt", query: `site:producthunt.com ${params.query}${ind} hiring OR careers`, active: strategy === "all" || strategy === "product_hunt" },
{ key: "regional", query: `${params.query}${ind} ${(portals_1.REGIONAL_BOARDS[regionKey] ?? portals_1.REGIONAL_BOARDS.startup).join(" OR ")}`, active: strategy === "all" || strategy === "regional" },
{ key: "tech_contract", query: `${params.query}${ind}${loc} site:dice.com OR site:hired.com OR site:arc.dev/remote-jobs`, active: strategy === "all" || strategy === "tech_contract" },
{ key: "dev_community", query: `${params.query}${ind}${loc} site:dev.to/jobs OR site:lobste.rs OR site:hashnode.com "hiring"`, active: strategy === "all" || strategy === "dev_community" },
];
const results = {};
for (const s of searches) {
if (!s.active)
continue;
if (ctx.signal.aborted)
break;
ctx.status(`Searching ${s.key}β¦`);
try {
results[s.key] = await webSearch(s.query, maxResults(), searchWindow());
}
catch {
results[s.key] = [];
}
}
return json({ query: params.query, sector: params.sector || "any", location: params.location || "any", regionDetected: regionKey, results });
}
// ββ remote βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "remote") {
const src = params.source;
const results = {};
if (src === "all" || src === "remotive") {
ctx.status("Querying Remotive APIβ¦");
try {
results.remotive = await (0, portals_1.fetchRemotive)(params.query, params.max ?? 50, ctx.signal);
}
catch (e) {
results.remotive = { error: e instanceof Error ? e.message : String(e) };
}
}
if (src === "all" || src === "remoteok") {
ctx.status("Querying Remote OK APIβ¦");
try {
results.remoteok = await (0, portals_1.fetchRemoteOK)(params.query, ctx.signal);
}
catch (e) {
results.remoteok = { error: e instanceof Error ? e.message : String(e) };
}
}
if (src === "all" || src === "boards") {
ctx.status("Searching remote job boardsβ¦");
try {
results.boards = await webSearch(`${params.query} ${portals_1.REGIONAL_BOARDS.remote.join(" OR ")}`, params.max ?? maxResults(), searchWindow());
}
catch (e) {
results.boards = { error: e instanceof Error ? e.message : String(e) };
}
}
const totalJobs = [
...(Array.isArray(results.remotive) ? results.remotive : []),
...(Array.isArray(results.remoteok) ? results.remoteok : []),
...(Array.isArray(results.boards) ? results.boards : []),
].length;
return json({ query: params.query, source: src, totalJobs, results });
}
// ββ funded βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "funded") {
const stageLabel = params.stage === "any" ? "" : ` ${params.stage.replace("_", " ")}`;
const loc = params.location ? ` ${params.location}` : "";
const sec = params.sector || params.query;
const yr = new Date().getFullYear();
const q = `${sec}${stageLabel} startup funded ${yr - 1} OR ${yr}${loc} "million" hiring`;
const results = await webSearch(q, maxResults(), "year");
const tcQuery = `site:techcrunch.com ${sec}${stageLabel} raised funding ${yr - 1} OR ${yr}`;
const techCrunchResults = await webSearch(tcQuery, 5, "year");
return json({
sector: sec, stage: params.stage, location: params.location || "global",
generalResults: results, techCrunchResults,
why: "Companies typically hire 30β60% more in the 6 months after a funding round.",
nextSteps: ["Check each company's LinkedIn and careers page", "Find the hiring manager on LinkedIn and reach out directly"],
});
}
// ββ government βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "government") {
const sec = params.sector ? ` ${params.sector}` : "";
const role = params.query;
const boards = {
india: [
{ key: "ncs_portal", query: `site:ncs.gov.in ${role}${sec}` },
{ key: "employment_news", query: `site:employmentnews.gov.in ${role}${sec}` },
{ key: "upsc", query: `site:upsc.gov.in ${role}${sec}` },
{ key: "ssc", query: `site:ssc.nic.in ${role}${sec}` },
{ key: "ibps_banking", query: `(site:ibps.in OR site:sbi.co.in OR site:rbi.org.in) ${role}${sec}` },
{ key: "psu_jobs", query: `${role}${sec} site:isro.gov.in OR site:drdo.gov.in OR site:bhel.in OR site:ntpc.co.in OR site:ongcindia.com OR site:hal-india.co.in` },
{ key: "railways_rrb", query: `${role}${sec} site:indianrailways.gov.in OR site:rrbcdg.gov.in` },
{ key: "state_govts", query: `${role}${sec} state government jobs site:gov.in recruitment 2025` },
{ key: "defence_forces", query: `${role}${sec} site:joinindianarmy.nic.in OR site:joinindiannavy.gov.in OR site:careerindianairforce.cdac.in` },
],
usa: [{ key: "usajobs", query: `site:usajobs.gov ${role}${sec}` }, { key: "clearance", query: `site:clearancejobs.com ${role}${sec}` }],
uk: [{ key: "civil_service", query: `site:civilservicejobs.service.gov.uk ${role}${sec}` }, { key: "nhs", query: `site:jobs.nhs.uk ${role}${sec}` }],
australia: [{ key: "aps_jobs", query: `site:apsjobs.gov.au ${role}${sec}` }],
eu: [{ key: "epso", query: `site:epso.europa.eu ${role}${sec}` }],
canada: [{ key: "gc_jobs", query: `site:jobs-emplois.gc.ca ${role}${sec}` }],
};
const targets = params.country === "all"
? Object.values(boards).flat()
: (boards[params.country] ?? []);
const results = {};
for (const t of targets) {
if (ctx.signal.aborted)
break;
ctx.status(`Searching ${t.key}β¦`);
try {
results[t.key] = await webSearch(t.query, maxResults(), searchWindow());
}
catch {
results[t.key] = [];
}
}
const indiaTips = params.country === "india" || params.country === "all" ? {
apply_ncs: "Register at ncs.gov.in β India's official National Career Service portal.",
psu_direct: "PSUs like ISRO, DRDO, BHEL post on their own sites weeks before aggregators.",
banking_cycle: "IBPS runs annual PO/Clerk/SO exams β check ibps.in for the notification calendar.",
rrb_ntpc: "RRBs run large periodic drives β check indianrailways.gov.in and regional RRB sites.",
} : undefined;
return json({ query: role, country: params.country, sector: params.sector || "any", results, ...(indiaTips ? { indiaTips } : {}) });
}
// ββ academic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "academic") {
const fld = params.field ? ` ${params.field}` : "";
const role = params.query;
const academicRegion = params.region;
const boards = [
{ key: "iit_isc_portals", query: `${role}${fld} site:iitb.ac.in OR site:iitd.ac.in OR site:iisc.ac.in OR site:iitm.ac.in recruitment`, regions: ["india", "global"] },
{ key: "iiser_portals", query: `${role}${fld} site:iiserpune.ac.in OR site:iiserbhopal.ac.in OR site:iiserkolkata.ac.in recruitment`, regions: ["india", "global"] },
{ key: "dst_serb", query: `${role}${fld} site:dst.gov.in OR site:serb.gov.in fellowship position`, regions: ["india", "global"] },
{ key: "csir_dbt_icmr", query: `${role}${fld} site:csir.res.in OR site:dbtindia.gov.in OR site:icmr.gov.in recruitment`, regions: ["india", "global"] },
{ key: "jobs_ac_uk", query: `site:jobs.ac.uk ${role}${fld}`, regions: ["uk", "global"] },
{ key: "chronicle", query: `site:chronicle.com/jobs ${role}${fld}`, regions: ["usa", "global"] },
{ key: "inside_higher_ed", query: `site:insidehighered.com/jobs ${role}${fld}`, regions: ["usa", "global"] },
{ key: "nature_careers", query: `site:nature.com/naturecareers ${role}${fld}`, regions: ["global", "uk", "europe"] },
{ key: "science_careers", query: `site:science.org/careers/jobs ${role}${fld}`, regions: ["global", "usa"] },
{ key: "euraxess", query: `site:euraxess.net/jobs ${role}${fld}`, regions: ["europe", "global"] },
{ key: "academicpositions", query: `site:academicpositions.com ${role}${fld}`, regions: ["global", "europe"] },
];
const targets = boards.filter((b) => b.regions.includes(academicRegion));
const results = {};
for (const t of targets) {
if (ctx.signal.aborted)
break;
ctx.status(`Searching ${t.key}β¦`);
try {
results[t.key] = await webSearch(t.query, maxResults(), searchWindow());
}
catch {
results[t.key] = [];
}
}
return json({ query: role, field: params.field || "any", region: academicRegion, results });
}
return json({ error: `Unknown channel: ${channel}` });
}),
}),
(0, sdk_1.tool)({
name: "fetch_job_page",
description: (0, sdk_1.text) `
Fetch and parse a job posting page from a URL.
Returns the cleaned text content of the page (HTML stripped).
Use this to get the full job description from a URL found via search_jobs.
`,
parameters: {
url: zod_1.z.string().url().describe("URL of the job posting page"),
},
implementation: safe_impl("fetch_job_page", async ({ url }, ctx) => {
ctx.status(`Fetching job page: ${url}`);
const res = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (compatible; job-search-plugin/1.0)" },
signal: AbortSignal.timeout(15000),
});
if (!res.ok)
throw new Error(`HTTP ${res.status} fetching ${url}`);
const html = await res.text();
const text = stripHtml(html).slice(0, 8000);
return json({ url, content: text });
}),
}),
// =========================================================================
// ATS PORTAL TOOLS
// =========================================================================
(0, sdk_1.tool)({
name: "scan_portals",
description: (0, sdk_1.text) `
Query company ATS portals directly (Greenhouse, Ashby, Lever, Workable) β no browser, pure API.
mode: "single" β fetch one company by slug and ATS type
mode: "all" β scan all 60+ AI/ML companies in the built-in database
Returns live job listings that often appear days before job boards index them.
`,
parameters: {
mode: zod_1.z.enum(["single", "all"]).default("all").describe("single = one company; all = scan built-in 60+ company database"),
company: zod_1.z.string().default("").describe("Company name (single mode)"),
slug: zod_1.z.string().default("").describe("ATS board slug e.g. 'stripe' (single mode)"),
ats: zod_1.z.enum(["greenhouse", "ashby", "lever", "workable"]).optional().describe("ATS platform (single mode)"),
roleFilter: zod_1.z.string().default("").describe("Keyword filter on job titles"),
region: zod_1.z.enum(["global", "us", "eu", "india", "remote"]).default("global").describe("Region filter (all mode)"),
limit: zod_1.z.coerce.number().int().min(1).max(200).default(50).describe("Max jobs (all mode)"),
},
implementation: safe_impl("scan_portals", async ({ mode, company, slug, ats, roleFilter, region, limit }, ctx) => {
if (mode === "single") {
if (!slug || !ats)
throw new Error("single mode requires both slug and ats parameters.");
ctx.status(`Fetching ${company || slug} jobs via ${ats}β¦`);
const portal = { name: company || slug, slug, ats, region: "global", category: "tech" };
const jobs = await (0, portals_1.fetchCompanyJobs)(portal, ctx.signal);
const filtered = (0, portals_1.filterByTitle)(jobs, roleFilter || undefined);
return json({ company: company || slug, ats, slug, total: jobs.length, filtered: filtered.length, jobs: filtered });
}
// mode === "all"
const portals = region === "global"
? portals_1.COMPANY_PORTALS
: portals_1.COMPANY_PORTALS.filter((p) => p.region === region || p.region === "global");
ctx.status(`Scanning ${portals.length} company portalsβ¦`);
const results = [];
let total = 0;
for (const portal of portals) {
if (total >= limit)
break;
if (ctx.signal.aborted)
break;
try {
ctx.status(`Fetching ${portal.name}β¦`);
const jobs = await (0, portals_1.fetchCompanyJobs)(portal, ctx.signal);
const filtered = (0, portals_1.filterByTitle)(jobs, roleFilter || undefined);
if (filtered.length > 0) {
results.push({ company: portal.name, jobs: filtered });
total += filtered.length;
}
}
catch { /* skip silently */ }
}
const allJobs = results.flatMap((r) => r.jobs).slice(0, limit);
return json({ roleFilter: roleFilter || "all", region, companiesScanned: portals.length, companiesWithMatches: results.length, totalJobs: allJobs.length, jobs: allJobs });
}),
}),
// =========================================================================
// APPLICATION TRACKER
// =========================================================================
(0, sdk_1.tool)({
name: "manage_application",
description: (0, sdk_1.text) `
Create, read, update, delete, or log interviews for a tracked job application.
action: "add" β create new application
action: "update" β update fields on existing application by ID
action: "get" β fetch full details of one application by ID
action: "delete" β permanently delete application by ID
action: "add_interview" β log an interview round to an existing application
`,
parameters: {
action: zod_1.z.enum(["add", "update", "get", "delete", "add_interview"]).describe("Operation to perform"),
id: zod_1.z.string().default("").describe("Application ID (required for update/get/delete/add_interview)"),
company: zod_1.z.string().default("").describe("Company name (add: required)"),
role: zod_1.z.string().default("").describe("Job title (add: required)"),
url: zod_1.z.string().default(""),
location: zod_1.z.string().default(""),
status: zod_1.z.enum(["saved", "applied", "interview", "offer", "rejected", "withdrawn"]).optional(),
jobDescription: zod_1.z.string().default(""),
notes: zod_1.z.string().default("").describe("For add: initial notes. For update: text to APPEND."),
salary: zod_1.z.string().default(""),
totalComp: zod_1.z.string().optional(),
nextStep: zod_1.z.string().default(""),
postedDate: zod_1.z.string().default(""),
followUpDate: zod_1.z.string().default(""),
workPermitStatus: zod_1.z.enum(["not_required", "eligible", "requires_sponsorship", "not_eligible", "unknown"]).optional(),
workPermitNotes: zod_1.z.string().optional(),
rejectionReason: zod_1.z.string().optional(),
round: zod_1.z.string().default("").describe("Interview round name (add_interview only)"),
date: zod_1.z.string().default("").describe("Interview date YYYY-MM-DD (add_interview only)"),
interviewerName: zod_1.z.string().default(""),
interviewerRole: zod_1.z.string().default(""),
questions: zod_1.z.array(zod_1.z.string()).default([]),
feedback: zod_1.z.string().default(""),
outcome: zod_1.z.enum(["passed", "failed", "pending", "unknown"]).default("pending"),
},
implementation: safe_impl("manage_application", async (params) => {
const db = await loadDB(dataDir());
// ββ add ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "add") {
if (!params.company.trim())
throw new Error("company is required for action=add.");
if (!params.role.trim())
throw new Error("role is required for action=add.");
const country = extractCountry(params.location);
const international = isInternationalJob(country) && country !== "Remote";
const companyLower = params.company.toLowerCase();
const roleNorm = params.role.toLowerCase().replace(/[^a-z0-9]/g, "");
const duplicate = db.applications.find((a) => a.company.toLowerCase() === companyLower && a.role.toLowerCase().replace(/[^a-z0-9]/g, "") === roleNorm);
const st = params.status ?? "saved";
const app = {
id: makeId(), company: params.company, role: params.role, url: params.url,
location: params.location, country, isInternational: international,
status: st, appliedDate: "", postedDate: params.postedDate, followUpDate: params.followUpDate,
jobDescription: params.jobDescription, notes: params.notes, salary: params.salary,
totalComp: "", nextStep: params.nextStep, contacts: [],
workPermitStatus: international ? "unknown" : "not_required",
workPermitNotes: international ? `International role in ${country}. Run check_work_permit for visa requirements.` : "",
legitimacyScore: -1, legitimacyFlags: [], interviewNotes: [], rejectionReason: "",
statusHistory: [{ status: st, date: todayStr() }],
updatedAt: new Date().toISOString(),
};
if (st === "applied")
stampAppliedDate(app);
const previousFlag = db.applications.find((a) => a.company.toLowerCase() === companyLower && a.legitimacyScore >= 0 && a.legitimacyScore < 50);
db.applications.push(app);
await saveDB(dataDir(), db);
return json({
success: true, application: app,
duplicateWarning: duplicate ? `β Possible duplicate: "${duplicate.company} β ${duplicate.role}" already tracked (ID: ${duplicate.id}).` : null,
internationalAlert: international ? `β International role (${country}). Use check_work_permit to verify visa requirements.` : null,
legitimacyWarning: previousFlag ? `β ${params.company} previously flagged suspicious (score ${previousFlag.legitimacyScore}/100).` : null,
});
}
// ββ update βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "update") {
if (!params.id.trim())
throw new Error("id is required for action=update.");
const idx = db.applications.findIndex((a) => a.id === params.id);
if (idx === -1)
throw new Error(`Application ID '${params.id}' not found.`);
const app = db.applications[idx];
const merged = {
...app,
...(params.company && { company: params.company }),
...(params.role && { role: params.role }),
...(params.url && { url: params.url }),
...(params.location && { location: params.location }),
...(params.status && { status: params.status }),
...(params.jobDescription && { jobDescription: params.jobDescription }),
...(params.notes && { notes: app.notes ? `${app.notes}\n${params.notes}` : params.notes }),
...(params.salary && { salary: params.salary }),
...(params.totalComp !== undefined && { totalComp: params.totalComp }),
...(params.nextStep && { nextStep: params.nextStep }),
...(params.postedDate && { postedDate: params.postedDate }),
...(params.followUpDate && { followUpDate: params.followUpDate }),
...(params.workPermitStatus !== undefined && { workPermitStatus: params.workPermitStatus }),
...(params.workPermitNotes !== undefined && { workPermitNotes: params.workPermitNotes }),
...(params.rejectionReason !== undefined && { rejectionReason: params.rejectionReason }),
updatedAt: new Date().toISOString(),
};
if (params.status === "applied")
stampAppliedDate(merged);
if (params.status && params.status !== app.status)
merged.statusHistory.push({ status: params.status, date: todayStr() });
db.applications[idx] = merged;
await saveDB(dataDir(), db);
return json({ success: true, application: merged });
}
// ββ get ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "get") {
if (!params.id.trim())
throw new Error("id is required for action=get.");
const app = db.applications.find((a) => a.id === params.id);
if (!app)
throw new Error(`Application ID '${params.id}' not found.`);
return json(app);
}
// ββ delete βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "delete") {
if (!params.id.trim())
throw new Error("id is required for action=delete.");
const before = db.applications.length;
db.applications = db.applications.filter((a) => a.id !== params.id);
if (db.applications.length === before)
throw new Error(`Application ID '${params.id}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: params.id });
}
// ββ add_interview βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "add_interview") {
if (!params.id.trim())
throw new Error("id is required for action=add_interview.");
const idx = db.applications.findIndex((a) => a.id === params.id);
if (idx === -1)
throw new Error(`Application ID '${params.id}' not found.`);
const note = {
round: params.round || "Unknown Round",
date: params.date || new Date().toISOString().slice(0, 10),
interviewerName: params.interviewerName,
interviewerRole: params.interviewerRole,
questions: params.questions,
feedback: params.feedback,
outcome: params.outcome,
};
db.applications[idx].interviewNotes.push(note);
if (db.applications[idx].status === "applied") {
db.applications[idx].statusHistory.push({ status: "interview", date: todayStr() });
db.applications[idx].status = "interview";
}
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, application: db.applications[idx].company + " β " + db.applications[idx].role, totalRounds: db.applications[idx].interviewNotes.length, latestNote: note });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
(0, sdk_1.tool)({
name: "list_applications",
description: (0, sdk_1.text) `
List all tracked job applications, optionally filtered by status.
Returns a summary table: ID, company, role, status, next step, updated date.
`,
parameters: {
status: zod_1.z.enum(["saved", "applied", "interview", "offer", "rejected", "withdrawn", "all"])
.default("all").describe("Filter by status, or 'all' for everything"),
search: zod_1.z.string().default("").describe("Search keyword in company or role name"),
internationalOnly: zod_1.z.coerce.boolean().default(false)
.describe("Show only international / cross-border applications"),
workPermitStatus: zod_1.z.enum(["not_required", "eligible", "requires_sponsorship", "not_eligible", "unknown", "all"])
.default("all").describe("Filter by work permit status"),
},
implementation: safe_impl("list_applications", async ({ status, search, internationalOnly, workPermitStatus }) => {
const db = await loadDB(dataDir());
let apps = db.applications;
if (status !== "all")
apps = apps.filter((a) => a.status === status);
if (internationalOnly)
apps = apps.filter((a) => a.isInternational);
if (workPermitStatus !== "all")
apps = apps.filter((a) => a.workPermitStatus === workPermitStatus);
if (search) {
const kw = search.toLowerCase();
apps = apps.filter((a) => a.company.toLowerCase().includes(kw) || a.role.toLowerCase().includes(kw));
}
// Sort: most recently updated first
apps = apps.slice().sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
const summary = apps.map((a) => ({
id: a.id,
company: a.company,
role: a.role,
location: a.location,
country: a.country,
isInternational: a.isInternational,
status: a.status,
appliedDate: a.appliedDate,
salary: a.salary,
workPermitStatus: a.workPermitStatus,
nextStep: a.nextStep,
updatedAt: a.updatedAt.slice(0, 10),
}));
const stats = {
total: db.applications.length,
saved: db.applications.filter((a) => a.status === "saved").length,
applied: db.applications.filter((a) => a.status === "applied").length,
interview: db.applications.filter((a) => a.status === "interview").length,
offer: db.applications.filter((a) => a.status === "offer").length,
rejected: db.applications.filter((a) => a.status === "rejected").length,
international: db.applications.filter((a) => a.isInternational).length,
pendingWorkPermitCheck: db.applications.filter((a) => a.isInternational && a.workPermitStatus === "unknown").length,
};
return json({ stats, filter: { status, search, internationalOnly, workPermitStatus }, applications: summary });
}),
}),
// =========================================================================
// ANALYSIS
// =========================================================================
(0, sdk_1.tool)({
name: "analyze_job_description",
description: (0, sdk_1.text) `
Analyze a job description and extract structured information:
required skills, nice-to-have skills, responsibilities, seniority level,
red flags, and key questions to ask the interviewer.
Paste the raw job description text as input.
`,
parameters: {
jobDescription: zod_1.z.string().min(50)
.describe("Full job description text (paste it in directly)"),
role: zod_1.z.string().default("").describe("Job title (optional, improves analysis)"),
},
implementation: safe_impl("analyze_job_description", async ({ jobDescription, role }, ctx) => {
ctx.status("Analyzing job descriptionβ¦");
const wordCount = jobDescription.split(/\s+/).length;
const hasRemote = /remote|hybrid/i.test(jobDescription);
const hasSalary = /\$[\d,]+|\d+k\b|salary|compensation/i.test(jobDescription);
const techMatches = jobDescription.match(/\b(TypeScript|JavaScript|Python|Go|Rust|Java|C\+\+|React|Vue|Angular|Node\.js|Next\.js|AWS|GCP|Azure|Docker|Kubernetes|PostgreSQL|MySQL|MongoDB|Redis|GraphQL|REST|gRPC|Terraform|Kafka|Spark|Ray|Airflow|MLflow|LangChain|LlamaIndex|Pinecone|Weaviate|Qdrant|vLLM|TGI|LoRA|PEFT|HuggingFace|PyTorch|TensorFlow|JAX|ONNX|ML|LLM|AI|RAG|RLHF|SFT|FineTuning|MCP|OpenAI|Anthropic|Gemini)\b/gi);
const techs = [...new Set((techMatches ?? []).map((t) => t.toLowerCase()))];
const archetype = (0, portals_1.detectArchetype)(role || "", jobDescription);
const legitimacy = (0, portals_1.assessJobLegitimacy)(jobDescription);
const atsKeywords = (0, portals_1.extractAtsKeywords)(jobDescription, 15);
return json({
role: role || "Unknown",
wordCount,
mentionsRemote: hasRemote,
mentionsSalary: hasSalary,
detectedTechnologies: techs,
archetype: archetype.archetype,
archetypeLabel: archetype.label,
legitimacy: {
tier: legitimacy.tier,
signals: legitimacy.signals,
},
atsKeywords,
jobDescription,
instructions: "Using the jobDescription above, extract and present:\n" +
"(A) Required skills β hard requirements only\n" +
"(B) Nice-to-have skills β explicit 'preferred' / 'plus' items\n" +
"(C) Key responsibilities β top 5 bullet points\n" +
"(D) Seniority level (IC1βIC6) with rationale\n" +
"(E) Red flags β unrealistic requirements, vague scope, high churn signals\n" +
"(F) Top 5 questions to ask the interviewer specific to this role\n" +
`(G) Archetype framing: this is a '${archetype.label}' role β highlight what matters most for this archetype`,
});
}),
}),
(0, sdk_1.tool)({
name: "match_resume_to_job",
description: (0, sdk_1.text) `
Compare the user's resume against a job description and produce a fit score (0β100),
list of matched skills, gaps, and prioritized suggestions to improve fit.
Reads the resume from the path configured in plugin settings, or accepts inline text.
`,
parameters: {
jobDescription: zod_1.z.string().min(20).describe("Full job description text"),
resumeText: zod_1.z.string().default("")
.describe("Inline resume text. If blank, reads from the configured Resume File Path."),
},
implementation: safe_impl("match_resume_to_job", async ({ jobDescription, resumeText }, ctx) => {
ctx.status("Loading resumeβ¦");
let resume = resumeText.trim();
if (!resume) {
const rp = resumePath();
if (!rp)
throw new Error("No resume text provided and Resume File Path is not configured in plugin settings.");
resume = await readResumeFile(rp);
}
ctx.status("Extracting skills and ATS keywordsβ¦");
const extractSkills = (t) => {
const m = t.match(/\b(TypeScript|JavaScript|Python|Go|Rust|Java|C\+\+|React|Vue|Angular|Node\.js|Next\.js|AWS|GCP|Azure|Docker|Kubernetes|PostgreSQL|MySQL|MongoDB|Redis|GraphQL|REST|gRPC|Terraform|Kafka|Spark|Ray|MLflow|LangChain|LlamaIndex|Pinecone|Weaviate|vLLM|PyTorch|TensorFlow|JAX|ONNX|ML|LLM|AI|RAG|RLHF|LoRA|HuggingFace|Machine Learning|Data Science|Product Management|Agile|Scrum|CI\/CD|DevOps|MCP|OpenAI|Anthropic)\b/gi);
return [...new Set((m ?? []).map((s) => s.toLowerCase()))];
};
const jobSkills = extractSkills(jobDescription);
const resumeSkills = extractSkills(resume);
const matched = jobSkills.filter((s) => resumeSkills.includes(s));
const gaps = jobSkills.filter((s) => !resumeSkills.includes(s));
const roughScore = jobSkills.length > 0
? Math.round((matched.length / jobSkills.length) * 100)
: 50;
const archetype = (0, portals_1.detectArchetype)("", jobDescription);
const atsKeywords = (0, portals_1.extractAtsKeywords)(jobDescription, 20);
return json({
roughFitScore: roughScore,
matchedSkills: matched,
skillGaps: gaps,
resumeSkillsDetected: resumeSkills,
jobSkillsDetected: jobSkills,
archetype: archetype.archetype,
archetypeLabel: archetype.label,
atsKeywords,
resume: resume.slice(0, 8000),
jobDescription: jobDescription.slice(0, 4000),
instructions: "Using the resume and job description above, produce a structured analysis:\n" +
"(1) Overall fit score 0β100 with rationale.\n" +
"(2) Strengths β what already matches well (quote specific resume lines).\n" +
"(3) Skill gaps β list each missing keyword/skill from the JD absent in the resume.\n" +
"(4) Editing guide β for EACH gap:\n" +
" β’ If experience likely exists but is unmentioned: name WHERE to add it and provide a model bullet rewritten using JD vocabulary.\n" +
" β’ If skill is genuinely missing: say so and suggest the fastest path (side project, cert, OSS).\n" +
"(5) ATS keyword injection β embed all atsKeywords above verbatim into the resume; list which bullets to update.\n" +
`(6) Archetype fit: this is a '${archetype.label}' role β call out the 2β3 signals the resume must emphasise for this archetype.\n` +
"(7) Recommendation: Apply now / Apply after minor edits / Significant rework needed β with a one-line reason.",
});
}),
}),
// =========================================================================
// GENERATION
// =========================================================================
(0, sdk_1.tool)({
name: "generate_cover_letter",
description: (0, sdk_1.text) `
Generate a tailored cover letter draft for a specific job.
Provide the job description and basic info about the applicant.
The model will write a compelling, concise cover letter (3β4 paragraphs).
`,
parameters: {
company: zod_1.z.string().describe("Company name"),
role: zod_1.z.string().describe("Job title"),
jobDescription: zod_1.z.string().min(20).describe("Job description text"),
applicantName: zod_1.z.string().default("").describe("Your full name"),
applicantBackground: zod_1.z.string().default("")
.describe("Brief background: years of experience, key skills, notable achievements"),
tone: zod_1.z.enum(["professional", "conversational", "enthusiastic"]).default("professional")
.describe("Tone of the letter"),
resumeText: zod_1.z.string().default("")
.describe("Optional resume text; if blank, reads from configured Resume File Path"),
},
implementation: safe_impl("generate_cover_letter", async (params, ctx) => {
ctx.status("Loading resume for cover letterβ¦");
let resume = params.resumeText.trim();
if (!resume) {
const rp = resumePath();
if (rp) {
resume = await readResumeFile(rp);
}
}
const archetype = (0, portals_1.detectArchetype)(params.role, params.jobDescription);
const atsKeywords = (0, portals_1.extractAtsKeywords)(params.jobDescription, 10);
const clicheList = portals_1.CLICHE_WORDS.slice(0, 20).join(", ");
return json({
company: params.company,
role: params.role,
applicantName: params.applicantName,
applicantBackground: params.applicantBackground,
tone: params.tone,
archetype: archetype.archetype,
archetypeLabel: archetype.label,
atsKeywords,
jobDescription: params.jobDescription.slice(0, 4000),
resume: resume.slice(0, 6000),
instructions: `Write a ${params.tone} cover letter for ${params.applicantName || "the applicant"} ` +
`applying to ${params.role} at ${params.company}.\n` +
"Structure:\n" +
"(1) Opening hook β a specific observation about the company's product/mission, not a generic intro.\n" +
"(2) Exit narrative bridge β explain briefly what you're transitioning FROM and why this role is the logical NEXT step.\n" +
"(3) Top 2β3 achievements with metrics that map directly to the JD's stated needs.\n" +
"(4) Archetype fit signal β for this '" + archetype.label + "' role, include one sentence that speaks directly to what this archetype values.\n" +
"(5) Confident close β no 'I hope', no 'looking forward to hearing from you'.\n" +
`Weave in these ATS keywords naturally: ${atsKeywords.join(", ")}.\n` +
`STRICTLY AVOID these clichΓ©s: ${clicheList}.\n` +
"Keep it under 320 words. No fluff. No hollow superlatives.",
});
}),
}),
(0, sdk_1.tool)({
name: "generate_resume_bullets",
description: (0, sdk_1.text) `
Generate strong, achievement-oriented resume bullet points for a role or experience.
Follows the "Accomplished X by doing Y, resulting in Z" pattern with metrics.
`,
parameters: {
role: zod_1.z.string().describe("Job title / role (e.g. 'Senior Backend Engineer')"),
company: zod_1.z.string().default("").describe("Company or project name"),
responsibilities: zod_1.z.string()
.describe("Describe what you did in this role (rough notes are fine)"),
targetRole: zod_1.z.string().default("")
.describe("Target job title you are applying to (helps tailor the bullets)"),
count: zod_1.z.coerce.number().int().min(2).max(8).default(4)
.describe("Number of bullet points to generate"),
},
implementation: safe_impl("generate_resume_bullets", async (params) => {
const archetype = params.targetRole ? (0, portals_1.detectArchetype)(params.targetRole, params.responsibilities) : null;
const clicheList = portals_1.CLICHE_WORDS.slice(0, 15).join(", ");
return json({
sourceRole: params.role,
company: params.company,
targetRole: params.targetRole,
responsibilities: params.responsibilities,
count: params.count,
archetypeLabel: archetype?.label ?? null,
instructions: `Generate ${params.count} strong resume bullet points for ${params.role}` +
(params.company ? ` at ${params.company}` : "") + ".\n" +
"Rules:\n" +
"β’ Start with a strong, specific action verb (not 'Assisted', 'Helped', 'Worked on').\n" +
"β’ Include a concrete metric or outcome for every bullet (%, $, latency, scale, time saved).\n" +
"β’ Keep each bullet under 20 words.\n" +
"β’ Replace vague claims with specific tech, system names, or team sizes.\n" +
`β’ NEVER use these clichΓ©s: ${clicheList}.\n` +
(params.targetRole
? `β’ Tailor vocabulary to a ${params.targetRole} role` +
(archetype ? ` (archetype: ${archetype.label})` : "") + ".\n"
: "") +
"Format: 'β’ <bullet>' per line. Output bullets only, no commentary.",
});
}),
}),
(0, sdk_1.tool)({
name: "prepare_interview_questions",
description: (0, sdk_1.text) `
Generate a tailored set of interview questions and suggested answers
based on the job description and the applicant's background.
Covers behavioral, technical, and situational questions.
`,
parameters: {
jobDescription: zod_1.z.string().min(20).describe("Job description text"),
role: zod_1.z.string().describe("Job title"),
interviewStage: zod_1.z.enum(["phone_screen", "technical", "behavioral", "system_design", "final"])
.default("behavioral").describe("Stage of interview to prepare for"),
applicantBackground: zod_1.z.string().default("")
.describe("Brief background to tailor questions"),
questionCount: zod_1.z.coerce.number().int().min(3).max(20).default(8)
.describe("Number of questions to generate"),
},
implementation: safe_impl("prepare_interview_questions", async (params) => {
return json({
role: params.role,
interviewStage: params.interviewStage,
questionCount: params.questionCount,
applicantBackground: params.applicantBackground,
jobDescription: params.jobDescription.slice(0, 2000),
instructions: `Generate ${params.questionCount} ${params.interviewStage.replace("_", " ")} ` +
`interview questions for a ${params.role} role, based on the job description above. ` +
"For each question, provide: " +
"(Q) The question, (Why) Why interviewers ask it, " +
"(A) A strong answer framework / sample answer outline. " +
"Focus on what actually matters for this specific role.",
});
}),
}),
(0, sdk_1.tool)({
name: "export_applications",
description: (0, sdk_1.text) `
Export tracked job applications to a file.
format: "report" β formatted Markdown report with pipeline summary and per-application details
format: "csv" β CSV spreadsheet for use in Excel / Google Sheets
`,
parameters: {
format: zod_1.z.enum(["report", "csv"]).default("report"),
outputPath: zod_1.z.string().default("").describe("Output file path. Defaults to <dataPath>/applications-<date>.<ext>"),
},
implementation: safe_impl("export_applications", async ({ format, outputPath }) => {
const db = await loadDB(dataDir());
const date = new Date().toISOString().slice(0, 10);
if (format === "csv") {
const outPath = outputPath.trim() || (0, path_1.join)(dataDir(), `applications-${date}.csv`);
const headers = ["ID", "Company", "Role", "Location", "Country", "Status", "Applied Date", "Posted Date", "Follow-Up Date", "Salary", "Total Comp", "URL", "Next Step", "Contacts", "Interview Rounds", "Work Permit Status", "Legitimacy Score", "Rejection Reason", "Notes", "Updated At"];
function csvEscape(s) { return (s.includes(",") || s.includes('"') || s.includes("\n")) ? `"${s.replace(/"/g, '""')}"` : s; }
const rows = db.applications.map((a) => [a.id, a.company, a.role, a.location, a.country, a.status, a.appliedDate, a.postedDate, a.followUpDate, a.salary, a.totalComp, a.url, a.nextStep, a.contacts.map((c) => c.name).join("; "), String(a.interviewNotes.length), a.workPermitStatus, a.legitimacyScore >= 0 ? String(a.legitimacyScore) : "", a.rejectionReason, a.notes, a.updatedAt.slice(0, 10)].map(csvEscape).join(","));
const csv = [headers.join(","), ...rows].join("\n");
await (0, promises_1.mkdir)(dataDir(), { recursive: true });
await (0, promises_1.writeFile)(outPath, csv, "utf8");
return json({ success: true, path: outPath, applicationCount: db.applications.length });
}
// format === "report"
const outPath = outputPath.trim() || (0, path_1.join)(dataDir(), `report-${date}.md`);
const apps = db.applications;
const byStatus = {};
for (const a of apps)
byStatus[a.status] = (byStatus[a.status] ?? 0) + 1;
const lines = [
`# Job Application Report β ${date}`, "",
"## Pipeline Summary", "",
`| Status | Count |`, `|--------|-------|`,
...Object.entries(byStatus).map(([k, v]) => `| ${k} | ${v} |`),
"", `**Total:** ${apps.length}`, "",
"## Applications", "",
];
for (const a of apps.slice().sort((x, y) => y.updatedAt.localeCompare(x.updatedAt))) {
lines.push(`### ${a.company} β ${a.role}`, `- **Status:** ${a.status}`, `- **Location:** ${a.location}`, `- **Applied:** ${a.appliedDate || "not yet"}`, `- **Salary:** ${a.salary || "unknown"}`, `- **URL:** ${a.url || "none"}`, `- **Notes:** ${a.notes || "none"}`, "");
}
await (0, promises_1.mkdir)(dataDir(), { recursive: true });
await (0, promises_1.writeFile)(outPath, lines.join("\n"), "utf8");
return json({ success: true, path: outPath, applicationCount: apps.length });
}),
}),
// =========================================================================
// TIMING & URGENCY
// =========================================================================
(0, sdk_1.tool)({
name: "application_insights",
description: (0, sdk_1.text) `
Analytics and status intelligence for your job application pipeline.
mode: "urgency" β score how urgent it is to apply for a job by posted date
mode: "followups" β list applications with upcoming or overdue follow-up dates
mode: "stale" β detect applications with no activity in N days
mode: "stats" β pipeline summary: counts by status, conversion rate, international split
mode: "rejections" β analyze patterns in rejected applications to find improvement areas
`,
parameters: {
mode: zod_1.z.enum(["urgency", "followups", "stale", "stats", "rejections"]).describe("Which insight to compute"),
postedDate: zod_1.z.string().default("").describe("Posted date string: YYYY-MM-DD, '3 days ago', 'today' (urgency mode)"),
applicationId: zod_1.z.string().default("").describe("Application ID to update with computed dates (urgency mode)"),
role: zod_1.z.string().default(""),
company: zod_1.z.string().default(""),
daysAhead: zod_1.z.coerce.number().int().min(0).max(30).default(7).describe("Show follow-ups due within N days (followups mode)"),
staleDays: zod_1.z.coerce.number().int().min(1).max(90).default(14).describe("Consider stale after N days with no update (stale mode)"),
includeStatuses: zod_1.z.array(zod_1.z.enum(["saved", "applied", "interview"])).default(["applied", "interview"]).describe("Statuses to check (stale mode)"),
},
implementation: safe_impl("application_insights", async (params) => {
const db = await loadDB(dataDir());
// ββ urgency βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "urgency") {
const today = new Date();
let parsedDate = null;
const pd = params.postedDate;
const daysAgoMatch = pd.match(/(\d+)\s*day/i);
const weeksAgoMatch = pd.match(/(\d+)\s*week/i);
const monthsAgoMatch = pd.match(/(\d+)\s*month/i);
if (daysAgoMatch) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - parseInt(daysAgoMatch[1]));
}
else if (weeksAgoMatch) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - parseInt(weeksAgoMatch[1]) * 7);
}
else if (monthsAgoMatch) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - parseInt(monthsAgoMatch[1]) * 30);
}
else if (/^\d{4}-\d{2}-\d{2}$/.test(pd)) {
const [y, m, d] = pd.split("-").map(Number);
parsedDate = new Date(y, m - 1, d);
}
else if (/today|just posted/i.test(pd)) {
parsedDate = new Date(today);
}
else if (/yesterday/i.test(pd)) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - 1);
}
const daysOld = parsedDate ? Math.floor((today.getTime() - parsedDate.getTime()) / (1000 * 60 * 60 * 24)) : null;
let urgency;
let score;
let recommendation;
let callbackMultiplier;
if (daysOld === null) {
urgency = "unknown";
score = 5;
recommendation = "Could not parse date. Apply ASAP.";
callbackMultiplier = "unknown";
}
else if (daysOld <= 2) {
urgency = "CRITICAL";
score = 10;
recommendation = "Apply within hours. Early applicants get 2β3Γ more callbacks.";
callbackMultiplier = "2β3Γ";
}
else if (daysOld <= 7) {
urgency = "HIGH";
score = 8;
recommendation = "Apply today. First week applicants have strong odds.";
callbackMultiplier = "1.5β2Γ";
}
else if (daysOld <= 14) {
urgency = "MEDIUM";
score = 5;
recommendation = "Apply soon. Odds declining but still worth it for a strong match.";
callbackMultiplier = "0.8β1Γ";
}
else if (daysOld <= 30) {
urgency = "LOW";
score = 3;
recommendation = "Late application. Referrals help.";
callbackMultiplier = "0.3β0.6Γ";
}
else {
urgency = "STALE";
score = 1;
recommendation = "Likely filled. Verify it's still open.";
callbackMultiplier = "~0Γ";
}
const followUpDays = (daysOld ?? 0) <= 7 ? 7 : 5;
const followUpDate = new Date(today);
followUpDate.setDate(followUpDate.getDate() + followUpDays);
const followUpDateStr = followUpDate.toISOString().slice(0, 10);
const postedDateStr = parsedDate ? parsedDate.toISOString().slice(0, 10) : "";
if (params.applicationId) {
const idx = db.applications.findIndex((a) => a.id === params.applicationId);
if (idx !== -1) {
if (postedDateStr)
db.applications[idx].postedDate = postedDateStr;
db.applications[idx].followUpDate = followUpDateStr;
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
}
}
return json({ role: params.role || "Unknown", company: params.company || "Unknown", postedDate: postedDateStr || pd, daysOld, urgency, urgencyScore: score, callbackMultiplier, recommendation, followUpDate: followUpDateStr });
}
// ββ followups βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "followups") {
const now = new Date();
const cutoff = new Date(now);
cutoff.setDate(cutoff.getDate() + params.daysAhead);
const todStr = now.toISOString().slice(0, 10);
const cutoffStr = cutoff.toISOString().slice(0, 10);
const dateRe = /^\d{4}-\d{2}-\d{2}$/;
const active = db.applications
.filter((a) => a.status !== "rejected" && a.status !== "withdrawn" && dateRe.test(a.followUpDate ?? "") && a.followUpDate <= cutoffStr)
.sort((a, b) => a.followUpDate.localeCompare(b.followUpDate))
.map((a) => {
const overdue = a.followUpDate < todStr;
const daysUntil = Math.floor((new Date(a.followUpDate).getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
return { id: a.id, company: a.company, role: a.role, status: a.status, followUpDate: a.followUpDate, daysUntil, overdue, urgency: overdue ? "OVERDUE" : daysUntil === 0 ? "TODAY" : `in ${daysUntil}d`, nextStep: a.nextStep };
});
return json({ summary: { overdue: active.filter((r) => r.overdue).length, dueToday: active.filter((r) => !r.overdue && r.daysUntil === 0).length, upcoming: active.filter((r) => !r.overdue && r.daysUntil > 0).length }, followUps: active });
}
// ββ stale βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "stale") {
const now = new Date();
const cutoff = new Date(now.getTime() - params.staleDays * 24 * 60 * 60 * 1000);
const stale = db.applications
.filter((a) => params.includeStatuses.includes(a.status) && new Date(a.updatedAt) < cutoff)
.map((a) => {
const daysSinceUpdate = Math.floor((now.getTime() - new Date(a.updatedAt).getTime()) / (1000 * 60 * 60 * 24));
const appliedDaysAgo = a.appliedDate ? Math.floor((now.getTime() - new Date(a.appliedDate).getTime()) / (1000 * 60 * 60 * 24)) : null;
let recommendedAction;
if (a.status === "interview")
recommendedAction = "Send a polite follow-up email";
else if (a.status === "applied" && appliedDaysAgo !== null && appliedDaysAgo > 30)
recommendedAction = daysSinceUpdate > 45 ? "Likely ghosted β consider withdrawing" : "Send one follow-up; if no response in 7 days, close it";
else if (a.status === "applied")
recommendedAction = "Send a polite follow-up if not already sent";
else
recommendedAction = "Reassess whether to apply or move on";
return { id: a.id, company: a.company, role: a.role, status: a.status, daysSinceUpdate, appliedDaysAgo, recommendedAction };
})
.sort((a, b) => b.daysSinceUpdate - a.daysSinceUpdate);
return json({ summary: { staleCount: stale.length, threshold: params.staleDays, likelyghosted: stale.filter((a) => a.daysSinceUpdate > 45).length }, staleApplications: stale });
}
// ββ stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "stats") {
const apps = db.applications;
const byStatus = {};
for (const a of apps)
byStatus[a.status] = (byStatus[a.status] ?? 0) + 1;
const applied = apps.filter((a) => ["applied", "interview", "offer", "rejected", "withdrawn"].includes(a.status)).length;
const offers = apps.filter((a) => a.status === "offer").length;
const interviewed = apps.filter((a) => ["interview", "offer"].includes(a.status) || (a.status === "rejected" && a.interviewNotes.length > 0)).length;
return json({ total: apps.length, byStatus, conversionRates: { applied: `${applied}/${apps.length}`, interviewed: `${interviewed}/${applied || 1}`, offered: `${offers}/${applied || 1}` }, international: apps.filter((a) => a.isInternational).length, pendingWorkPermit: apps.filter((a) => a.isInternational && a.workPermitStatus === "unknown").length, legitimacyWarnings: apps.filter((a) => a.legitimacyScore >= 0 && a.legitimacyScore < 50).length });
}
// ββ rejections ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "rejections") {
const rejected = db.applications.filter((a) => a.status === "rejected");
if (rejected.length < 3)
return json({ message: `Only ${rejected.length} rejected applications. Need β₯3 for pattern analysis.` });
const withInterviews = rejected.filter((a) => a.interviewNotes.length > 0);
const preInterview = rejected.filter((a) => a.interviewNotes.length === 0);
const byLastRound = {};
for (const a of withInterviews) {
const r = a.interviewNotes[a.interviewNotes.length - 1]?.round || "unknown";
byLastRound[r] = (byLastRound[r] ?? 0) + 1;
}
const byRole = {};
for (const a of rejected) {
const r = a.role.toLowerCase().replace(/senior|junior|lead|staff|principal|sr\.?|jr\.?/gi, "").trim();
byRole[r] = (byRole[r] ?? 0) + 1;
}
const times = rejected.filter((a) => a.appliedDate).map((a) => { const rEntry = a.statusHistory?.find((h) => h.status === "rejected"); return Math.floor((new Date(rEntry?.date ?? a.updatedAt).getTime() - new Date(a.appliedDate).getTime()) / (1000 * 60 * 60 * 24)); });
const avgDays = times.length > 0 ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : null;
return json({ totalRejected: rejected.length, stageBreakdown: { preInterview: preInterview.length, postInterview: withInterviews.length, byLastInterviewRound: byLastRound }, byRoleType: byRole, timing: { avgDaysToRejection: avgDays, fastest: times.length > 0 ? Math.min(...times) : null, slowest: times.length > 0 ? Math.max(...times) : null }, instructions: "Analyze these rejection patterns. (1) Where in pipeline do rejections happen β resume/targeting vs interview prep issue. (2) Are certain role types rejected more? (3) Is timing a factor β quick rejections suggest auto-filtering. (4) Give 3 specific actionable recommendations." });
}
throw new Error(`Unknown mode: ${params.mode}`);
}),
}),
(0, sdk_1.tool)({
name: "salary",
description: (0, sdk_1.text) `
Salary tools for evaluating and negotiating compensation.
mode: "calc" β calculate total compensation (base + bonus + equity + benefits + stipend)
mode: "market" β look up current market salary data (Glassdoor, LinkedIn, levels.fyi, Reddit)
`,
parameters: {
mode: zod_1.z.enum(["calc", "market"]).describe("calc = total comp calculator; market = salary market data lookup"),
role: zod_1.z.string().default("").describe("Job title (market mode: required)"),
location: zod_1.z.string().default("").describe("Job location"),
currency: zod_1.z.string().default("USD"),
applicationId: zod_1.z.string().default("").describe("Optional application ID to save result"),
baseSalary: zod_1.z.coerce.number().default(0).describe("Annual base salary (calc mode)"),
bonusPercent: zod_1.z.coerce.number().min(0).max(100).default(0),
equityValue: zod_1.z.coerce.number().min(0).default(0),
equityVestYears: zod_1.z.coerce.number().int().min(1).max(10).default(4),
signOnBonus: zod_1.z.coerce.number().min(0).default(0),
annualBenefitsValue: zod_1.z.coerce.number().min(0).default(0),
remoteStipend: zod_1.z.coerce.number().min(0).default(0),
currentTotalComp: zod_1.z.coerce.number().min(0).default(0),
yearsOfExperience: zod_1.z.coerce.number().int().min(0).max(40).default(0),
},
implementation: safe_impl("salary", async (params, ctx) => {
if (params.mode === "calc") {
const annualBonus = params.baseSalary * (params.bonusPercent / 100);
const annualEquity = params.equityVestYears > 0 ? params.equityValue / params.equityVestYears : 0;
const firstYearComp = params.baseSalary + annualBonus + annualEquity + params.signOnBonus + params.annualBenefitsValue + params.remoteStipend;
const steadyStateComp = params.baseSalary + annualBonus + annualEquity + params.annualBenefitsValue + params.remoteStipend;
const changeVsCurrent = params.currentTotalComp > 0 ? { absoluteDiff: steadyStateComp - params.currentTotalComp, percentDiff: Math.round(((steadyStateComp - params.currentTotalComp) / params.currentTotalComp) * 100) } : null;
if (params.applicationId) {
const db = await loadDB(dataDir());
const idx = db.applications.findIndex((a) => a.id === params.applicationId);
if (idx !== -1) {
db.applications[idx].salary = `${params.currency} ${params.baseSalary.toLocaleString()} base`;
db.applications[idx].totalComp = `${params.currency} ${Math.round(steadyStateComp).toLocaleString()} TC`;
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
}
}
return json({ location: params.location || "Not specified", breakdown: { baseSalary: params.baseSalary, annualBonus: Math.round(annualBonus), annualEquity: Math.round(annualEquity), signOnBonus: params.signOnBonus, annualBenefitsValue: params.annualBenefitsValue, remoteStipend: params.remoteStipend }, firstYearTotalComp: Math.round(firstYearComp), steadyStateTotalComp: Math.round(steadyStateComp), currency: params.currency, changeVsCurrent });
}
if (params.mode === "market") {
ctx.status(`Checking salary data for: ${params.role}`);
const yr = new Date().getFullYear();
const loc = params.location.trim() || preferredLocation();
const expTag = params.yearsOfExperience > 0 ? ` ${params.yearsOfExperience} years experience` : "";
const searches = [
{ angle: "glassdoor", query: `${params.role} salary ${loc}${expTag} ${yr} site:glassdoor.com` },
{ angle: "linkedin", query: `${params.role} salary ${loc} ${yr} site:linkedin.com/salary` },
{ angle: "levels_fyi", query: `${params.role} compensation ${loc} ${yr} site:levels.fyi` },
{ angle: "survey", query: `${params.role} salary range ${loc}${expTag} ${params.currency} median ${yr} survey` },
{ angle: "reddit", query: `${params.role} salary ${loc}${expTag} ${yr} site:reddit.com` },
];
const results = [];
for (const s of searches) {
try {
results.push({ angle: s.angle, hits: await webSearch(s.query, 5, searchWindow()) });
}
catch {
results.push({ angle: s.angle, hits: [] });
}
}
return json({ role: params.role, location: loc, yearsOfExperience: params.yearsOfExperience || "not specified", currency: params.currency || "inferred from location", year: yr, market_data: results, instructions: `From the market_data above, provide a salary benchmark for ${params.role} in ${loc}. Present low/median/high range citing each source. Include total comp range if present. Flag data gaps or source conflicts.` });
}
throw new Error(`Unknown mode: ${params.mode}`);
}),
}),
// =========================================================================
// COMPANY LEGITIMACY VERIFICATION
// =========================================================================
(0, sdk_1.tool)({
name: "verify_company_legitimacy",
description: (0, sdk_1.text) `
Verify whether a company and job posting are legitimate by checking multiple signals.
Detects common job scam patterns, ghost jobs, and red flags.
Returns a legitimacy score (0β100), red flags found, and green flags.
Common scams: fake companies, too-good-to-pay roles, asking for money/equipment upfront,
vague job descriptions, no verifiable web presence, mismatched domains.
`,
parameters: {
company: zod_1.z.string().describe("Company name to verify"),
jobTitle: zod_1.z.string().default("").describe("Job title from the posting"),
jobDescription: zod_1.z.string().default("").describe("Paste job description for red flag analysis"),
recruitingEmail: zod_1.z.string().default("").describe("Email or domain of the recruiter contact"),
jobUrl: zod_1.z.string().default("").describe("URL of the job posting"),
salary: zod_1.z.string().default("").describe("Stated salary (for outlier detection)"),
},
implementation: safe_impl("verify_company_legitimacy", async (params) => {
const redFlags = [];
const greenFlags = [];
let score = 50; // Neutral baseline β evidence moves the score in either direction
// --- JD analysis ---
if (params.jobDescription) {
const jd = params.jobDescription.toLowerCase();
// Scam red flags in JD
if (/work from home|be your own boss|unlimited earning/i.test(jd)) {
redFlags.push("JD uses MLM/pyramid-scheme language ('be your own boss', 'unlimited earning')");
score -= 20;
}
if (/send.*equipment|purchase.*equipment|reimburse.*gift card|wire transfer/i.test(jd)) {
redFlags.push("JD mentions buying equipment or gift cards β classic advance-fee scam");
score -= 30;
}
if (/no experience (required|needed)|anyone can do/i.test(jd)) {
redFlags.push("JD claims no experience required for what sounds like a skilled role");
score -= 10;
}
if (params.jobDescription.length < 150) {
redFlags.push("Job description is suspiciously short (< 150 chars) β ghost job or scam");
score -= 15;
}
if (/immediately|urgent|asap|start today/i.test(jd)) {
redFlags.push("Unusual urgency in the posting β pressure tactic common in scams");
score -= 5;
}
if (/gmail\.com|yahoo\.com|hotmail\.com/.test(jd)) {
redFlags.push("JD contains a free email domain β legitimate companies use corporate email");
score -= 20;
}
// Green flags in JD
if (/interview process|interview stages|hiring manager/i.test(jd)) {
greenFlags.push("JD mentions a structured interview process");
score += 5;
}
if (/team of \d+|company of \d+|\d+ employees/i.test(jd)) {
greenFlags.push("JD mentions specific team/company size");
score += 5;
}
}
// --- Email domain check ---
if (params.recruitingEmail) {
const emailLower = params.recruitingEmail.toLowerCase();
if (/gmail\.com|yahoo\.com|hotmail\.com|outlook\.com/.test(emailLower)) {
redFlags.push(`Recruiter using free email (${emailLower}) β legitimate companies use corporate email`);
score -= 25;
}
else {
const emailDomain = emailLower.split("@")[1] || "";
const companyDomain = params.company.toLowerCase().replace(/[^a-z0-9]/g, "");
// Only match when we have enough characters to avoid false positives on short names
const matchPrefix = companyDomain.slice(0, Math.min(companyDomain.length, 8));
if (emailDomain && matchPrefix.length >= 4 && !emailDomain.includes(matchPrefix)) {
redFlags.push(`Email domain (${emailDomain}) doesn't match company name β could be impersonation`);
score -= 10;
}
else if (emailDomain) {
greenFlags.push(`Recruiter email domain (${emailDomain}) matches company`);
score += 10;
}
}
}
// --- Salary outlier check ---
if (params.salary) {
// Extract the first number from ranges like "$120,000 - $150,000"
const salaryMatch = params.salary.match(/[\d,]+/);
const salaryNum = salaryMatch ? parseFloat(salaryMatch[0].replace(/,/g, "")) : 0;
if (salaryNum > 0) {
const isUsdSalary = /\$|USD/i.test(params.salary);
if (isUsdSalary && salaryNum > 500000) {
redFlags.push(`Salary of ${params.salary} is unusually high β verify this is not bait`);
score -= 10;
}
else if (isUsdSalary && salaryNum > 10000 && salaryNum < 200000) {
greenFlags.push(`Salary range (${params.salary}) is within normal market bounds`);
score += 5;
}
}
}
// --- Web search verification ---
const searchResults = [];
try {
const verifyQuery = `"${params.company}" company legitimacy reviews OR glassdoor OR linkedin`;
const hits = await webSearch(verifyQuery, 6, searchWindow());
let foundGlassdoor = false, foundLinkedin = false, foundCrunchbase = false;
for (const hit of hits) {
searchResults.push({ title: hit.title, url: hit.url, snippet: hit.snippet });
const url = (hit.url || "").toLowerCase();
if (!foundGlassdoor && url.includes("glassdoor")) {
greenFlags.push("Found Glassdoor listing β company has verifiable employee reviews");
score += 10;
foundGlassdoor = true;
}
if (!foundLinkedin && url.includes("linkedin.com/company")) {
greenFlags.push("Company LinkedIn page found β verifiable company profile");
score += 10;
foundLinkedin = true;
}
if (!foundCrunchbase && url.includes("crunchbase")) {
greenFlags.push("Company on Crunchbase β startup funding history verifiable");
score += 5;
foundCrunchbase = true;
}
if (/scam|fraud|fake|warning/i.test(hit.title + hit.snippet)) {
redFlags.push(`Web search found potential warning: "${hit.title}"`);
score -= 20;
}
}
if (hits.length === 0) {
redFlags.push("No web results found for company β no verifiable online presence");
score -= 20;
}
}
catch { /* web search failed β don't penalize */ }
// Clamp score
score = Math.max(0, Math.min(100, score));
let verdict;
if (score >= 75)
verdict = "LIKELY LEGITIMATE β proceed with normal caution";
else if (score >= 50)
verdict = "UNCERTAIN β verify before sharing personal info";
else if (score >= 25)
verdict = "SUSPICIOUS β multiple red flags, research thoroughly";
else
verdict = "HIGH RISK β likely a scam, do not proceed without verification";
// Persist flags to any matching application records
try {
const db = await loadDB(dataDir());
const companyLower = params.company.toLowerCase();
let saved = false;
for (const app of db.applications) {
if (app.company.toLowerCase() === companyLower) {
app.legitimacyScore = score;
app.legitimacyFlags = redFlags;
app.updatedAt = new Date().toISOString();
saved = true;
}
}
if (saved)
await saveDB(dataDir(), db);
}
catch { /* non-fatal */ }
return json({
company: params.company,
jobTitle: params.jobTitle || "Unknown",
legitimacyScore: score,
verdict,
redFlags,
greenFlags,
webSearchResults: searchResults,
verificationChecklist: [
"β Search the company on LinkedIn and verify employee count matches claims",
"β Check Glassdoor for employee reviews (scams rarely have real reviews)",
"β Verify the company domain was registered > 1 year ago (use WHOIS)",
"β Search '[company name] scam' or '[company name] fraud'",
"β Never pay for training, equipment, or background checks upfront",
"β Video interview with real people (not just text chat) is a good sign",
"β Verify the office address on Google Maps Street View",
],
});
}),
}),
// =========================================================================
// LOCATION
// =========================================================================
(0, sdk_1.tool)({
name: "job_settings",
description: (0, sdk_1.text) `
Plugin configuration helpers.
action: "location" β detect current location via IP-based geolocation
action: "toggle_intl" β show current international search setting and how to change it
`,
parameters: {
action: zod_1.z.enum(["location", "toggle_intl"]).describe("Which setting to query"),
enable: zod_1.z.coerce.boolean().optional().describe("For toggle_intl: true=enable, false=disable"),
},
implementation: safe_impl("job_settings", async ({ action, enable }) => {
if (action === "location") {
const res = await fetch("http://ip-api.com/json/?fields=status,country,regionName,city,lat,lon,isp,query", {
signal: AbortSignal.timeout(8000),
});
if (!res.ok)
throw new Error(`IP geolocation returned HTTP ${res.status}`);
const data = await res.json();
if (data.status !== "success")
throw new Error("IP geolocation failed β check network or try again");
const country = String(data.country ?? "");
const city = String(data.city ?? "");
const region = String(data.regionName ?? "");
const isHome = country.toLowerCase() === homeCountry().toLowerCase();
return json({
city, region, country,
coordinates: { lat: data.lat, lon: data.lon },
isp: data.isp,
ip: data.query,
isHomeCountry: isHome,
suggestedSearchLocation: city && region ? `${city}, ${region}` : country,
note: "Location based on public IP β VPNs affect accuracy.",
});
}
if (action === "toggle_intl") {
return json({
requested: enable ? "enable international" : enable === false ? "disable international" : "show current",
currentSetting: openToInternational(),
message: "To change: set 'Open to International Roles' in plugin settings. Or pass includeInternational: true/false on individual search_jobs calls.",
locationSettings: {
preferredLocation: preferredLocation(),
homeCountry: homeCountry(),
openToInternational: openToInternational(),
},
});
}
throw new Error(`Unknown action: ${action}`);
}),
}),
// =========================================================================
// WORK PERMIT & VISA
// =========================================================================
(0, sdk_1.tool)({
name: "check_work_permit",
description: (0, sdk_1.text) `
Check work permit and visa requirements for working in a destination country
as an Indian citizen (or the citizenship configured in plugin settings).
Covers the most common destination countries for Indian professionals:
USA, UK, Canada, Australia, Germany, Singapore, UAE, Netherlands, Japan, and more.
Returns: visa types available, whether employer sponsorship is needed,
typical processing time, eligibility conditions, salary thresholds,
restrictions (e.g. H-1B lottery, skill shortages), and a practical action plan.
Also does a live web search for any recent policy changes.
`,
parameters: {
destinationCountry: zod_1.z.string()
.describe("Country where the job is located (e.g. 'USA', 'UK', 'Germany', 'Singapore')"),
role: zod_1.z.string().default("").describe("Job title β some visas are role/skill specific"),
annualSalary: zod_1.z.string().default("").describe("Expected salary in destination currency β affects some visa thresholds"),
applicationId: zod_1.z.string().default("").describe("Optional: application ID to save the work permit result to"),
},
implementation: safe_impl("check_work_permit", async ({ destinationCountry, role, annualSalary, applicationId }) => {
const userCitizenship = citizenship();
const dest = destinationCountry.trim();
const yr = new Date().getFullYear();
const roleStr = role ? ` ${role}` : "";
const salaryStr = annualSalary ? ` salary ${annualSalary}` : "";
// Fully dynamic β no static database. Immigration rules change too often to hardcode.
const searches = [
{ angle: "visa_types", query: `work visa ${dest} for ${userCitizenship} citizens ${yr} types requirements` },
{ angle: "sponsorship", query: `${dest} work permit employer sponsorship requirements${roleStr} ${yr}` },
{ angle: "salary_threshold", query: `${dest} work visa minimum salary threshold${roleStr}${salaryStr} ${yr}` },
{ angle: "recent_changes", query: `${dest} immigration policy changes ${yr} tech workers ${userCitizenship}` },
{ angle: "official_source", query: `${dest} official immigration website work permit application process` },
];
const webResults = [];
for (const s of searches) {
try {
const hits = await webSearch(s.query, 6, searchWindow());
webResults.push({ angle: s.angle, query: s.query, results: hits });
}
catch {
webResults.push({ angle: s.angle, query: s.query, results: [] });
}
}
// Mark application record if ID provided (status stays unknown until LLM confirms from search)
if (applicationId) {
try {
const db = await loadDB(dataDir());
const idx = db.applications.findIndex((a) => a.id === applicationId);
if (idx !== -1 && db.applications[idx].workPermitStatus === "unknown") {
db.applications[idx].workPermitNotes = `Work permit research for ${dest} pending β see check_work_permit results. Use update_application to save confirmed status.`;
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
}
}
catch { /* non-fatal */ }
}
return json({
destinationCountry: dest,
citizenship: userCitizenship,
role: role || "any",
salaryContext: annualSalary || null,
web_research: webResults,
instructions: `Based on the live web_research above, provide a complete work permit guide for a ${userCitizenship} citizen working in ${dest}${roleStr}. ` +
"Include: (1) available visa types with current requirements and processing times, " +
"(2) whether employer sponsorship is needed, " +
"(3) current salary thresholds (note: these change annually β cite the source), " +
"(4) key restrictions or quota systems, " +
"(5) a practical 3-step action plan. " +
"ALWAYS cite which search result each fact comes from. " +
"Flag any information that may be outdated and direct the user to the official government source found in official_source results. " +
"If after the user confirms the permit status, instruct them to call update_application to save workPermitStatus and workPermitNotes.",
disclaimer: "Immigration rules change frequently. Always verify with official government sources before applying.",
});
}),
}),
// =========================================================================
// RESUME READING
// =========================================================================
(0, sdk_1.tool)({
name: "read_resume",
description: (0, sdk_1.text) `
Read and parse a resume file. Accepts an absolute file path (PDF, TXT, or MD).
If no path is provided, reads from the configured Resume File Path in plugin settings.
Use this tool FIRST when the user provides a resume path or asks you to look at their resume
before searching for jobs or doing any analysis.
`,
parameters: {
filePath: zod_1.z.string().default("")
.describe("Absolute path to the resume file (e.g. /Users/john/resume.pdf). If blank, uses the configured Resume File Path."),
},
implementation: safe_impl("read_resume", async ({ filePath }) => {
let rp = filePath.trim();
if (!rp) {
rp = resumePath();
}
else {
rp = expandPath(rp);
}
if (!rp)
throw new Error("No file path provided and Resume File Path is not configured in plugin settings.");
const content = await readResumeFile(rp);
if (!content.trim())
throw new Error("Resume file is empty.");
return json({
filePath: rp,
charCount: content.length,
resumeText: content.slice(0, 10000),
instructions: "You have now read the user's resume. Summarize the key details: " +
"name, current role, years of experience, core skills, industries, and notable achievements. " +
"Then ask the user what they'd like to do β search for matching jobs, analyze fit against a JD, or generate cover letters.",
});
}),
}),
// =========================================================================
// =========================================================================
// SAVED SEARCHES
// =========================================================================
(0, sdk_1.tool)({
name: "manage_saved_search",
description: (0, sdk_1.text) `
Save, run, list, and delete recurring job searches.
action: "save" β save a search with query/location/type settings
action: "run" β re-run one or all saved searches; returns new results (already-tracked jobs filtered out)
action: "list" β list all saved searches with last-run info
action: "delete" β delete a saved search by ID
`,
parameters: {
action: zod_1.z.enum(["save", "run", "list", "delete"]).describe("Operation"),
name: zod_1.z.string().default("").describe("Search name (save action)"),
query: zod_1.z.string().default("").describe("Search query (save action)"),
location: zod_1.z.string().default(""),
jobType: zod_1.z.enum(["any", "full_time", "part_time", "contract", "internship", "remote"]).default("any"),
includeInternational: zod_1.z.coerce.boolean().default(false),
searchId: zod_1.z.string().default("").describe("Saved search ID (run: specific search; delete: required)"),
},
implementation: safe_impl("manage_saved_search", async (params) => {
const db = await loadDB(dataDir());
if (params.action === "save") {
if (!params.name.trim())
throw new Error("name is required for action=save.");
if (!params.query.trim())
throw new Error("query is required for action=save.");
const ss = { id: makeId(), name: params.name, query: params.query, location: params.location, jobType: params.jobType, includeInternational: params.includeInternational, createdAt: new Date().toISOString(), lastRunAt: "", lastResultCount: 0 };
db.savedSearches.push(ss);
await saveDB(dataDir(), db);
return json({ success: true, savedSearch: ss });
}
if (params.action === "run") {
if (db.savedSearches.length === 0)
throw new Error("No saved searches. Use manage_saved_search(action='save') to create one.");
const searches = params.searchId ? db.savedSearches.filter((s) => s.id === params.searchId) : db.savedSearches;
if (searches.length === 0)
throw new Error(`Saved search ID '${params.searchId}' not found.`);
const tracked = buildTrackedSets(db.applications);
const allResults = [];
for (const s of searches) {
const loc = s.location || preferredLocation();
const isHome = extractCountry(loc).toLowerCase() === homeCountry().toLowerCase();
const sites = isHome ? homeBoards() : globalBoards();
const typeTag = s.jobType !== "any" ? ` ${s.jobType.replace("_", " ")}` : "";
const baseQuery = `${s.query}${typeTag} ${loc} job`;
let hits = await webSearch(`${baseQuery} ${sites}`, maxResults() + 10, searchWindow());
if (hits.length === 0)
hits = await webSearch(baseQuery, maxResults() + 10, searchWindow());
const { results: filtered } = filterTracked(hits, tracked, maxResults());
const idx = db.savedSearches.findIndex((ss) => ss.id === s.id);
if (idx !== -1) {
db.savedSearches[idx].lastRunAt = new Date().toISOString();
db.savedSearches[idx].lastResultCount = filtered.length;
}
allResults.push({ searchName: s.name, searchId: s.id, results: filtered, newCount: filtered.length });
}
await saveDB(dataDir(), db);
return json({ searchesRun: allResults.length, results: allResults });
}
if (params.action === "list") {
return json({ count: db.savedSearches.length, searches: db.savedSearches.map((s) => ({ id: s.id, name: s.name, query: s.query, location: s.location, jobType: s.jobType, lastRunAt: s.lastRunAt || "never", lastResultCount: s.lastResultCount })) });
}
if (params.action === "delete") {
if (!params.searchId.trim())
throw new Error("searchId is required for action=delete.");
const before = db.savedSearches.length;
db.savedSearches = db.savedSearches.filter((s) => s.id !== params.searchId);
if (db.savedSearches.length === before)
throw new Error(`Saved search ID '${params.searchId}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: params.searchId });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
// =========================================================================
// CONTACTS MANAGEMENT (section header moved up)
// =========================================================================
// CONTACTS MANAGEMENT
// =========================================================================
(0, sdk_1.tool)({
name: "manage_contacts",
description: (0, sdk_1.text) `
Add, remove, or list contacts for a tracked application.
Track recruiters, hiring managers, referrals, and networking connections.
`,
parameters: {
applicationId: zod_1.z.string().describe("Application ID"),
action: zod_1.z.enum(["add", "remove", "list"]).describe("Action to perform"),
name: zod_1.z.string().default("").describe("Contact name (required for add/remove)"),
role: zod_1.z.string().default("").describe("Contact's role/title (e.g. 'Recruiter', 'Hiring Manager')"),
email: zod_1.z.string().default("").describe("Contact email"),
linkedIn: zod_1.z.string().default("").describe("LinkedIn profile URL"),
notes: zod_1.z.string().default("").describe("Notes about this contact"),
},
implementation: safe_impl("manage_contacts", async (params) => {
const db = await loadDB(dataDir());
const idx = db.applications.findIndex((a) => a.id === params.applicationId);
if (idx === -1)
throw new Error(`Application ID '${params.applicationId}' not found.`);
const app = db.applications[idx];
if (params.action === "list") {
return json({ company: app.company, role: app.role, contacts: app.contacts });
}
if (params.action === "add") {
if (!params.name.trim())
throw new Error("Contact name is required.");
app.contacts.push({
name: params.name,
role: params.role,
email: params.email,
linkedIn: params.linkedIn,
notes: params.notes,
});
app.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, action: "added", contact: params.name, totalContacts: app.contacts.length });
}
if (params.action === "remove") {
if (!params.name.trim())
throw new Error("Contact name is required to remove.");
const before = app.contacts.length;
app.contacts = app.contacts.filter((c) => c.name.toLowerCase() !== params.name.toLowerCase());
if (app.contacts.length === before)
throw new Error(`Contact '${params.name}' not found.`);
app.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, action: "removed", contact: params.name, totalContacts: app.contacts.length });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
// =========================================================================
// =========================================================================
// =========================================================================
// DAILY BRIEFING
// =========================================================================
(0, sdk_1.tool)({
name: "daily_briefing",
description: (0, sdk_1.text) `
Morning check-in: runs follow-ups, stale detection, saved searches, and pipeline stats
in one call. Returns a combined briefing with action items.
Use when the user says "morning briefing", "what's new", "daily update", "check-in".
`,
parameters: {
staleDays: zod_1.z.coerce.number().int().min(1).max(90).default(14)
.describe("Days before considering an application stale"),
followUpDays: zod_1.z.coerce.number().int().min(0).max(30).default(7)
.describe("Show follow-ups due within this many days"),
},
implementation: safe_impl("daily_briefing", async ({ staleDays, followUpDays }) => {
const db = await loadDB(dataDir());
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const apps = db.applications;
// --- Pipeline stats ---
const byStatus = {};
for (const a of apps)
byStatus[a.status] = (byStatus[a.status] ?? 0) + 1;
const applied = apps.filter((a) => ["applied", "interview", "offer", "rejected", "withdrawn"].includes(a.status)).length;
const interviewed = apps.filter((a) => ["interview", "offer"].includes(a.status) || (a.status === "rejected" && a.interviewNotes.length > 0)).length;
const offers = apps.filter((a) => a.status === "offer").length;
// --- Follow-ups ---
const cutoffDate = new Date(now);
cutoffDate.setDate(cutoffDate.getDate() + followUpDays);
const cutoffStr = cutoffDate.toISOString().slice(0, 10);
const dateRe = /^\d{4}-\d{2}-\d{2}$/;
const followUps = apps
.filter((a) => !["rejected", "withdrawn"].includes(a.status) && dateRe.test(a.followUpDate ?? "") && a.followUpDate <= cutoffStr)
.map((a) => ({
id: a.id, company: a.company, role: a.role, status: a.status,
followUpDate: a.followUpDate,
overdue: a.followUpDate < todayStr,
nextStep: a.nextStep,
}))
.sort((a, b) => a.followUpDate.localeCompare(b.followUpDate));
// --- Stale applications ---
const staleCutoff = new Date(now.getTime() - staleDays * 24 * 60 * 60 * 1000);
const stale = apps
.filter((a) => ["applied", "interview"].includes(a.status) && new Date(a.updatedAt) < staleCutoff)
.map((a) => ({
id: a.id, company: a.company, role: a.role, status: a.status,
daysSinceUpdate: Math.floor((now.getTime() - new Date(a.updatedAt).getTime()) / (1000 * 60 * 60 * 24)),
}));
// --- Saved searches ---
const searchResults = [];
const tracked = buildTrackedSets(apps);
for (const s of db.savedSearches) {
const loc = s.location || preferredLocation();
const isHome = extractCountry(loc).toLowerCase() === homeCountry().toLowerCase();
const sites = isHome ? homeBoards() : globalBoards();
const typeTag = s.jobType !== "any" ? ` ${s.jobType.replace("_", " ")}` : "";
const baseQuery = `${s.query}${typeTag} ${loc} job`;
try {
let hits = await webSearch(`${baseQuery} ${sites}`, maxResults() + 5, searchWindow());
if (hits.length === 0)
hits = await webSearch(baseQuery, maxResults() + 5, searchWindow());
const { results: filtered } = filterTracked(hits, tracked, maxResults());
const idx = db.savedSearches.findIndex((ss) => ss.id === s.id);
if (idx !== -1) {
db.savedSearches[idx].lastRunAt = now.toISOString();
db.savedSearches[idx].lastResultCount = filtered.length;
}
searchResults.push({ name: s.name, newCount: filtered.length, results: filtered });
}
catch {
searchResults.push({ name: s.name, newCount: 0, results: [] });
}
}
if (db.savedSearches.length > 0)
await saveDB(dataDir(), db);
// --- Network follow-ups ---
const networkOverdue = db.network
.filter((c) => c.followUpDate && c.followUpDate <= todayStr)
.map((c) => ({
id: c.id, name: c.name, company: c.company, relationship: c.relationship,
followUpDate: c.followUpDate, lastContactDate: c.lastContactDate,
}));
return json({
date: todayStr,
pipeline: {
total: apps.length,
byStatus,
conversionRate: applied > 0 ? `${Math.round((offers / applied) * 100)}% appliedβoffer` : "no data",
},
followUps: {
count: followUps.length,
overdue: followUps.filter((f) => f.overdue).length,
items: followUps,
},
staleApplications: {
count: stale.length,
items: stale,
},
networkFollowUps: {
overdueCount: networkOverdue.length,
contacts: networkOverdue,
},
newJobResults: {
searchesRun: searchResults.length,
totalNewJobs: searchResults.reduce((s, r) => s + r.newCount, 0),
bySearch: searchResults,
},
instructions: "Present this as a morning briefing. Lead with urgent items (overdue follow-ups, stale apps, network follow-ups). " +
"Then show new job results from saved searches. End with pipeline health. " +
"Give concrete action items: 'Send follow-up to X', 'Withdraw stale app at Y', 'Reach out to Z', 'Check out 3 new postings'.",
});
}),
}),
// =========================================================================
// JOB COMPARISON
// =========================================================================
(0, sdk_1.tool)({
name: "compare_jobs",
description: (0, sdk_1.text) `
Side-by-side comparison of 2β5 tracked applications.
Compares salary, location, status, fit indicators, legitimacy, contacts,
interview progress, and any other tracked data.
Pass application IDs to compare.
`,
parameters: {
applicationIds: zod_1.z.array(zod_1.z.string()).min(2).max(5)
.describe("Array of 2β5 application IDs to compare"),
},
implementation: safe_impl("compare_jobs", async ({ applicationIds }) => {
const db = await loadDB(dataDir());
const apps = applicationIds.map((id) => {
const app = db.applications.find((a) => a.id === id);
if (!app)
throw new Error(`Application ID '${id}' not found.`);
return app;
});
const comparison = apps.map((a) => ({
id: a.id,
company: a.company,
role: a.role,
location: a.location,
country: a.country,
isInternational: a.isInternational,
status: a.status,
salary: a.salary || "not specified",
totalComp: a.totalComp || "not calculated",
appliedDate: a.appliedDate || "not applied",
interviewRounds: a.interviewNotes.length,
lastInterviewOutcome: a.interviewNotes.length > 0
? a.interviewNotes[a.interviewNotes.length - 1].outcome
: "n/a",
contactCount: a.contacts.length,
legitimacyScore: a.legitimacyScore >= 0 ? a.legitimacyScore : "not checked",
workPermitStatus: a.workPermitStatus,
nextStep: a.nextStep || "none",
followUpDate: a.followUpDate || "not set",
}));
return json({
count: comparison.length,
jobs: comparison,
instructions: "Present this as a side-by-side comparison table. Highlight: " +
"(1) Which has the best compensation. " +
"(2) Which is furthest along in the pipeline. " +
"(3) Any red flags (low legitimacy, missing salary, no contacts). " +
"(4) A recommendation on which to prioritize and why.",
});
}),
}),
// =========================================================================
// BATCH OPERATIONS
// =========================================================================
(0, sdk_1.tool)({
name: "batch_update_applications",
description: (0, sdk_1.text) `
Bulk-update multiple applications at once. Useful for:
- Withdrawing all stale applications
- Marking multiple as rejected
- Cleaning up the pipeline
Pass an array of application IDs and the fields to update on all of them.
`,
parameters: {
applicationIds: zod_1.z.array(zod_1.z.string()).min(1)
.describe("Array of application IDs to update"),
status: zod_1.z.enum(["saved", "applied", "interview", "offer", "rejected", "withdrawn"]).optional()
.describe("New status for all selected applications"),
rejectionReason: zod_1.z.string().optional()
.describe("Rejection reason (when bulk-marking as rejected)"),
notes: zod_1.z.string().optional()
.describe("Notes to append (not replace) on all selected applications"),
},
implementation: safe_impl("batch_update_applications", async ({ applicationIds, status, rejectionReason, notes }) => {
if (!status && !rejectionReason && !notes)
throw new Error("Nothing to update. Provide at least one of: status, rejectionReason, notes.");
const db = await loadDB(dataDir());
const updated = [];
const notFound = [];
for (const id of applicationIds) {
const idx = db.applications.findIndex((a) => a.id === id);
if (idx === -1) {
notFound.push(id);
continue;
}
const app = db.applications[idx];
if (status && status !== app.status) {
app.statusHistory.push({ status, date: todayStr() });
app.status = status;
if (status === "applied")
stampAppliedDate(app);
}
if (rejectionReason)
app.rejectionReason = rejectionReason;
if (notes)
app.notes = app.notes ? `${app.notes}\n${notes}` : notes;
app.updatedAt = new Date().toISOString();
updated.push(`${app.company} β ${app.role}`);
}
await saveDB(dataDir(), db);
return json({
success: true,
updatedCount: updated.length,
updated,
notFound: notFound.length > 0 ? notFound : null,
changes: { status: status ?? "unchanged", rejectionReason: rejectionReason ?? "unchanged", notesAppended: !!notes },
});
}),
}),
// =========================================================================
// NETWORKING
// =========================================================================
(0, sdk_1.tool)({
name: "manage_network",
description: (0, sdk_1.text) `
Professional network tracker β referrers, hiring managers, alumni, ex-colleagues.
action: "add" β add a new contact
action: "update" β update contact fields by ID
action: "list" β list network with optional filters
action: "log" β log an interaction (updates lastContactDate)
action: "delete" β delete a contact by ID
action: "referrers" β find contacts who can refer you to a specific company
`,
parameters: {
action: zod_1.z.enum(["add", "update", "list", "log", "delete", "referrers"]).describe("Operation"),
id: zod_1.z.string().default("").describe("Contact ID (update/log/delete)"),
name: zod_1.z.string().default(""),
company: zod_1.z.string().default(""),
role: zod_1.z.string().default(""),
email: zod_1.z.string().default(""),
linkedIn: zod_1.z.string().default(""),
relationship: zod_1.z.enum(["cold", "warm", "strong", "referrer"]).optional(),
source: zod_1.z.string().default(""),
targetCompanies: zod_1.z.array(zod_1.z.string()).default([]),
notes: zod_1.z.string().default("").describe("For update/log: text to APPEND"),
tags: zod_1.z.array(zod_1.z.string()).default([]),
followUpDate: zod_1.z.string().default(""),
filterRelationship: zod_1.z.enum(["cold", "warm", "strong", "referrer", "all"]).default("all"),
filterCompany: zod_1.z.string().default(""),
filterTag: zod_1.z.string().default(""),
overdueOnly: zod_1.z.coerce.boolean().default(false),
search: zod_1.z.string().default(""),
summary: zod_1.z.string().default("").describe("What happened in the interaction (log action)"),
upgradeRelationship: zod_1.z.coerce.boolean().default(false),
followUpInDays: zod_1.z.coerce.number().int().min(0).max(90).default(0),
targetCompany: zod_1.z.string().default("").describe("Company to find referrers for (referrers action)"),
},
implementation: safe_impl("manage_network", async (params) => {
const db = await loadDB(dataDir());
if (params.action === "add") {
if (!params.name.trim())
throw new Error("name is required for action=add.");
const contact = {
id: makeId(), name: params.name, company: params.company, role: params.role,
email: params.email, linkedIn: params.linkedIn,
relationship: params.relationship ?? "cold", source: params.source,
targetCompanies: params.targetCompanies, lastContactDate: todayStr(),
followUpDate: params.followUpDate, notes: params.notes, tags: params.tags,
linkedApplicationIds: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
};
db.network.push(contact);
await saveDB(dataDir(), db);
return json({ success: true, contact });
}
if (params.action === "update") {
if (!params.id.trim())
throw new Error("id is required for action=update.");
const idx = db.network.findIndex((c) => c.id === params.id);
if (idx === -1)
throw new Error(`Network contact '${params.id}' not found.`);
const c = db.network[idx];
if (params.name)
c.name = params.name;
if (params.company)
c.company = params.company;
if (params.role)
c.role = params.role;
if (params.email)
c.email = params.email;
if (params.linkedIn)
c.linkedIn = params.linkedIn;
if (params.relationship)
c.relationship = params.relationship;
if (params.source)
c.source = params.source;
if (params.targetCompanies.length > 0)
c.targetCompanies = params.targetCompanies;
if (params.followUpDate)
c.followUpDate = params.followUpDate;
if (params.notes)
c.notes = c.notes ? `${c.notes}\n${params.notes}` : params.notes;
if (params.tags.length > 0)
c.tags = params.tags;
c.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, contact: c });
}
if (params.action === "list") {
let contacts = db.network;
if (params.filterRelationship !== "all")
contacts = contacts.filter((c) => c.relationship === params.filterRelationship);
if (params.filterCompany) {
const kw = params.filterCompany.toLowerCase();
contacts = contacts.filter((c) => c.company.toLowerCase().includes(kw));
}
if (params.filterTag) {
const kw = params.filterTag.toLowerCase();
contacts = contacts.filter((c) => c.tags.some((t) => t.toLowerCase().includes(kw)));
}
if (params.search) {
const kw = params.search.toLowerCase();
contacts = contacts.filter((c) => c.name.toLowerCase().includes(kw) || c.company.toLowerCase().includes(kw) || c.notes.toLowerCase().includes(kw));
}
const today = todayStr();
if (params.overdueOnly)
contacts = contacts.filter((c) => c.followUpDate && c.followUpDate <= today);
contacts = contacts.slice().sort((a, b) => { const ao = a.followUpDate && a.followUpDate <= today ? 0 : 1; const bo = b.followUpDate && b.followUpDate <= today ? 0 : 1; return ao !== bo ? ao - bo : (a.followUpDate && b.followUpDate ? a.followUpDate.localeCompare(b.followUpDate) : a.lastContactDate.localeCompare(b.lastContactDate)); });
const stats = { total: db.network.length, cold: db.network.filter((c) => c.relationship === "cold").length, warm: db.network.filter((c) => c.relationship === "warm").length, strong: db.network.filter((c) => c.relationship === "strong").length, referrer: db.network.filter((c) => c.relationship === "referrer").length, overdueFollowUps: db.network.filter((c) => c.followUpDate && c.followUpDate <= today).length };
return json({ stats, contacts: contacts.map((c) => ({ id: c.id, name: c.name, company: c.company, role: c.role, relationship: c.relationship, lastContactDate: c.lastContactDate, followUpDate: c.followUpDate || "not set", overdue: c.followUpDate ? c.followUpDate <= today : false, targetCompanies: c.targetCompanies, tags: c.tags })) });
}
if (params.action === "log") {
if (!params.id.trim())
throw new Error("id is required for action=log.");
if (!params.summary.trim())
throw new Error("summary is required for action=log.");
const idx = db.network.findIndex((c) => c.id === params.id);
if (idx === -1)
throw new Error(`Network contact '${params.id}' not found.`);
const c = db.network[idx];
const order = ["cold", "warm", "strong", "referrer"];
if (params.upgradeRelationship) {
const cur = order.indexOf(c.relationship);
if (cur < order.length - 1)
c.relationship = order[cur + 1];
}
c.lastContactDate = todayStr();
if (params.followUpInDays > 0) {
const d = new Date();
d.setDate(d.getDate() + params.followUpInDays);
c.followUpDate = d.toISOString().slice(0, 10);
}
c.notes = c.notes ? `${c.notes}\n[${todayStr()}] ${params.summary}` : `[${todayStr()}] ${params.summary}`;
c.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, contact: c.name, relationship: c.relationship, followUpDate: c.followUpDate || "not set" });
}
if (params.action === "delete") {
if (!params.id.trim())
throw new Error("id is required for action=delete.");
const before = db.network.length;
db.network = db.network.filter((c) => c.id !== params.id);
if (db.network.length === before)
throw new Error(`Network contact '${params.id}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: params.id });
}
if (params.action === "referrers") {
const kw = (params.targetCompany || params.company).toLowerCase();
if (!kw)
throw new Error("targetCompany is required for action=referrers.");
const candidates = db.network.filter((c) => c.company.toLowerCase().includes(kw) ||
c.targetCompanies.some((t) => t.toLowerCase().includes(kw))).map((c) => ({
id: c.id, name: c.name, company: c.company, role: c.role,
relationship: c.relationship, email: c.email, linkedIn: c.linkedIn,
canRefer: c.company.toLowerCase().includes(kw),
connectedTo: c.targetCompanies.filter((t) => t.toLowerCase().includes(kw)),
}));
return json({ targetCompany: params.targetCompany, candidates, instructions: "Present referral options by relationship strength. For 'strong'/'referrer': suggest asking for a direct referral. For 'warm': suggest a catch-up first. For 'cold': suggest warming up with a value-add message before asking." });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
];
return tools;
};
exports.toolsProvider = toolsProvider;
toolsProvider.js
"use strict";
/**
* Job Search Plugin β toolsProvider (23 tools)
*
* Tools:
* Search Β· search_jobs(channel), fetch_job_page
* ATS Portals Β· scan_portals(mode)
* Applications Β· manage_application(action), list_applications
* Insights Β· application_insights(mode)
* Salary Β· salary(mode)
* Export Β· export_applications(format)
* Network Β· manage_network(action)
* Saved Search Β· manage_saved_search(action)
* Settings Β· job_settings(action)
* Analysis Β· analyze_job_description, match_resume_to_job, verify_company_legitimacy
* Resume Β· read_resume
* Generation Β· generate_cover_letter, generate_resume_bullets, prepare_interview_questions
* Visa Β· check_work_permit
* Briefing Β· daily_briefing
* Comparison Β· compare_jobs
* Batch Β· batch_update_applications
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.toolsProvider = void 0;
const sdk_1 = require("@lmstudio/sdk");
const promises_1 = require("fs/promises");
const pdf_parse_1 = __importDefault(require("pdf-parse"));
const path_1 = require("path");
const os_1 = require("os");
const search_1 = require("./search");
const portals_1 = require("./portals");
const zod_1 = require("zod");
const config_1 = require("./config");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function json(obj) {
return JSON.stringify(obj, null, 2);
}
function safe_impl(name, fn) {
return async (params, ctx) => {
if (ctx.signal.aborted) {
return JSON.stringify({ tool_error: true, tool: name, error: "cancelled" });
}
try {
return await fn(params, ctx);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return JSON.stringify({
tool_error: true,
tool: name,
error: msg,
hint: "Read the error above, fix the parameter causing the issue, and retry the tool call.",
}, null, 2);
}
};
}
function stripHtml(html) {
return html
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/ /g, " ").replace(/"/g, '"').replace(/'/g, "'")
.replace(/[ \t]+/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function getDataDir(configPath) {
return expandPath(configPath.trim() || (0, path_1.join)((0, os_1.homedir)(), "job-search-data"));
}
// Expand leading ~/ to the user's home directory (Node's readFile doesn't do this)
function expandPath(p) {
if (p.startsWith("~/"))
return (0, path_1.join)((0, os_1.homedir)(), p.slice(2));
if (p === "~")
return (0, os_1.homedir)();
return p;
}
// Read a resume file β supports plain text (.txt, .md) and PDF
async function readResumeFile(filePath) {
const buf = await (0, promises_1.readFile)(filePath);
if (filePath.toLowerCase().endsWith(".pdf")) {
const parsed = await (0, pdf_parse_1.default)(buf);
return parsed.text;
}
return buf.toString("utf8");
}
function dbPath(dataDir) {
return (0, path_1.join)(dataDir, "applications.json");
}
async function loadDB(dataDir) {
try {
const raw = await (0, promises_1.readFile)(dbPath(dataDir), "utf8");
const db = JSON.parse(raw);
if (!db.applications)
db.applications = [];
if (!db.savedSearches)
db.savedSearches = [];
if (!db.network)
db.network = [];
// Hydrate fields added after initial release so old records don't crash
for (const app of db.applications) {
app.postedDate ??= "";
app.followUpDate ??= "";
app.totalComp ??= "";
// Migrate old string[] contacts to new structured format
if (Array.isArray(app.contacts) && typeof app.contacts[0] === "string") {
app.contacts = app.contacts.map((c) => ({
name: c, role: "", email: "", linkedIn: "", notes: "",
}));
}
app.contacts ??= [];
app.country ??= "";
app.isInternational ??= false;
app.workPermitStatus ??= "unknown";
app.workPermitNotes ??= "";
app.legitimacyScore ??= -1;
app.legitimacyFlags ??= [];
app.interviewNotes ??= [];
app.rejectionReason ??= "";
app.statusHistory ??= [];
}
return db;
}
catch (err) {
if (err.code === "ENOENT") {
return { applications: [], savedSearches: [], network: [] };
}
throw err;
}
}
const BACKUP_RETENTION = 3;
async function saveDB(dataDir, db) {
await (0, promises_1.mkdir)(dataDir, { recursive: true });
const dbFile = dbPath(dataDir);
const dbName = dbFile.split("/").pop();
try {
await (0, promises_1.copyFile)(dbFile, `${dbFile}.bak.${Date.now()}`);
}
catch (e) {
if (e.code !== "ENOENT")
throw e;
}
await (0, promises_1.writeFile)(dbFile, JSON.stringify(db, null, 2), "utf8");
const entries = await (0, promises_1.readdir)(dataDir);
const backups = entries
.filter((n) => n.startsWith(`${dbName}.bak.`))
.sort()
.reverse();
for (const stale of backups.slice(BACKUP_RETENTION)) {
await (0, promises_1.unlink)((0, path_1.join)(dataDir, stale));
}
}
function makeId() {
return crypto.randomUUID();
}
function todayStr() {
return new Date().toISOString().slice(0, 10);
}
/** Set appliedDate and followUpDate (+7 days) if not already set. Mutates in place. */
function stampAppliedDate(app) {
if (!app.appliedDate) {
app.appliedDate = todayStr();
}
if (!app.followUpDate) {
const fu = new Date();
fu.setDate(fu.getDate() + 7);
app.followUpDate = fu.toISOString().slice(0, 10);
}
}
/** Build sets for dedup filtering against already-tracked applications. */
function buildTrackedSets(apps) {
return {
urls: new Set(apps.map((a) => a.url.toLowerCase()).filter(Boolean)),
keys: new Set(apps.map((a) => `${a.company.toLowerCase().replace(/[^a-z0-9]/g, "")}::${a.role.toLowerCase().replace(/[^a-z0-9]/g, "")}`)),
};
}
/** Filter out search hits that match already-tracked applications by URL or fuzzy company+role. */
function filterTracked(hits, tracked, limit) {
const filtered = hits.filter((h) => {
if (tracked.urls.has(h.url.toLowerCase()))
return false;
const titleNorm = h.title.toLowerCase().replace(/[^a-z0-9]/g, "");
for (const key of tracked.keys) {
const [comp, role] = key.split("::");
if (comp.length >= 3 && role.length >= 3 && titleNorm.includes(comp) && titleNorm.includes(role))
return false;
}
return true;
});
return { results: filtered.slice(0, limit), skipped: hits.length - filtered.length };
}
// ---------------------------------------------------------------------------
// Tools Provider
// ---------------------------------------------------------------------------
const toolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(config_1.pluginConfigSchematics);
const dataDir = () => getDataDir(cfg.get("dataPath"));
const maxResults = () => cfg.get("maxSearchResults");
const resumePath = () => expandPath(cfg.get("resumePath").trim());
const preferredLocation = () => cfg.get("preferredLocation").trim() || "India";
const homeCountry = () => cfg.get("homeCountry").trim() || "India";
const citizenship = () => cfg.get("citizenship").trim() || "Indian";
const openToInternational = () => cfg.get("openToInternational");
// Convert a comma-separated domain list to a DuckDuckGo site: filter string
function toSiteFilter(domains, fallback) {
const parts = (domains.trim() || fallback)
.split(",")
.map((d) => `site:${d.trim()}`)
.filter((s) => s !== "site:");
return parts.join(" OR ");
}
const homeBoards = () => toSiteFilter(cfg.get("homeJobBoards"), "naukri.com,linkedin.com,foundit.in,instahyre.com,iimjobs.com,wellfound.com,shine.com");
const globalBoards = () => toSiteFilter(cfg.get("globalJobBoards"), "linkedin.com,indeed.com,glassdoor.com,lever.co,greenhouse.io,wellfound.com");
const searxng = () => cfg.get("searxngUrl").trim() || undefined;
const searchWindow = () => {
const v = cfg.get("searchRecencyWindow").trim().toLowerCase();
return (["day", "week", "month", "year"].includes(v) ? v : undefined);
};
const webSearch = (query, max, timeRange) => (0, search_1.webSearch)(query, max, 10_000, searxng(), timeRange ?? searchWindow());
// Returns true when the job country differs from the user's home country
function isInternationalJob(jobCountry) {
if (!jobCountry)
return false;
return jobCountry.toLowerCase() !== homeCountry().toLowerCase();
}
// Normalise a raw location string to a country name best-effort
function extractCountry(location) {
const l = location.toLowerCase();
if (/\bindia\b|bengaluru|bangalore|mumbai|delhi|hyderabad|pune|chennai|kolkata|noida|gurugram|gurgaon/.test(l))
return "India";
if (/\busa\b|\bunited states\b|\bamerican\b|new york|san francisco|seattle|boston|austin|chicago|remote us/.test(l))
return "USA";
if (/\buk\b|\bunited kingdom\b|london|manchester|edinburgh/.test(l))
return "UK";
if (/\bcanada\b|toronto|vancouver|montreal/.test(l))
return "Canada";
if (/\baustralia\b|sydney|melbourne|brisbane/.test(l))
return "Australia";
if (/\bgermany\b|berlin|munich|hamburg/.test(l))
return "Germany";
if (/\bsingapore\b/.test(l))
return "Singapore";
if (/\buae\b|dubai|abu dhabi/.test(l))
return "UAE";
if (/\bnetherlands\b|amsterdam/.test(l))
return "Netherlands";
if (/\bfrance\b|paris/.test(l))
return "France";
if (/\bjapan\b|tokyo/.test(l))
return "Japan";
if (/\bremote\b/.test(l))
return "Remote";
return location; // Return as-is if unrecognised
}
const tools = [
// =========================================================================
// SEARCH
// =========================================================================
(0, sdk_1.tool)({
name: "search_jobs",
description: (0, sdk_1.text) `
Search for job listings across all channels.
channel options:
boards β standard job boards (Naukri, LinkedIn, Indeed, Glassdoor etc.)
company β research a company: culture, reviews, funding, tech stack
hidden β unadvertised roles: LinkedIn/X hiring posts, HN Who's Hiring, Wellfound, YC, Reddit, regional boards
remote β verified remote boards via Remotive API + Remote OK API + WWR/Remote.co
funded β recently funded startups likely hiring (TechCrunch + Crunchbase)
government β government/public sector portals (India: NCS/UPSC/SSC/PSUs/Railways; USA: USAJobs; UK: Civil Service; AU/EU/CA)
academic β research and university boards (IIT/IISc/CSIR/DST for India; jobs.ac.uk; Chronicle; Nature Careers; EURAXESS)
`,
parameters: {
query: zod_1.z.string().describe("Role, skills, keywords, or company name"),
channel: zod_1.z.enum(["boards", "company", "hidden", "remote", "funded", "government", "academic"])
.default("boards").describe("Which market channel to search"),
location: zod_1.z.string().default("").describe("Location override. Leave blank to use plugin default."),
includeInternational: zod_1.z.coerce.boolean().optional()
.describe("Override international toggle for this search (boards channel only)"),
jobType: zod_1.z.enum(["any", "full_time", "part_time", "contract", "internship", "remote"]).default("any")
.describe("Employment type filter (boards channel only)"),
max: zod_1.z.coerce.number().int().min(1).max(20).optional().describe("Max results"),
strategy: zod_1.z.enum([
"all", "hiring_posts", "career_pages", "referral_network", "hn_whoishiring",
"wellfound", "yc_startups", "reddit", "product_hunt", "regional",
"tech_contract", "dev_community",
]).default("all").describe("Hidden market strategy (hidden channel only)"),
source: zod_1.z.enum(["all", "remotive", "remoteok", "boards"]).default("all")
.describe("Remote source (remote channel only)"),
stage: zod_1.z.enum(["seed", "series_a", "series_b", "series_c", "any"]).default("any")
.describe("Funding stage (funded channel only)"),
sector: zod_1.z.string().default("").describe("Sector/industry for funded/government/academic channels"),
country: zod_1.z.enum(["india", "usa", "uk", "australia", "eu", "canada", "all"]).default("india")
.describe("Country for government channel"),
field: zod_1.z.string().default("").describe("Research field (academic channel only)"),
region: zod_1.z.enum(["global", "india", "usa", "uk", "europe", "us", "eu", "remote"]).default("global")
.describe("Region (academic/remote/funded channels)"),
},
implementation: safe_impl("search_jobs", async (params, ctx) => {
const { channel } = params;
// ββ boards ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "boards") {
ctx.status(`Searching jobs: ${params.query}`);
const limit = params.max ?? maxResults();
const loc = params.location.trim() || preferredLocation();
const country = extractCountry(loc);
const international = isInternationalJob(country) && country !== "Remote";
const intlAllowed = params.includeInternational ?? openToInternational();
if (international && !intlAllowed) {
return json({
blocked: true,
reason: `International search (${country}) is disabled. Enable "Open to International Roles" in plugin settings, or pass includeInternational: true to override.`,
suggestion: `To search locally, try location="${preferredLocation()}" or leave it blank.`,
});
}
const typeTag = params.jobType !== "any" ? ` ${params.jobType.replace("_", " ")}` : "";
const locTag = loc ? ` ${loc}` : "";
const baseQuery = `${params.query}${typeTag}${locTag} job`;
const isHomeSearch = extractCountry(loc).toLowerCase() === homeCountry().toLowerCase();
const sites = isHomeSearch ? homeBoards() : globalBoards();
const fetchLimit = limit + 10;
let hits = await webSearch(`${baseQuery} ${sites}`, fetchLimit, searchWindow());
if (hits.length === 0)
hits = await webSearch(baseQuery, fetchLimit, searchWindow());
const db = await loadDB(dataDir());
const tracked = buildTrackedSets(db.applications);
const { results: filtered, skipped } = filterTracked(hits, tracked, limit);
return json({
query: params.query, location: loc, country, isInternational: international,
workPermitNote: international ? `International search (${country}). Run check_work_permit for visa requirements.` : null,
jobBoards: sites.split(" OR ").map((s) => s.replace("site:", "")),
results: filtered,
alreadyTracked: skipped > 0 ? `${skipped} result(s) hidden β already tracked.` : null,
});
}
// ββ company βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "company") {
ctx.status(`Researching company: ${params.query}`);
const q = params.sector
? `${params.query} company ${params.sector}`
: `${params.query} company culture reviews tech stack ${new Date().getFullYear()}`;
const hits = await webSearch(q, maxResults(), "year");
return json({ company: params.query, focus: params.sector || "general", results: hits });
}
// ββ hidden βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "hidden") {
ctx.status(`Finding hidden ${params.query} opportunitiesβ¦`);
const strategy = params.strategy;
const loc = params.location ? ` ${params.location}` : "";
const ind = params.sector ? ` ${params.sector}` : "";
const regionKey = (() => {
const l = params.location.toLowerCase();
if (/india|bangalore|mumbai|delhi|hyderabad|pune|chennai/.test(l))
return "india";
if (/europe|germany|france|uk|netherlands|spain|poland/.test(l))
return "europe";
if (/singapore|malaysia|philippines|thailand|indonesia|vietnam|japan|australia/.test(l))
return "sea";
if (/brazil|mexico|colombia|argentina|chile|peru/.test(l))
return "latam";
if (/uae|dubai|saudi|qatar|bahrain|kuwait|oman/.test(l))
return "me";
if (/nigeria|kenya|ghana|south africa|egypt/.test(l))
return "africa";
if (/remote/.test(l))
return "remote";
return "startup";
})();
const searches = [
{ key: "hiring_posts", query: `"we're hiring" OR "we are hiring" ${params.query}${ind}${loc} site:linkedin.com OR site:twitter.com OR site:x.com`, active: strategy === "all" || strategy === "hiring_posts" },
{ key: "career_pages", query: `${params.query}${ind}${loc} "careers" OR "jobs" -site:linkedin.com -site:indeed.com -site:glassdoor.com`, active: strategy === "all" || strategy === "career_pages" },
{ key: "hn_whoishiring", query: `site:news.ycombinator.com "Who is Hiring" ${params.query}${ind}`, active: strategy === "all" || strategy === "hn_whoishiring" },
{ key: "referral_network", query: `${params.query}${ind}${loc} "looking for" OR "open to referrals" OR "DM me" hiring`, active: strategy === "all" || strategy === "referral_network" },
{ key: "wellfound", query: `site:wellfound.com/jobs ${params.query}${ind}${loc}`, active: strategy === "all" || strategy === "wellfound" },
{ key: "yc_startups", query: `site:workatastartup.com ${params.query}${ind}`, active: strategy === "all" || strategy === "yc_startups" },
{ key: "reddit", query: `(site:reddit.com/r/forhire OR site:reddit.com/r/cscareerquestions) ${params.query} hiring`, active: strategy === "all" || strategy === "reddit" },
{ key: "product_hunt", query: `site:producthunt.com ${params.query}${ind} hiring OR careers`, active: strategy === "all" || strategy === "product_hunt" },
{ key: "regional", query: `${params.query}${ind} ${(portals_1.REGIONAL_BOARDS[regionKey] ?? portals_1.REGIONAL_BOARDS.startup).join(" OR ")}`, active: strategy === "all" || strategy === "regional" },
{ key: "tech_contract", query: `${params.query}${ind}${loc} site:dice.com OR site:hired.com OR site:arc.dev/remote-jobs`, active: strategy === "all" || strategy === "tech_contract" },
{ key: "dev_community", query: `${params.query}${ind}${loc} site:dev.to/jobs OR site:lobste.rs OR site:hashnode.com "hiring"`, active: strategy === "all" || strategy === "dev_community" },
];
const results = {};
for (const s of searches) {
if (!s.active)
continue;
if (ctx.signal.aborted)
break;
ctx.status(`Searching ${s.key}β¦`);
try {
results[s.key] = await webSearch(s.query, maxResults(), searchWindow());
}
catch {
results[s.key] = [];
}
}
return json({ query: params.query, sector: params.sector || "any", location: params.location || "any", regionDetected: regionKey, results });
}
// ββ remote βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "remote") {
const src = params.source;
const results = {};
if (src === "all" || src === "remotive") {
ctx.status("Querying Remotive APIβ¦");
try {
results.remotive = await (0, portals_1.fetchRemotive)(params.query, params.max ?? 50, ctx.signal);
}
catch (e) {
results.remotive = { error: e instanceof Error ? e.message : String(e) };
}
}
if (src === "all" || src === "remoteok") {
ctx.status("Querying Remote OK APIβ¦");
try {
results.remoteok = await (0, portals_1.fetchRemoteOK)(params.query, ctx.signal);
}
catch (e) {
results.remoteok = { error: e instanceof Error ? e.message : String(e) };
}
}
if (src === "all" || src === "boards") {
ctx.status("Searching remote job boardsβ¦");
try {
results.boards = await webSearch(`${params.query} ${portals_1.REGIONAL_BOARDS.remote.join(" OR ")}`, params.max ?? maxResults(), searchWindow());
}
catch (e) {
results.boards = { error: e instanceof Error ? e.message : String(e) };
}
}
const totalJobs = [
...(Array.isArray(results.remotive) ? results.remotive : []),
...(Array.isArray(results.remoteok) ? results.remoteok : []),
...(Array.isArray(results.boards) ? results.boards : []),
].length;
return json({ query: params.query, source: src, totalJobs, results });
}
// ββ funded βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "funded") {
const stageLabel = params.stage === "any" ? "" : ` ${params.stage.replace("_", " ")}`;
const loc = params.location ? ` ${params.location}` : "";
const sec = params.sector || params.query;
const yr = new Date().getFullYear();
const q = `${sec}${stageLabel} startup funded ${yr - 1} OR ${yr}${loc} "million" hiring`;
const results = await webSearch(q, maxResults(), "year");
const tcQuery = `site:techcrunch.com ${sec}${stageLabel} raised funding ${yr - 1} OR ${yr}`;
const techCrunchResults = await webSearch(tcQuery, 5, "year");
return json({
sector: sec, stage: params.stage, location: params.location || "global",
generalResults: results, techCrunchResults,
why: "Companies typically hire 30β60% more in the 6 months after a funding round.",
nextSteps: ["Check each company's LinkedIn and careers page", "Find the hiring manager on LinkedIn and reach out directly"],
});
}
// ββ government βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "government") {
const sec = params.sector ? ` ${params.sector}` : "";
const role = params.query;
const boards = {
india: [
{ key: "ncs_portal", query: `site:ncs.gov.in ${role}${sec}` },
{ key: "employment_news", query: `site:employmentnews.gov.in ${role}${sec}` },
{ key: "upsc", query: `site:upsc.gov.in ${role}${sec}` },
{ key: "ssc", query: `site:ssc.nic.in ${role}${sec}` },
{ key: "ibps_banking", query: `(site:ibps.in OR site:sbi.co.in OR site:rbi.org.in) ${role}${sec}` },
{ key: "psu_jobs", query: `${role}${sec} site:isro.gov.in OR site:drdo.gov.in OR site:bhel.in OR site:ntpc.co.in OR site:ongcindia.com OR site:hal-india.co.in` },
{ key: "railways_rrb", query: `${role}${sec} site:indianrailways.gov.in OR site:rrbcdg.gov.in` },
{ key: "state_govts", query: `${role}${sec} state government jobs site:gov.in recruitment 2025` },
{ key: "defence_forces", query: `${role}${sec} site:joinindianarmy.nic.in OR site:joinindiannavy.gov.in OR site:careerindianairforce.cdac.in` },
],
usa: [{ key: "usajobs", query: `site:usajobs.gov ${role}${sec}` }, { key: "clearance", query: `site:clearancejobs.com ${role}${sec}` }],
uk: [{ key: "civil_service", query: `site:civilservicejobs.service.gov.uk ${role}${sec}` }, { key: "nhs", query: `site:jobs.nhs.uk ${role}${sec}` }],
australia: [{ key: "aps_jobs", query: `site:apsjobs.gov.au ${role}${sec}` }],
eu: [{ key: "epso", query: `site:epso.europa.eu ${role}${sec}` }],
canada: [{ key: "gc_jobs", query: `site:jobs-emplois.gc.ca ${role}${sec}` }],
};
const targets = params.country === "all"
? Object.values(boards).flat()
: (boards[params.country] ?? []);
const results = {};
for (const t of targets) {
if (ctx.signal.aborted)
break;
ctx.status(`Searching ${t.key}β¦`);
try {
results[t.key] = await webSearch(t.query, maxResults(), searchWindow());
}
catch {
results[t.key] = [];
}
}
const indiaTips = params.country === "india" || params.country === "all" ? {
apply_ncs: "Register at ncs.gov.in β India's official National Career Service portal.",
psu_direct: "PSUs like ISRO, DRDO, BHEL post on their own sites weeks before aggregators.",
banking_cycle: "IBPS runs annual PO/Clerk/SO exams β check ibps.in for the notification calendar.",
rrb_ntpc: "RRBs run large periodic drives β check indianrailways.gov.in and regional RRB sites.",
} : undefined;
return json({ query: role, country: params.country, sector: params.sector || "any", results, ...(indiaTips ? { indiaTips } : {}) });
}
// ββ academic βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (channel === "academic") {
const fld = params.field ? ` ${params.field}` : "";
const role = params.query;
const academicRegion = params.region;
const boards = [
{ key: "iit_isc_portals", query: `${role}${fld} site:iitb.ac.in OR site:iitd.ac.in OR site:iisc.ac.in OR site:iitm.ac.in recruitment`, regions: ["india", "global"] },
{ key: "iiser_portals", query: `${role}${fld} site:iiserpune.ac.in OR site:iiserbhopal.ac.in OR site:iiserkolkata.ac.in recruitment`, regions: ["india", "global"] },
{ key: "dst_serb", query: `${role}${fld} site:dst.gov.in OR site:serb.gov.in fellowship position`, regions: ["india", "global"] },
{ key: "csir_dbt_icmr", query: `${role}${fld} site:csir.res.in OR site:dbtindia.gov.in OR site:icmr.gov.in recruitment`, regions: ["india", "global"] },
{ key: "jobs_ac_uk", query: `site:jobs.ac.uk ${role}${fld}`, regions: ["uk", "global"] },
{ key: "chronicle", query: `site:chronicle.com/jobs ${role}${fld}`, regions: ["usa", "global"] },
{ key: "inside_higher_ed", query: `site:insidehighered.com/jobs ${role}${fld}`, regions: ["usa", "global"] },
{ key: "nature_careers", query: `site:nature.com/naturecareers ${role}${fld}`, regions: ["global", "uk", "europe"] },
{ key: "science_careers", query: `site:science.org/careers/jobs ${role}${fld}`, regions: ["global", "usa"] },
{ key: "euraxess", query: `site:euraxess.net/jobs ${role}${fld}`, regions: ["europe", "global"] },
{ key: "academicpositions", query: `site:academicpositions.com ${role}${fld}`, regions: ["global", "europe"] },
];
const targets = boards.filter((b) => b.regions.includes(academicRegion));
const results = {};
for (const t of targets) {
if (ctx.signal.aborted)
break;
ctx.status(`Searching ${t.key}β¦`);
try {
results[t.key] = await webSearch(t.query, maxResults(), searchWindow());
}
catch {
results[t.key] = [];
}
}
return json({ query: role, field: params.field || "any", region: academicRegion, results });
}
return json({ error: `Unknown channel: ${channel}` });
}),
}),
(0, sdk_1.tool)({
name: "fetch_job_page",
description: (0, sdk_1.text) `
Fetch and parse a job posting page from a URL.
Returns the cleaned text content of the page (HTML stripped).
Use this to get the full job description from a URL found via search_jobs.
`,
parameters: {
url: zod_1.z.string().url().describe("URL of the job posting page"),
},
implementation: safe_impl("fetch_job_page", async ({ url }, ctx) => {
ctx.status(`Fetching job page: ${url}`);
const res = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (compatible; job-search-plugin/1.0)" },
signal: AbortSignal.timeout(15000),
});
if (!res.ok)
throw new Error(`HTTP ${res.status} fetching ${url}`);
const html = await res.text();
const text = stripHtml(html).slice(0, 8000);
return json({ url, content: text });
}),
}),
// =========================================================================
// ATS PORTAL TOOLS
// =========================================================================
(0, sdk_1.tool)({
name: "scan_portals",
description: (0, sdk_1.text) `
Query company ATS portals directly (Greenhouse, Ashby, Lever, Workable) β no browser, pure API.
mode: "single" β fetch one company by slug and ATS type
mode: "all" β scan all 60+ AI/ML companies in the built-in database
Returns live job listings that often appear days before job boards index them.
`,
parameters: {
mode: zod_1.z.enum(["single", "all"]).default("all").describe("single = one company; all = scan built-in 60+ company database"),
company: zod_1.z.string().default("").describe("Company name (single mode)"),
slug: zod_1.z.string().default("").describe("ATS board slug e.g. 'stripe' (single mode)"),
ats: zod_1.z.enum(["greenhouse", "ashby", "lever", "workable"]).optional().describe("ATS platform (single mode)"),
roleFilter: zod_1.z.string().default("").describe("Keyword filter on job titles"),
region: zod_1.z.enum(["global", "us", "eu", "india", "remote"]).default("global").describe("Region filter (all mode)"),
limit: zod_1.z.coerce.number().int().min(1).max(200).default(50).describe("Max jobs (all mode)"),
},
implementation: safe_impl("scan_portals", async ({ mode, company, slug, ats, roleFilter, region, limit }, ctx) => {
if (mode === "single") {
if (!slug || !ats)
throw new Error("single mode requires both slug and ats parameters.");
ctx.status(`Fetching ${company || slug} jobs via ${ats}β¦`);
const portal = { name: company || slug, slug, ats, region: "global", category: "tech" };
const jobs = await (0, portals_1.fetchCompanyJobs)(portal, ctx.signal);
const filtered = (0, portals_1.filterByTitle)(jobs, roleFilter || undefined);
return json({ company: company || slug, ats, slug, total: jobs.length, filtered: filtered.length, jobs: filtered });
}
// mode === "all"
const portals = region === "global"
? portals_1.COMPANY_PORTALS
: portals_1.COMPANY_PORTALS.filter((p) => p.region === region || p.region === "global");
ctx.status(`Scanning ${portals.length} company portalsβ¦`);
const results = [];
let total = 0;
for (const portal of portals) {
if (total >= limit)
break;
if (ctx.signal.aborted)
break;
try {
ctx.status(`Fetching ${portal.name}β¦`);
const jobs = await (0, portals_1.fetchCompanyJobs)(portal, ctx.signal);
const filtered = (0, portals_1.filterByTitle)(jobs, roleFilter || undefined);
if (filtered.length > 0) {
results.push({ company: portal.name, jobs: filtered });
total += filtered.length;
}
}
catch { /* skip silently */ }
}
const allJobs = results.flatMap((r) => r.jobs).slice(0, limit);
return json({ roleFilter: roleFilter || "all", region, companiesScanned: portals.length, companiesWithMatches: results.length, totalJobs: allJobs.length, jobs: allJobs });
}),
}),
// =========================================================================
// APPLICATION TRACKER
// =========================================================================
(0, sdk_1.tool)({
name: "manage_application",
description: (0, sdk_1.text) `
Create, read, update, delete, or log interviews for a tracked job application.
action: "add" β create new application
action: "update" β update fields on existing application by ID
action: "get" β fetch full details of one application by ID
action: "delete" β permanently delete application by ID
action: "add_interview" β log an interview round to an existing application
`,
parameters: {
action: zod_1.z.enum(["add", "update", "get", "delete", "add_interview"]).describe("Operation to perform"),
id: zod_1.z.string().default("").describe("Application ID (required for update/get/delete/add_interview)"),
company: zod_1.z.string().default("").describe("Company name (add: required)"),
role: zod_1.z.string().default("").describe("Job title (add: required)"),
url: zod_1.z.string().default(""),
location: zod_1.z.string().default(""),
status: zod_1.z.enum(["saved", "applied", "interview", "offer", "rejected", "withdrawn"]).optional(),
jobDescription: zod_1.z.string().default(""),
notes: zod_1.z.string().default("").describe("For add: initial notes. For update: text to APPEND."),
salary: zod_1.z.string().default(""),
totalComp: zod_1.z.string().optional(),
nextStep: zod_1.z.string().default(""),
postedDate: zod_1.z.string().default(""),
followUpDate: zod_1.z.string().default(""),
workPermitStatus: zod_1.z.enum(["not_required", "eligible", "requires_sponsorship", "not_eligible", "unknown"]).optional(),
workPermitNotes: zod_1.z.string().optional(),
rejectionReason: zod_1.z.string().optional(),
round: zod_1.z.string().default("").describe("Interview round name (add_interview only)"),
date: zod_1.z.string().default("").describe("Interview date YYYY-MM-DD (add_interview only)"),
interviewerName: zod_1.z.string().default(""),
interviewerRole: zod_1.z.string().default(""),
questions: zod_1.z.array(zod_1.z.string()).default([]),
feedback: zod_1.z.string().default(""),
outcome: zod_1.z.enum(["passed", "failed", "pending", "unknown"]).default("pending"),
},
implementation: safe_impl("manage_application", async (params) => {
const db = await loadDB(dataDir());
// ββ add ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "add") {
if (!params.company.trim())
throw new Error("company is required for action=add.");
if (!params.role.trim())
throw new Error("role is required for action=add.");
const country = extractCountry(params.location);
const international = isInternationalJob(country) && country !== "Remote";
const companyLower = params.company.toLowerCase();
const roleNorm = params.role.toLowerCase().replace(/[^a-z0-9]/g, "");
const duplicate = db.applications.find((a) => a.company.toLowerCase() === companyLower && a.role.toLowerCase().replace(/[^a-z0-9]/g, "") === roleNorm);
const st = params.status ?? "saved";
const app = {
id: makeId(), company: params.company, role: params.role, url: params.url,
location: params.location, country, isInternational: international,
status: st, appliedDate: "", postedDate: params.postedDate, followUpDate: params.followUpDate,
jobDescription: params.jobDescription, notes: params.notes, salary: params.salary,
totalComp: "", nextStep: params.nextStep, contacts: [],
workPermitStatus: international ? "unknown" : "not_required",
workPermitNotes: international ? `International role in ${country}. Run check_work_permit for visa requirements.` : "",
legitimacyScore: -1, legitimacyFlags: [], interviewNotes: [], rejectionReason: "",
statusHistory: [{ status: st, date: todayStr() }],
updatedAt: new Date().toISOString(),
};
if (st === "applied")
stampAppliedDate(app);
const previousFlag = db.applications.find((a) => a.company.toLowerCase() === companyLower && a.legitimacyScore >= 0 && a.legitimacyScore < 50);
db.applications.push(app);
await saveDB(dataDir(), db);
return json({
success: true, application: app,
duplicateWarning: duplicate ? `β Possible duplicate: "${duplicate.company} β ${duplicate.role}" already tracked (ID: ${duplicate.id}).` : null,
internationalAlert: international ? `β International role (${country}). Use check_work_permit to verify visa requirements.` : null,
legitimacyWarning: previousFlag ? `β ${params.company} previously flagged suspicious (score ${previousFlag.legitimacyScore}/100).` : null,
});
}
// ββ update βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "update") {
if (!params.id.trim())
throw new Error("id is required for action=update.");
const idx = db.applications.findIndex((a) => a.id === params.id);
if (idx === -1)
throw new Error(`Application ID '${params.id}' not found.`);
const app = db.applications[idx];
const merged = {
...app,
...(params.company && { company: params.company }),
...(params.role && { role: params.role }),
...(params.url && { url: params.url }),
...(params.location && { location: params.location }),
...(params.status && { status: params.status }),
...(params.jobDescription && { jobDescription: params.jobDescription }),
...(params.notes && { notes: app.notes ? `${app.notes}\n${params.notes}` : params.notes }),
...(params.salary && { salary: params.salary }),
...(params.totalComp !== undefined && { totalComp: params.totalComp }),
...(params.nextStep && { nextStep: params.nextStep }),
...(params.postedDate && { postedDate: params.postedDate }),
...(params.followUpDate && { followUpDate: params.followUpDate }),
...(params.workPermitStatus !== undefined && { workPermitStatus: params.workPermitStatus }),
...(params.workPermitNotes !== undefined && { workPermitNotes: params.workPermitNotes }),
...(params.rejectionReason !== undefined && { rejectionReason: params.rejectionReason }),
updatedAt: new Date().toISOString(),
};
if (params.status === "applied")
stampAppliedDate(merged);
if (params.status && params.status !== app.status)
merged.statusHistory.push({ status: params.status, date: todayStr() });
db.applications[idx] = merged;
await saveDB(dataDir(), db);
return json({ success: true, application: merged });
}
// ββ get ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "get") {
if (!params.id.trim())
throw new Error("id is required for action=get.");
const app = db.applications.find((a) => a.id === params.id);
if (!app)
throw new Error(`Application ID '${params.id}' not found.`);
return json(app);
}
// ββ delete βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "delete") {
if (!params.id.trim())
throw new Error("id is required for action=delete.");
const before = db.applications.length;
db.applications = db.applications.filter((a) => a.id !== params.id);
if (db.applications.length === before)
throw new Error(`Application ID '${params.id}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: params.id });
}
// ββ add_interview βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.action === "add_interview") {
if (!params.id.trim())
throw new Error("id is required for action=add_interview.");
const idx = db.applications.findIndex((a) => a.id === params.id);
if (idx === -1)
throw new Error(`Application ID '${params.id}' not found.`);
const note = {
round: params.round || "Unknown Round",
date: params.date || new Date().toISOString().slice(0, 10),
interviewerName: params.interviewerName,
interviewerRole: params.interviewerRole,
questions: params.questions,
feedback: params.feedback,
outcome: params.outcome,
};
db.applications[idx].interviewNotes.push(note);
if (db.applications[idx].status === "applied") {
db.applications[idx].statusHistory.push({ status: "interview", date: todayStr() });
db.applications[idx].status = "interview";
}
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, application: db.applications[idx].company + " β " + db.applications[idx].role, totalRounds: db.applications[idx].interviewNotes.length, latestNote: note });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
(0, sdk_1.tool)({
name: "list_applications",
description: (0, sdk_1.text) `
List all tracked job applications, optionally filtered by status.
Returns a summary table: ID, company, role, status, next step, updated date.
`,
parameters: {
status: zod_1.z.enum(["saved", "applied", "interview", "offer", "rejected", "withdrawn", "all"])
.default("all").describe("Filter by status, or 'all' for everything"),
search: zod_1.z.string().default("").describe("Search keyword in company or role name"),
internationalOnly: zod_1.z.coerce.boolean().default(false)
.describe("Show only international / cross-border applications"),
workPermitStatus: zod_1.z.enum(["not_required", "eligible", "requires_sponsorship", "not_eligible", "unknown", "all"])
.default("all").describe("Filter by work permit status"),
},
implementation: safe_impl("list_applications", async ({ status, search, internationalOnly, workPermitStatus }) => {
const db = await loadDB(dataDir());
let apps = db.applications;
if (status !== "all")
apps = apps.filter((a) => a.status === status);
if (internationalOnly)
apps = apps.filter((a) => a.isInternational);
if (workPermitStatus !== "all")
apps = apps.filter((a) => a.workPermitStatus === workPermitStatus);
if (search) {
const kw = search.toLowerCase();
apps = apps.filter((a) => a.company.toLowerCase().includes(kw) || a.role.toLowerCase().includes(kw));
}
// Sort: most recently updated first
apps = apps.slice().sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
const summary = apps.map((a) => ({
id: a.id,
company: a.company,
role: a.role,
location: a.location,
country: a.country,
isInternational: a.isInternational,
status: a.status,
appliedDate: a.appliedDate,
salary: a.salary,
workPermitStatus: a.workPermitStatus,
nextStep: a.nextStep,
updatedAt: a.updatedAt.slice(0, 10),
}));
const stats = {
total: db.applications.length,
saved: db.applications.filter((a) => a.status === "saved").length,
applied: db.applications.filter((a) => a.status === "applied").length,
interview: db.applications.filter((a) => a.status === "interview").length,
offer: db.applications.filter((a) => a.status === "offer").length,
rejected: db.applications.filter((a) => a.status === "rejected").length,
international: db.applications.filter((a) => a.isInternational).length,
pendingWorkPermitCheck: db.applications.filter((a) => a.isInternational && a.workPermitStatus === "unknown").length,
};
return json({ stats, filter: { status, search, internationalOnly, workPermitStatus }, applications: summary });
}),
}),
// =========================================================================
// ANALYSIS
// =========================================================================
(0, sdk_1.tool)({
name: "analyze_job_description",
description: (0, sdk_1.text) `
Analyze a job description and extract structured information:
required skills, nice-to-have skills, responsibilities, seniority level,
red flags, and key questions to ask the interviewer.
Paste the raw job description text as input.
`,
parameters: {
jobDescription: zod_1.z.string().min(50)
.describe("Full job description text (paste it in directly)"),
role: zod_1.z.string().default("").describe("Job title (optional, improves analysis)"),
},
implementation: safe_impl("analyze_job_description", async ({ jobDescription, role }, ctx) => {
ctx.status("Analyzing job descriptionβ¦");
const wordCount = jobDescription.split(/\s+/).length;
const hasRemote = /remote|hybrid/i.test(jobDescription);
const hasSalary = /\$[\d,]+|\d+k\b|salary|compensation/i.test(jobDescription);
const techMatches = jobDescription.match(/\b(TypeScript|JavaScript|Python|Go|Rust|Java|C\+\+|React|Vue|Angular|Node\.js|Next\.js|AWS|GCP|Azure|Docker|Kubernetes|PostgreSQL|MySQL|MongoDB|Redis|GraphQL|REST|gRPC|Terraform|Kafka|Spark|Ray|Airflow|MLflow|LangChain|LlamaIndex|Pinecone|Weaviate|Qdrant|vLLM|TGI|LoRA|PEFT|HuggingFace|PyTorch|TensorFlow|JAX|ONNX|ML|LLM|AI|RAG|RLHF|SFT|FineTuning|MCP|OpenAI|Anthropic|Gemini)\b/gi);
const techs = [...new Set((techMatches ?? []).map((t) => t.toLowerCase()))];
const archetype = (0, portals_1.detectArchetype)(role || "", jobDescription);
const legitimacy = (0, portals_1.assessJobLegitimacy)(jobDescription);
const atsKeywords = (0, portals_1.extractAtsKeywords)(jobDescription, 15);
return json({
role: role || "Unknown",
wordCount,
mentionsRemote: hasRemote,
mentionsSalary: hasSalary,
detectedTechnologies: techs,
archetype: archetype.archetype,
archetypeLabel: archetype.label,
legitimacy: {
tier: legitimacy.tier,
signals: legitimacy.signals,
},
atsKeywords,
jobDescription,
instructions: "Using the jobDescription above, extract and present:\n" +
"(A) Required skills β hard requirements only\n" +
"(B) Nice-to-have skills β explicit 'preferred' / 'plus' items\n" +
"(C) Key responsibilities β top 5 bullet points\n" +
"(D) Seniority level (IC1βIC6) with rationale\n" +
"(E) Red flags β unrealistic requirements, vague scope, high churn signals\n" +
"(F) Top 5 questions to ask the interviewer specific to this role\n" +
`(G) Archetype framing: this is a '${archetype.label}' role β highlight what matters most for this archetype`,
});
}),
}),
(0, sdk_1.tool)({
name: "match_resume_to_job",
description: (0, sdk_1.text) `
Compare the user's resume against a job description and produce a fit score (0β100),
list of matched skills, gaps, and prioritized suggestions to improve fit.
Reads the resume from the path configured in plugin settings, or accepts inline text.
`,
parameters: {
jobDescription: zod_1.z.string().min(20).describe("Full job description text"),
resumeText: zod_1.z.string().default("")
.describe("Inline resume text. If blank, reads from the configured Resume File Path."),
},
implementation: safe_impl("match_resume_to_job", async ({ jobDescription, resumeText }, ctx) => {
ctx.status("Loading resumeβ¦");
let resume = resumeText.trim();
if (!resume) {
const rp = resumePath();
if (!rp)
throw new Error("No resume text provided and Resume File Path is not configured in plugin settings.");
resume = await readResumeFile(rp);
}
ctx.status("Extracting skills and ATS keywordsβ¦");
const extractSkills = (t) => {
const m = t.match(/\b(TypeScript|JavaScript|Python|Go|Rust|Java|C\+\+|React|Vue|Angular|Node\.js|Next\.js|AWS|GCP|Azure|Docker|Kubernetes|PostgreSQL|MySQL|MongoDB|Redis|GraphQL|REST|gRPC|Terraform|Kafka|Spark|Ray|MLflow|LangChain|LlamaIndex|Pinecone|Weaviate|vLLM|PyTorch|TensorFlow|JAX|ONNX|ML|LLM|AI|RAG|RLHF|LoRA|HuggingFace|Machine Learning|Data Science|Product Management|Agile|Scrum|CI\/CD|DevOps|MCP|OpenAI|Anthropic)\b/gi);
return [...new Set((m ?? []).map((s) => s.toLowerCase()))];
};
const jobSkills = extractSkills(jobDescription);
const resumeSkills = extractSkills(resume);
const matched = jobSkills.filter((s) => resumeSkills.includes(s));
const gaps = jobSkills.filter((s) => !resumeSkills.includes(s));
const roughScore = jobSkills.length > 0
? Math.round((matched.length / jobSkills.length) * 100)
: 50;
const archetype = (0, portals_1.detectArchetype)("", jobDescription);
const atsKeywords = (0, portals_1.extractAtsKeywords)(jobDescription, 20);
return json({
roughFitScore: roughScore,
matchedSkills: matched,
skillGaps: gaps,
resumeSkillsDetected: resumeSkills,
jobSkillsDetected: jobSkills,
archetype: archetype.archetype,
archetypeLabel: archetype.label,
atsKeywords,
resume: resume.slice(0, 8000),
jobDescription: jobDescription.slice(0, 4000),
instructions: "Using the resume and job description above, produce a structured analysis:\n" +
"(1) Overall fit score 0β100 with rationale.\n" +
"(2) Strengths β what already matches well (quote specific resume lines).\n" +
"(3) Skill gaps β list each missing keyword/skill from the JD absent in the resume.\n" +
"(4) Editing guide β for EACH gap:\n" +
" β’ If experience likely exists but is unmentioned: name WHERE to add it and provide a model bullet rewritten using JD vocabulary.\n" +
" β’ If skill is genuinely missing: say so and suggest the fastest path (side project, cert, OSS).\n" +
"(5) ATS keyword injection β embed all atsKeywords above verbatim into the resume; list which bullets to update.\n" +
`(6) Archetype fit: this is a '${archetype.label}' role β call out the 2β3 signals the resume must emphasise for this archetype.\n` +
"(7) Recommendation: Apply now / Apply after minor edits / Significant rework needed β with a one-line reason.",
});
}),
}),
// =========================================================================
// GENERATION
// =========================================================================
(0, sdk_1.tool)({
name: "generate_cover_letter",
description: (0, sdk_1.text) `
Generate a tailored cover letter draft for a specific job.
Provide the job description and basic info about the applicant.
The model will write a compelling, concise cover letter (3β4 paragraphs).
`,
parameters: {
company: zod_1.z.string().describe("Company name"),
role: zod_1.z.string().describe("Job title"),
jobDescription: zod_1.z.string().min(20).describe("Job description text"),
applicantName: zod_1.z.string().default("").describe("Your full name"),
applicantBackground: zod_1.z.string().default("")
.describe("Brief background: years of experience, key skills, notable achievements"),
tone: zod_1.z.enum(["professional", "conversational", "enthusiastic"]).default("professional")
.describe("Tone of the letter"),
resumeText: zod_1.z.string().default("")
.describe("Optional resume text; if blank, reads from configured Resume File Path"),
},
implementation: safe_impl("generate_cover_letter", async (params, ctx) => {
ctx.status("Loading resume for cover letterβ¦");
let resume = params.resumeText.trim();
if (!resume) {
const rp = resumePath();
if (rp) {
resume = await readResumeFile(rp);
}
}
const archetype = (0, portals_1.detectArchetype)(params.role, params.jobDescription);
const atsKeywords = (0, portals_1.extractAtsKeywords)(params.jobDescription, 10);
const clicheList = portals_1.CLICHE_WORDS.slice(0, 20).join(", ");
return json({
company: params.company,
role: params.role,
applicantName: params.applicantName,
applicantBackground: params.applicantBackground,
tone: params.tone,
archetype: archetype.archetype,
archetypeLabel: archetype.label,
atsKeywords,
jobDescription: params.jobDescription.slice(0, 4000),
resume: resume.slice(0, 6000),
instructions: `Write a ${params.tone} cover letter for ${params.applicantName || "the applicant"} ` +
`applying to ${params.role} at ${params.company}.\n` +
"Structure:\n" +
"(1) Opening hook β a specific observation about the company's product/mission, not a generic intro.\n" +
"(2) Exit narrative bridge β explain briefly what you're transitioning FROM and why this role is the logical NEXT step.\n" +
"(3) Top 2β3 achievements with metrics that map directly to the JD's stated needs.\n" +
"(4) Archetype fit signal β for this '" + archetype.label + "' role, include one sentence that speaks directly to what this archetype values.\n" +
"(5) Confident close β no 'I hope', no 'looking forward to hearing from you'.\n" +
`Weave in these ATS keywords naturally: ${atsKeywords.join(", ")}.\n` +
`STRICTLY AVOID these clichΓ©s: ${clicheList}.\n` +
"Keep it under 320 words. No fluff. No hollow superlatives.",
});
}),
}),
(0, sdk_1.tool)({
name: "generate_resume_bullets",
description: (0, sdk_1.text) `
Generate strong, achievement-oriented resume bullet points for a role or experience.
Follows the "Accomplished X by doing Y, resulting in Z" pattern with metrics.
`,
parameters: {
role: zod_1.z.string().describe("Job title / role (e.g. 'Senior Backend Engineer')"),
company: zod_1.z.string().default("").describe("Company or project name"),
responsibilities: zod_1.z.string()
.describe("Describe what you did in this role (rough notes are fine)"),
targetRole: zod_1.z.string().default("")
.describe("Target job title you are applying to (helps tailor the bullets)"),
count: zod_1.z.coerce.number().int().min(2).max(8).default(4)
.describe("Number of bullet points to generate"),
},
implementation: safe_impl("generate_resume_bullets", async (params) => {
const archetype = params.targetRole ? (0, portals_1.detectArchetype)(params.targetRole, params.responsibilities) : null;
const clicheList = portals_1.CLICHE_WORDS.slice(0, 15).join(", ");
return json({
sourceRole: params.role,
company: params.company,
targetRole: params.targetRole,
responsibilities: params.responsibilities,
count: params.count,
archetypeLabel: archetype?.label ?? null,
instructions: `Generate ${params.count} strong resume bullet points for ${params.role}` +
(params.company ? ` at ${params.company}` : "") + ".\n" +
"Rules:\n" +
"β’ Start with a strong, specific action verb (not 'Assisted', 'Helped', 'Worked on').\n" +
"β’ Include a concrete metric or outcome for every bullet (%, $, latency, scale, time saved).\n" +
"β’ Keep each bullet under 20 words.\n" +
"β’ Replace vague claims with specific tech, system names, or team sizes.\n" +
`β’ NEVER use these clichΓ©s: ${clicheList}.\n` +
(params.targetRole
? `β’ Tailor vocabulary to a ${params.targetRole} role` +
(archetype ? ` (archetype: ${archetype.label})` : "") + ".\n"
: "") +
"Format: 'β’ <bullet>' per line. Output bullets only, no commentary.",
});
}),
}),
(0, sdk_1.tool)({
name: "prepare_interview_questions",
description: (0, sdk_1.text) `
Generate a tailored set of interview questions and suggested answers
based on the job description and the applicant's background.
Covers behavioral, technical, and situational questions.
`,
parameters: {
jobDescription: zod_1.z.string().min(20).describe("Job description text"),
role: zod_1.z.string().describe("Job title"),
interviewStage: zod_1.z.enum(["phone_screen", "technical", "behavioral", "system_design", "final"])
.default("behavioral").describe("Stage of interview to prepare for"),
applicantBackground: zod_1.z.string().default("")
.describe("Brief background to tailor questions"),
questionCount: zod_1.z.coerce.number().int().min(3).max(20).default(8)
.describe("Number of questions to generate"),
},
implementation: safe_impl("prepare_interview_questions", async (params) => {
return json({
role: params.role,
interviewStage: params.interviewStage,
questionCount: params.questionCount,
applicantBackground: params.applicantBackground,
jobDescription: params.jobDescription.slice(0, 2000),
instructions: `Generate ${params.questionCount} ${params.interviewStage.replace("_", " ")} ` +
`interview questions for a ${params.role} role, based on the job description above. ` +
"For each question, provide: " +
"(Q) The question, (Why) Why interviewers ask it, " +
"(A) A strong answer framework / sample answer outline. " +
"Focus on what actually matters for this specific role.",
});
}),
}),
(0, sdk_1.tool)({
name: "export_applications",
description: (0, sdk_1.text) `
Export tracked job applications to a file.
format: "report" β formatted Markdown report with pipeline summary and per-application details
format: "csv" β CSV spreadsheet for use in Excel / Google Sheets
`,
parameters: {
format: zod_1.z.enum(["report", "csv"]).default("report"),
outputPath: zod_1.z.string().default("").describe("Output file path. Defaults to <dataPath>/applications-<date>.<ext>"),
},
implementation: safe_impl("export_applications", async ({ format, outputPath }) => {
const db = await loadDB(dataDir());
const date = new Date().toISOString().slice(0, 10);
if (format === "csv") {
const outPath = outputPath.trim() || (0, path_1.join)(dataDir(), `applications-${date}.csv`);
const headers = ["ID", "Company", "Role", "Location", "Country", "Status", "Applied Date", "Posted Date", "Follow-Up Date", "Salary", "Total Comp", "URL", "Next Step", "Contacts", "Interview Rounds", "Work Permit Status", "Legitimacy Score", "Rejection Reason", "Notes", "Updated At"];
function csvEscape(s) { return (s.includes(",") || s.includes('"') || s.includes("\n")) ? `"${s.replace(/"/g, '""')}"` : s; }
const rows = db.applications.map((a) => [a.id, a.company, a.role, a.location, a.country, a.status, a.appliedDate, a.postedDate, a.followUpDate, a.salary, a.totalComp, a.url, a.nextStep, a.contacts.map((c) => c.name).join("; "), String(a.interviewNotes.length), a.workPermitStatus, a.legitimacyScore >= 0 ? String(a.legitimacyScore) : "", a.rejectionReason, a.notes, a.updatedAt.slice(0, 10)].map(csvEscape).join(","));
const csv = [headers.join(","), ...rows].join("\n");
await (0, promises_1.mkdir)(dataDir(), { recursive: true });
await (0, promises_1.writeFile)(outPath, csv, "utf8");
return json({ success: true, path: outPath, applicationCount: db.applications.length });
}
// format === "report"
const outPath = outputPath.trim() || (0, path_1.join)(dataDir(), `report-${date}.md`);
const apps = db.applications;
const byStatus = {};
for (const a of apps)
byStatus[a.status] = (byStatus[a.status] ?? 0) + 1;
const lines = [
`# Job Application Report β ${date}`, "",
"## Pipeline Summary", "",
`| Status | Count |`, `|--------|-------|`,
...Object.entries(byStatus).map(([k, v]) => `| ${k} | ${v} |`),
"", `**Total:** ${apps.length}`, "",
"## Applications", "",
];
for (const a of apps.slice().sort((x, y) => y.updatedAt.localeCompare(x.updatedAt))) {
lines.push(`### ${a.company} β ${a.role}`, `- **Status:** ${a.status}`, `- **Location:** ${a.location}`, `- **Applied:** ${a.appliedDate || "not yet"}`, `- **Salary:** ${a.salary || "unknown"}`, `- **URL:** ${a.url || "none"}`, `- **Notes:** ${a.notes || "none"}`, "");
}
await (0, promises_1.mkdir)(dataDir(), { recursive: true });
await (0, promises_1.writeFile)(outPath, lines.join("\n"), "utf8");
return json({ success: true, path: outPath, applicationCount: apps.length });
}),
}),
// =========================================================================
// TIMING & URGENCY
// =========================================================================
(0, sdk_1.tool)({
name: "application_insights",
description: (0, sdk_1.text) `
Analytics and status intelligence for your job application pipeline.
mode: "urgency" β score how urgent it is to apply for a job by posted date
mode: "followups" β list applications with upcoming or overdue follow-up dates
mode: "stale" β detect applications with no activity in N days
mode: "stats" β pipeline summary: counts by status, conversion rate, international split
mode: "rejections" β analyze patterns in rejected applications to find improvement areas
`,
parameters: {
mode: zod_1.z.enum(["urgency", "followups", "stale", "stats", "rejections"]).describe("Which insight to compute"),
postedDate: zod_1.z.string().default("").describe("Posted date string: YYYY-MM-DD, '3 days ago', 'today' (urgency mode)"),
applicationId: zod_1.z.string().default("").describe("Application ID to update with computed dates (urgency mode)"),
role: zod_1.z.string().default(""),
company: zod_1.z.string().default(""),
daysAhead: zod_1.z.coerce.number().int().min(0).max(30).default(7).describe("Show follow-ups due within N days (followups mode)"),
staleDays: zod_1.z.coerce.number().int().min(1).max(90).default(14).describe("Consider stale after N days with no update (stale mode)"),
includeStatuses: zod_1.z.array(zod_1.z.enum(["saved", "applied", "interview"])).default(["applied", "interview"]).describe("Statuses to check (stale mode)"),
},
implementation: safe_impl("application_insights", async (params) => {
const db = await loadDB(dataDir());
// ββ urgency βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "urgency") {
const today = new Date();
let parsedDate = null;
const pd = params.postedDate;
const daysAgoMatch = pd.match(/(\d+)\s*day/i);
const weeksAgoMatch = pd.match(/(\d+)\s*week/i);
const monthsAgoMatch = pd.match(/(\d+)\s*month/i);
if (daysAgoMatch) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - parseInt(daysAgoMatch[1]));
}
else if (weeksAgoMatch) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - parseInt(weeksAgoMatch[1]) * 7);
}
else if (monthsAgoMatch) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - parseInt(monthsAgoMatch[1]) * 30);
}
else if (/^\d{4}-\d{2}-\d{2}$/.test(pd)) {
const [y, m, d] = pd.split("-").map(Number);
parsedDate = new Date(y, m - 1, d);
}
else if (/today|just posted/i.test(pd)) {
parsedDate = new Date(today);
}
else if (/yesterday/i.test(pd)) {
parsedDate = new Date(today);
parsedDate.setDate(parsedDate.getDate() - 1);
}
const daysOld = parsedDate ? Math.floor((today.getTime() - parsedDate.getTime()) / (1000 * 60 * 60 * 24)) : null;
let urgency;
let score;
let recommendation;
let callbackMultiplier;
if (daysOld === null) {
urgency = "unknown";
score = 5;
recommendation = "Could not parse date. Apply ASAP.";
callbackMultiplier = "unknown";
}
else if (daysOld <= 2) {
urgency = "CRITICAL";
score = 10;
recommendation = "Apply within hours. Early applicants get 2β3Γ more callbacks.";
callbackMultiplier = "2β3Γ";
}
else if (daysOld <= 7) {
urgency = "HIGH";
score = 8;
recommendation = "Apply today. First week applicants have strong odds.";
callbackMultiplier = "1.5β2Γ";
}
else if (daysOld <= 14) {
urgency = "MEDIUM";
score = 5;
recommendation = "Apply soon. Odds declining but still worth it for a strong match.";
callbackMultiplier = "0.8β1Γ";
}
else if (daysOld <= 30) {
urgency = "LOW";
score = 3;
recommendation = "Late application. Referrals help.";
callbackMultiplier = "0.3β0.6Γ";
}
else {
urgency = "STALE";
score = 1;
recommendation = "Likely filled. Verify it's still open.";
callbackMultiplier = "~0Γ";
}
const followUpDays = (daysOld ?? 0) <= 7 ? 7 : 5;
const followUpDate = new Date(today);
followUpDate.setDate(followUpDate.getDate() + followUpDays);
const followUpDateStr = followUpDate.toISOString().slice(0, 10);
const postedDateStr = parsedDate ? parsedDate.toISOString().slice(0, 10) : "";
if (params.applicationId) {
const idx = db.applications.findIndex((a) => a.id === params.applicationId);
if (idx !== -1) {
if (postedDateStr)
db.applications[idx].postedDate = postedDateStr;
db.applications[idx].followUpDate = followUpDateStr;
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
}
}
return json({ role: params.role || "Unknown", company: params.company || "Unknown", postedDate: postedDateStr || pd, daysOld, urgency, urgencyScore: score, callbackMultiplier, recommendation, followUpDate: followUpDateStr });
}
// ββ followups βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "followups") {
const now = new Date();
const cutoff = new Date(now);
cutoff.setDate(cutoff.getDate() + params.daysAhead);
const todStr = now.toISOString().slice(0, 10);
const cutoffStr = cutoff.toISOString().slice(0, 10);
const dateRe = /^\d{4}-\d{2}-\d{2}$/;
const active = db.applications
.filter((a) => a.status !== "rejected" && a.status !== "withdrawn" && dateRe.test(a.followUpDate ?? "") && a.followUpDate <= cutoffStr)
.sort((a, b) => a.followUpDate.localeCompare(b.followUpDate))
.map((a) => {
const overdue = a.followUpDate < todStr;
const daysUntil = Math.floor((new Date(a.followUpDate).getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
return { id: a.id, company: a.company, role: a.role, status: a.status, followUpDate: a.followUpDate, daysUntil, overdue, urgency: overdue ? "OVERDUE" : daysUntil === 0 ? "TODAY" : `in ${daysUntil}d`, nextStep: a.nextStep };
});
return json({ summary: { overdue: active.filter((r) => r.overdue).length, dueToday: active.filter((r) => !r.overdue && r.daysUntil === 0).length, upcoming: active.filter((r) => !r.overdue && r.daysUntil > 0).length }, followUps: active });
}
// ββ stale βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "stale") {
const now = new Date();
const cutoff = new Date(now.getTime() - params.staleDays * 24 * 60 * 60 * 1000);
const stale = db.applications
.filter((a) => params.includeStatuses.includes(a.status) && new Date(a.updatedAt) < cutoff)
.map((a) => {
const daysSinceUpdate = Math.floor((now.getTime() - new Date(a.updatedAt).getTime()) / (1000 * 60 * 60 * 24));
const appliedDaysAgo = a.appliedDate ? Math.floor((now.getTime() - new Date(a.appliedDate).getTime()) / (1000 * 60 * 60 * 24)) : null;
let recommendedAction;
if (a.status === "interview")
recommendedAction = "Send a polite follow-up email";
else if (a.status === "applied" && appliedDaysAgo !== null && appliedDaysAgo > 30)
recommendedAction = daysSinceUpdate > 45 ? "Likely ghosted β consider withdrawing" : "Send one follow-up; if no response in 7 days, close it";
else if (a.status === "applied")
recommendedAction = "Send a polite follow-up if not already sent";
else
recommendedAction = "Reassess whether to apply or move on";
return { id: a.id, company: a.company, role: a.role, status: a.status, daysSinceUpdate, appliedDaysAgo, recommendedAction };
})
.sort((a, b) => b.daysSinceUpdate - a.daysSinceUpdate);
return json({ summary: { staleCount: stale.length, threshold: params.staleDays, likelyghosted: stale.filter((a) => a.daysSinceUpdate > 45).length }, staleApplications: stale });
}
// ββ stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "stats") {
const apps = db.applications;
const byStatus = {};
for (const a of apps)
byStatus[a.status] = (byStatus[a.status] ?? 0) + 1;
const applied = apps.filter((a) => ["applied", "interview", "offer", "rejected", "withdrawn"].includes(a.status)).length;
const offers = apps.filter((a) => a.status === "offer").length;
const interviewed = apps.filter((a) => ["interview", "offer"].includes(a.status) || (a.status === "rejected" && a.interviewNotes.length > 0)).length;
return json({ total: apps.length, byStatus, conversionRates: { applied: `${applied}/${apps.length}`, interviewed: `${interviewed}/${applied || 1}`, offered: `${offers}/${applied || 1}` }, international: apps.filter((a) => a.isInternational).length, pendingWorkPermit: apps.filter((a) => a.isInternational && a.workPermitStatus === "unknown").length, legitimacyWarnings: apps.filter((a) => a.legitimacyScore >= 0 && a.legitimacyScore < 50).length });
}
// ββ rejections ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (params.mode === "rejections") {
const rejected = db.applications.filter((a) => a.status === "rejected");
if (rejected.length < 3)
return json({ message: `Only ${rejected.length} rejected applications. Need β₯3 for pattern analysis.` });
const withInterviews = rejected.filter((a) => a.interviewNotes.length > 0);
const preInterview = rejected.filter((a) => a.interviewNotes.length === 0);
const byLastRound = {};
for (const a of withInterviews) {
const r = a.interviewNotes[a.interviewNotes.length - 1]?.round || "unknown";
byLastRound[r] = (byLastRound[r] ?? 0) + 1;
}
const byRole = {};
for (const a of rejected) {
const r = a.role.toLowerCase().replace(/senior|junior|lead|staff|principal|sr\.?|jr\.?/gi, "").trim();
byRole[r] = (byRole[r] ?? 0) + 1;
}
const times = rejected.filter((a) => a.appliedDate).map((a) => { const rEntry = a.statusHistory?.find((h) => h.status === "rejected"); return Math.floor((new Date(rEntry?.date ?? a.updatedAt).getTime() - new Date(a.appliedDate).getTime()) / (1000 * 60 * 60 * 24)); });
const avgDays = times.length > 0 ? Math.round(times.reduce((a, b) => a + b, 0) / times.length) : null;
return json({ totalRejected: rejected.length, stageBreakdown: { preInterview: preInterview.length, postInterview: withInterviews.length, byLastInterviewRound: byLastRound }, byRoleType: byRole, timing: { avgDaysToRejection: avgDays, fastest: times.length > 0 ? Math.min(...times) : null, slowest: times.length > 0 ? Math.max(...times) : null }, instructions: "Analyze these rejection patterns. (1) Where in pipeline do rejections happen β resume/targeting vs interview prep issue. (2) Are certain role types rejected more? (3) Is timing a factor β quick rejections suggest auto-filtering. (4) Give 3 specific actionable recommendations." });
}
throw new Error(`Unknown mode: ${params.mode}`);
}),
}),
(0, sdk_1.tool)({
name: "salary",
description: (0, sdk_1.text) `
Salary tools for evaluating and negotiating compensation.
mode: "calc" β calculate total compensation (base + bonus + equity + benefits + stipend)
mode: "market" β look up current market salary data (Glassdoor, LinkedIn, levels.fyi, Reddit)
`,
parameters: {
mode: zod_1.z.enum(["calc", "market"]).describe("calc = total comp calculator; market = salary market data lookup"),
role: zod_1.z.string().default("").describe("Job title (market mode: required)"),
location: zod_1.z.string().default("").describe("Job location"),
currency: zod_1.z.string().default("USD"),
applicationId: zod_1.z.string().default("").describe("Optional application ID to save result"),
baseSalary: zod_1.z.coerce.number().default(0).describe("Annual base salary (calc mode)"),
bonusPercent: zod_1.z.coerce.number().min(0).max(100).default(0),
equityValue: zod_1.z.coerce.number().min(0).default(0),
equityVestYears: zod_1.z.coerce.number().int().min(1).max(10).default(4),
signOnBonus: zod_1.z.coerce.number().min(0).default(0),
annualBenefitsValue: zod_1.z.coerce.number().min(0).default(0),
remoteStipend: zod_1.z.coerce.number().min(0).default(0),
currentTotalComp: zod_1.z.coerce.number().min(0).default(0),
yearsOfExperience: zod_1.z.coerce.number().int().min(0).max(40).default(0),
},
implementation: safe_impl("salary", async (params, ctx) => {
if (params.mode === "calc") {
const annualBonus = params.baseSalary * (params.bonusPercent / 100);
const annualEquity = params.equityVestYears > 0 ? params.equityValue / params.equityVestYears : 0;
const firstYearComp = params.baseSalary + annualBonus + annualEquity + params.signOnBonus + params.annualBenefitsValue + params.remoteStipend;
const steadyStateComp = params.baseSalary + annualBonus + annualEquity + params.annualBenefitsValue + params.remoteStipend;
const changeVsCurrent = params.currentTotalComp > 0 ? { absoluteDiff: steadyStateComp - params.currentTotalComp, percentDiff: Math.round(((steadyStateComp - params.currentTotalComp) / params.currentTotalComp) * 100) } : null;
if (params.applicationId) {
const db = await loadDB(dataDir());
const idx = db.applications.findIndex((a) => a.id === params.applicationId);
if (idx !== -1) {
db.applications[idx].salary = `${params.currency} ${params.baseSalary.toLocaleString()} base`;
db.applications[idx].totalComp = `${params.currency} ${Math.round(steadyStateComp).toLocaleString()} TC`;
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
}
}
return json({ location: params.location || "Not specified", breakdown: { baseSalary: params.baseSalary, annualBonus: Math.round(annualBonus), annualEquity: Math.round(annualEquity), signOnBonus: params.signOnBonus, annualBenefitsValue: params.annualBenefitsValue, remoteStipend: params.remoteStipend }, firstYearTotalComp: Math.round(firstYearComp), steadyStateTotalComp: Math.round(steadyStateComp), currency: params.currency, changeVsCurrent });
}
if (params.mode === "market") {
ctx.status(`Checking salary data for: ${params.role}`);
const yr = new Date().getFullYear();
const loc = params.location.trim() || preferredLocation();
const expTag = params.yearsOfExperience > 0 ? ` ${params.yearsOfExperience} years experience` : "";
const searches = [
{ angle: "glassdoor", query: `${params.role} salary ${loc}${expTag} ${yr} site:glassdoor.com` },
{ angle: "linkedin", query: `${params.role} salary ${loc} ${yr} site:linkedin.com/salary` },
{ angle: "levels_fyi", query: `${params.role} compensation ${loc} ${yr} site:levels.fyi` },
{ angle: "survey", query: `${params.role} salary range ${loc}${expTag} ${params.currency} median ${yr} survey` },
{ angle: "reddit", query: `${params.role} salary ${loc}${expTag} ${yr} site:reddit.com` },
];
const results = [];
for (const s of searches) {
try {
results.push({ angle: s.angle, hits: await webSearch(s.query, 5, searchWindow()) });
}
catch {
results.push({ angle: s.angle, hits: [] });
}
}
return json({ role: params.role, location: loc, yearsOfExperience: params.yearsOfExperience || "not specified", currency: params.currency || "inferred from location", year: yr, market_data: results, instructions: `From the market_data above, provide a salary benchmark for ${params.role} in ${loc}. Present low/median/high range citing each source. Include total comp range if present. Flag data gaps or source conflicts.` });
}
throw new Error(`Unknown mode: ${params.mode}`);
}),
}),
// =========================================================================
// COMPANY LEGITIMACY VERIFICATION
// =========================================================================
(0, sdk_1.tool)({
name: "verify_company_legitimacy",
description: (0, sdk_1.text) `
Verify whether a company and job posting are legitimate by checking multiple signals.
Detects common job scam patterns, ghost jobs, and red flags.
Returns a legitimacy score (0β100), red flags found, and green flags.
Common scams: fake companies, too-good-to-pay roles, asking for money/equipment upfront,
vague job descriptions, no verifiable web presence, mismatched domains.
`,
parameters: {
company: zod_1.z.string().describe("Company name to verify"),
jobTitle: zod_1.z.string().default("").describe("Job title from the posting"),
jobDescription: zod_1.z.string().default("").describe("Paste job description for red flag analysis"),
recruitingEmail: zod_1.z.string().default("").describe("Email or domain of the recruiter contact"),
jobUrl: zod_1.z.string().default("").describe("URL of the job posting"),
salary: zod_1.z.string().default("").describe("Stated salary (for outlier detection)"),
},
implementation: safe_impl("verify_company_legitimacy", async (params) => {
const redFlags = [];
const greenFlags = [];
let score = 50; // Neutral baseline β evidence moves the score in either direction
// --- JD analysis ---
if (params.jobDescription) {
const jd = params.jobDescription.toLowerCase();
// Scam red flags in JD
if (/work from home|be your own boss|unlimited earning/i.test(jd)) {
redFlags.push("JD uses MLM/pyramid-scheme language ('be your own boss', 'unlimited earning')");
score -= 20;
}
if (/send.*equipment|purchase.*equipment|reimburse.*gift card|wire transfer/i.test(jd)) {
redFlags.push("JD mentions buying equipment or gift cards β classic advance-fee scam");
score -= 30;
}
if (/no experience (required|needed)|anyone can do/i.test(jd)) {
redFlags.push("JD claims no experience required for what sounds like a skilled role");
score -= 10;
}
if (params.jobDescription.length < 150) {
redFlags.push("Job description is suspiciously short (< 150 chars) β ghost job or scam");
score -= 15;
}
if (/immediately|urgent|asap|start today/i.test(jd)) {
redFlags.push("Unusual urgency in the posting β pressure tactic common in scams");
score -= 5;
}
if (/gmail\.com|yahoo\.com|hotmail\.com/.test(jd)) {
redFlags.push("JD contains a free email domain β legitimate companies use corporate email");
score -= 20;
}
// Green flags in JD
if (/interview process|interview stages|hiring manager/i.test(jd)) {
greenFlags.push("JD mentions a structured interview process");
score += 5;
}
if (/team of \d+|company of \d+|\d+ employees/i.test(jd)) {
greenFlags.push("JD mentions specific team/company size");
score += 5;
}
}
// --- Email domain check ---
if (params.recruitingEmail) {
const emailLower = params.recruitingEmail.toLowerCase();
if (/gmail\.com|yahoo\.com|hotmail\.com|outlook\.com/.test(emailLower)) {
redFlags.push(`Recruiter using free email (${emailLower}) β legitimate companies use corporate email`);
score -= 25;
}
else {
const emailDomain = emailLower.split("@")[1] || "";
const companyDomain = params.company.toLowerCase().replace(/[^a-z0-9]/g, "");
// Only match when we have enough characters to avoid false positives on short names
const matchPrefix = companyDomain.slice(0, Math.min(companyDomain.length, 8));
if (emailDomain && matchPrefix.length >= 4 && !emailDomain.includes(matchPrefix)) {
redFlags.push(`Email domain (${emailDomain}) doesn't match company name β could be impersonation`);
score -= 10;
}
else if (emailDomain) {
greenFlags.push(`Recruiter email domain (${emailDomain}) matches company`);
score += 10;
}
}
}
// --- Salary outlier check ---
if (params.salary) {
// Extract the first number from ranges like "$120,000 - $150,000"
const salaryMatch = params.salary.match(/[\d,]+/);
const salaryNum = salaryMatch ? parseFloat(salaryMatch[0].replace(/,/g, "")) : 0;
if (salaryNum > 0) {
const isUsdSalary = /\$|USD/i.test(params.salary);
if (isUsdSalary && salaryNum > 500000) {
redFlags.push(`Salary of ${params.salary} is unusually high β verify this is not bait`);
score -= 10;
}
else if (isUsdSalary && salaryNum > 10000 && salaryNum < 200000) {
greenFlags.push(`Salary range (${params.salary}) is within normal market bounds`);
score += 5;
}
}
}
// --- Web search verification ---
const searchResults = [];
try {
const verifyQuery = `"${params.company}" company legitimacy reviews OR glassdoor OR linkedin`;
const hits = await webSearch(verifyQuery, 6, searchWindow());
let foundGlassdoor = false, foundLinkedin = false, foundCrunchbase = false;
for (const hit of hits) {
searchResults.push({ title: hit.title, url: hit.url, snippet: hit.snippet });
const url = (hit.url || "").toLowerCase();
if (!foundGlassdoor && url.includes("glassdoor")) {
greenFlags.push("Found Glassdoor listing β company has verifiable employee reviews");
score += 10;
foundGlassdoor = true;
}
if (!foundLinkedin && url.includes("linkedin.com/company")) {
greenFlags.push("Company LinkedIn page found β verifiable company profile");
score += 10;
foundLinkedin = true;
}
if (!foundCrunchbase && url.includes("crunchbase")) {
greenFlags.push("Company on Crunchbase β startup funding history verifiable");
score += 5;
foundCrunchbase = true;
}
if (/scam|fraud|fake|warning/i.test(hit.title + hit.snippet)) {
redFlags.push(`Web search found potential warning: "${hit.title}"`);
score -= 20;
}
}
if (hits.length === 0) {
redFlags.push("No web results found for company β no verifiable online presence");
score -= 20;
}
}
catch { /* web search failed β don't penalize */ }
// Clamp score
score = Math.max(0, Math.min(100, score));
let verdict;
if (score >= 75)
verdict = "LIKELY LEGITIMATE β proceed with normal caution";
else if (score >= 50)
verdict = "UNCERTAIN β verify before sharing personal info";
else if (score >= 25)
verdict = "SUSPICIOUS β multiple red flags, research thoroughly";
else
verdict = "HIGH RISK β likely a scam, do not proceed without verification";
// Persist flags to any matching application records
try {
const db = await loadDB(dataDir());
const companyLower = params.company.toLowerCase();
let saved = false;
for (const app of db.applications) {
if (app.company.toLowerCase() === companyLower) {
app.legitimacyScore = score;
app.legitimacyFlags = redFlags;
app.updatedAt = new Date().toISOString();
saved = true;
}
}
if (saved)
await saveDB(dataDir(), db);
}
catch { /* non-fatal */ }
return json({
company: params.company,
jobTitle: params.jobTitle || "Unknown",
legitimacyScore: score,
verdict,
redFlags,
greenFlags,
webSearchResults: searchResults,
verificationChecklist: [
"β Search the company on LinkedIn and verify employee count matches claims",
"β Check Glassdoor for employee reviews (scams rarely have real reviews)",
"β Verify the company domain was registered > 1 year ago (use WHOIS)",
"β Search '[company name] scam' or '[company name] fraud'",
"β Never pay for training, equipment, or background checks upfront",
"β Video interview with real people (not just text chat) is a good sign",
"β Verify the office address on Google Maps Street View",
],
});
}),
}),
// =========================================================================
// LOCATION
// =========================================================================
(0, sdk_1.tool)({
name: "job_settings",
description: (0, sdk_1.text) `
Plugin configuration helpers.
action: "location" β detect current location via IP-based geolocation
action: "toggle_intl" β show current international search setting and how to change it
`,
parameters: {
action: zod_1.z.enum(["location", "toggle_intl"]).describe("Which setting to query"),
enable: zod_1.z.coerce.boolean().optional().describe("For toggle_intl: true=enable, false=disable"),
},
implementation: safe_impl("job_settings", async ({ action, enable }) => {
if (action === "location") {
const res = await fetch("http://ip-api.com/json/?fields=status,country,regionName,city,lat,lon,isp,query", {
signal: AbortSignal.timeout(8000),
});
if (!res.ok)
throw new Error(`IP geolocation returned HTTP ${res.status}`);
const data = await res.json();
if (data.status !== "success")
throw new Error("IP geolocation failed β check network or try again");
const country = String(data.country ?? "");
const city = String(data.city ?? "");
const region = String(data.regionName ?? "");
const isHome = country.toLowerCase() === homeCountry().toLowerCase();
return json({
city, region, country,
coordinates: { lat: data.lat, lon: data.lon },
isp: data.isp,
ip: data.query,
isHomeCountry: isHome,
suggestedSearchLocation: city && region ? `${city}, ${region}` : country,
note: "Location based on public IP β VPNs affect accuracy.",
});
}
if (action === "toggle_intl") {
return json({
requested: enable ? "enable international" : enable === false ? "disable international" : "show current",
currentSetting: openToInternational(),
message: "To change: set 'Open to International Roles' in plugin settings. Or pass includeInternational: true/false on individual search_jobs calls.",
locationSettings: {
preferredLocation: preferredLocation(),
homeCountry: homeCountry(),
openToInternational: openToInternational(),
},
});
}
throw new Error(`Unknown action: ${action}`);
}),
}),
// =========================================================================
// WORK PERMIT & VISA
// =========================================================================
(0, sdk_1.tool)({
name: "check_work_permit",
description: (0, sdk_1.text) `
Check work permit and visa requirements for working in a destination country
as an Indian citizen (or the citizenship configured in plugin settings).
Covers the most common destination countries for Indian professionals:
USA, UK, Canada, Australia, Germany, Singapore, UAE, Netherlands, Japan, and more.
Returns: visa types available, whether employer sponsorship is needed,
typical processing time, eligibility conditions, salary thresholds,
restrictions (e.g. H-1B lottery, skill shortages), and a practical action plan.
Also does a live web search for any recent policy changes.
`,
parameters: {
destinationCountry: zod_1.z.string()
.describe("Country where the job is located (e.g. 'USA', 'UK', 'Germany', 'Singapore')"),
role: zod_1.z.string().default("").describe("Job title β some visas are role/skill specific"),
annualSalary: zod_1.z.string().default("").describe("Expected salary in destination currency β affects some visa thresholds"),
applicationId: zod_1.z.string().default("").describe("Optional: application ID to save the work permit result to"),
},
implementation: safe_impl("check_work_permit", async ({ destinationCountry, role, annualSalary, applicationId }) => {
const userCitizenship = citizenship();
const dest = destinationCountry.trim();
const yr = new Date().getFullYear();
const roleStr = role ? ` ${role}` : "";
const salaryStr = annualSalary ? ` salary ${annualSalary}` : "";
// Fully dynamic β no static database. Immigration rules change too often to hardcode.
const searches = [
{ angle: "visa_types", query: `work visa ${dest} for ${userCitizenship} citizens ${yr} types requirements` },
{ angle: "sponsorship", query: `${dest} work permit employer sponsorship requirements${roleStr} ${yr}` },
{ angle: "salary_threshold", query: `${dest} work visa minimum salary threshold${roleStr}${salaryStr} ${yr}` },
{ angle: "recent_changes", query: `${dest} immigration policy changes ${yr} tech workers ${userCitizenship}` },
{ angle: "official_source", query: `${dest} official immigration website work permit application process` },
];
const webResults = [];
for (const s of searches) {
try {
const hits = await webSearch(s.query, 6, searchWindow());
webResults.push({ angle: s.angle, query: s.query, results: hits });
}
catch {
webResults.push({ angle: s.angle, query: s.query, results: [] });
}
}
// Mark application record if ID provided (status stays unknown until LLM confirms from search)
if (applicationId) {
try {
const db = await loadDB(dataDir());
const idx = db.applications.findIndex((a) => a.id === applicationId);
if (idx !== -1 && db.applications[idx].workPermitStatus === "unknown") {
db.applications[idx].workPermitNotes = `Work permit research for ${dest} pending β see check_work_permit results. Use update_application to save confirmed status.`;
db.applications[idx].updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
}
}
catch { /* non-fatal */ }
}
return json({
destinationCountry: dest,
citizenship: userCitizenship,
role: role || "any",
salaryContext: annualSalary || null,
web_research: webResults,
instructions: `Based on the live web_research above, provide a complete work permit guide for a ${userCitizenship} citizen working in ${dest}${roleStr}. ` +
"Include: (1) available visa types with current requirements and processing times, " +
"(2) whether employer sponsorship is needed, " +
"(3) current salary thresholds (note: these change annually β cite the source), " +
"(4) key restrictions or quota systems, " +
"(5) a practical 3-step action plan. " +
"ALWAYS cite which search result each fact comes from. " +
"Flag any information that may be outdated and direct the user to the official government source found in official_source results. " +
"If after the user confirms the permit status, instruct them to call update_application to save workPermitStatus and workPermitNotes.",
disclaimer: "Immigration rules change frequently. Always verify with official government sources before applying.",
});
}),
}),
// =========================================================================
// RESUME READING
// =========================================================================
(0, sdk_1.tool)({
name: "read_resume",
description: (0, sdk_1.text) `
Read and parse a resume file. Accepts an absolute file path (PDF, TXT, or MD).
If no path is provided, reads from the configured Resume File Path in plugin settings.
Use this tool FIRST when the user provides a resume path or asks you to look at their resume
before searching for jobs or doing any analysis.
`,
parameters: {
filePath: zod_1.z.string().default("")
.describe("Absolute path to the resume file (e.g. /Users/john/resume.pdf). If blank, uses the configured Resume File Path."),
},
implementation: safe_impl("read_resume", async ({ filePath }) => {
let rp = filePath.trim();
if (!rp) {
rp = resumePath();
}
else {
rp = expandPath(rp);
}
if (!rp)
throw new Error("No file path provided and Resume File Path is not configured in plugin settings.");
const content = await readResumeFile(rp);
if (!content.trim())
throw new Error("Resume file is empty.");
return json({
filePath: rp,
charCount: content.length,
resumeText: content.slice(0, 10000),
instructions: "You have now read the user's resume. Summarize the key details: " +
"name, current role, years of experience, core skills, industries, and notable achievements. " +
"Then ask the user what they'd like to do β search for matching jobs, analyze fit against a JD, or generate cover letters.",
});
}),
}),
// =========================================================================
// =========================================================================
// SAVED SEARCHES
// =========================================================================
(0, sdk_1.tool)({
name: "manage_saved_search",
description: (0, sdk_1.text) `
Save, run, list, and delete recurring job searches.
action: "save" β save a search with query/location/type settings
action: "run" β re-run one or all saved searches; returns new results (already-tracked jobs filtered out)
action: "list" β list all saved searches with last-run info
action: "delete" β delete a saved search by ID
`,
parameters: {
action: zod_1.z.enum(["save", "run", "list", "delete"]).describe("Operation"),
name: zod_1.z.string().default("").describe("Search name (save action)"),
query: zod_1.z.string().default("").describe("Search query (save action)"),
location: zod_1.z.string().default(""),
jobType: zod_1.z.enum(["any", "full_time", "part_time", "contract", "internship", "remote"]).default("any"),
includeInternational: zod_1.z.coerce.boolean().default(false),
searchId: zod_1.z.string().default("").describe("Saved search ID (run: specific search; delete: required)"),
},
implementation: safe_impl("manage_saved_search", async (params) => {
const db = await loadDB(dataDir());
if (params.action === "save") {
if (!params.name.trim())
throw new Error("name is required for action=save.");
if (!params.query.trim())
throw new Error("query is required for action=save.");
const ss = { id: makeId(), name: params.name, query: params.query, location: params.location, jobType: params.jobType, includeInternational: params.includeInternational, createdAt: new Date().toISOString(), lastRunAt: "", lastResultCount: 0 };
db.savedSearches.push(ss);
await saveDB(dataDir(), db);
return json({ success: true, savedSearch: ss });
}
if (params.action === "run") {
if (db.savedSearches.length === 0)
throw new Error("No saved searches. Use manage_saved_search(action='save') to create one.");
const searches = params.searchId ? db.savedSearches.filter((s) => s.id === params.searchId) : db.savedSearches;
if (searches.length === 0)
throw new Error(`Saved search ID '${params.searchId}' not found.`);
const tracked = buildTrackedSets(db.applications);
const allResults = [];
for (const s of searches) {
const loc = s.location || preferredLocation();
const isHome = extractCountry(loc).toLowerCase() === homeCountry().toLowerCase();
const sites = isHome ? homeBoards() : globalBoards();
const typeTag = s.jobType !== "any" ? ` ${s.jobType.replace("_", " ")}` : "";
const baseQuery = `${s.query}${typeTag} ${loc} job`;
let hits = await webSearch(`${baseQuery} ${sites}`, maxResults() + 10, searchWindow());
if (hits.length === 0)
hits = await webSearch(baseQuery, maxResults() + 10, searchWindow());
const { results: filtered } = filterTracked(hits, tracked, maxResults());
const idx = db.savedSearches.findIndex((ss) => ss.id === s.id);
if (idx !== -1) {
db.savedSearches[idx].lastRunAt = new Date().toISOString();
db.savedSearches[idx].lastResultCount = filtered.length;
}
allResults.push({ searchName: s.name, searchId: s.id, results: filtered, newCount: filtered.length });
}
await saveDB(dataDir(), db);
return json({ searchesRun: allResults.length, results: allResults });
}
if (params.action === "list") {
return json({ count: db.savedSearches.length, searches: db.savedSearches.map((s) => ({ id: s.id, name: s.name, query: s.query, location: s.location, jobType: s.jobType, lastRunAt: s.lastRunAt || "never", lastResultCount: s.lastResultCount })) });
}
if (params.action === "delete") {
if (!params.searchId.trim())
throw new Error("searchId is required for action=delete.");
const before = db.savedSearches.length;
db.savedSearches = db.savedSearches.filter((s) => s.id !== params.searchId);
if (db.savedSearches.length === before)
throw new Error(`Saved search ID '${params.searchId}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: params.searchId });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
// =========================================================================
// CONTACTS MANAGEMENT (section header moved up)
// =========================================================================
// CONTACTS MANAGEMENT
// =========================================================================
(0, sdk_1.tool)({
name: "manage_contacts",
description: (0, sdk_1.text) `
Add, remove, or list contacts for a tracked application.
Track recruiters, hiring managers, referrals, and networking connections.
`,
parameters: {
applicationId: zod_1.z.string().describe("Application ID"),
action: zod_1.z.enum(["add", "remove", "list"]).describe("Action to perform"),
name: zod_1.z.string().default("").describe("Contact name (required for add/remove)"),
role: zod_1.z.string().default("").describe("Contact's role/title (e.g. 'Recruiter', 'Hiring Manager')"),
email: zod_1.z.string().default("").describe("Contact email"),
linkedIn: zod_1.z.string().default("").describe("LinkedIn profile URL"),
notes: zod_1.z.string().default("").describe("Notes about this contact"),
},
implementation: safe_impl("manage_contacts", async (params) => {
const db = await loadDB(dataDir());
const idx = db.applications.findIndex((a) => a.id === params.applicationId);
if (idx === -1)
throw new Error(`Application ID '${params.applicationId}' not found.`);
const app = db.applications[idx];
if (params.action === "list") {
return json({ company: app.company, role: app.role, contacts: app.contacts });
}
if (params.action === "add") {
if (!params.name.trim())
throw new Error("Contact name is required.");
app.contacts.push({
name: params.name,
role: params.role,
email: params.email,
linkedIn: params.linkedIn,
notes: params.notes,
});
app.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, action: "added", contact: params.name, totalContacts: app.contacts.length });
}
if (params.action === "remove") {
if (!params.name.trim())
throw new Error("Contact name is required to remove.");
const before = app.contacts.length;
app.contacts = app.contacts.filter((c) => c.name.toLowerCase() !== params.name.toLowerCase());
if (app.contacts.length === before)
throw new Error(`Contact '${params.name}' not found.`);
app.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, action: "removed", contact: params.name, totalContacts: app.contacts.length });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
// =========================================================================
// =========================================================================
// =========================================================================
// DAILY BRIEFING
// =========================================================================
(0, sdk_1.tool)({
name: "daily_briefing",
description: (0, sdk_1.text) `
Morning check-in: runs follow-ups, stale detection, saved searches, and pipeline stats
in one call. Returns a combined briefing with action items.
Use when the user says "morning briefing", "what's new", "daily update", "check-in".
`,
parameters: {
staleDays: zod_1.z.coerce.number().int().min(1).max(90).default(14)
.describe("Days before considering an application stale"),
followUpDays: zod_1.z.coerce.number().int().min(0).max(30).default(7)
.describe("Show follow-ups due within this many days"),
},
implementation: safe_impl("daily_briefing", async ({ staleDays, followUpDays }) => {
const db = await loadDB(dataDir());
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const apps = db.applications;
// --- Pipeline stats ---
const byStatus = {};
for (const a of apps)
byStatus[a.status] = (byStatus[a.status] ?? 0) + 1;
const applied = apps.filter((a) => ["applied", "interview", "offer", "rejected", "withdrawn"].includes(a.status)).length;
const interviewed = apps.filter((a) => ["interview", "offer"].includes(a.status) || (a.status === "rejected" && a.interviewNotes.length > 0)).length;
const offers = apps.filter((a) => a.status === "offer").length;
// --- Follow-ups ---
const cutoffDate = new Date(now);
cutoffDate.setDate(cutoffDate.getDate() + followUpDays);
const cutoffStr = cutoffDate.toISOString().slice(0, 10);
const dateRe = /^\d{4}-\d{2}-\d{2}$/;
const followUps = apps
.filter((a) => !["rejected", "withdrawn"].includes(a.status) && dateRe.test(a.followUpDate ?? "") && a.followUpDate <= cutoffStr)
.map((a) => ({
id: a.id, company: a.company, role: a.role, status: a.status,
followUpDate: a.followUpDate,
overdue: a.followUpDate < todayStr,
nextStep: a.nextStep,
}))
.sort((a, b) => a.followUpDate.localeCompare(b.followUpDate));
// --- Stale applications ---
const staleCutoff = new Date(now.getTime() - staleDays * 24 * 60 * 60 * 1000);
const stale = apps
.filter((a) => ["applied", "interview"].includes(a.status) && new Date(a.updatedAt) < staleCutoff)
.map((a) => ({
id: a.id, company: a.company, role: a.role, status: a.status,
daysSinceUpdate: Math.floor((now.getTime() - new Date(a.updatedAt).getTime()) / (1000 * 60 * 60 * 24)),
}));
// --- Saved searches ---
const searchResults = [];
const tracked = buildTrackedSets(apps);
for (const s of db.savedSearches) {
const loc = s.location || preferredLocation();
const isHome = extractCountry(loc).toLowerCase() === homeCountry().toLowerCase();
const sites = isHome ? homeBoards() : globalBoards();
const typeTag = s.jobType !== "any" ? ` ${s.jobType.replace("_", " ")}` : "";
const baseQuery = `${s.query}${typeTag} ${loc} job`;
try {
let hits = await webSearch(`${baseQuery} ${sites}`, maxResults() + 5, searchWindow());
if (hits.length === 0)
hits = await webSearch(baseQuery, maxResults() + 5, searchWindow());
const { results: filtered } = filterTracked(hits, tracked, maxResults());
const idx = db.savedSearches.findIndex((ss) => ss.id === s.id);
if (idx !== -1) {
db.savedSearches[idx].lastRunAt = now.toISOString();
db.savedSearches[idx].lastResultCount = filtered.length;
}
searchResults.push({ name: s.name, newCount: filtered.length, results: filtered });
}
catch {
searchResults.push({ name: s.name, newCount: 0, results: [] });
}
}
if (db.savedSearches.length > 0)
await saveDB(dataDir(), db);
// --- Network follow-ups ---
const networkOverdue = db.network
.filter((c) => c.followUpDate && c.followUpDate <= todayStr)
.map((c) => ({
id: c.id, name: c.name, company: c.company, relationship: c.relationship,
followUpDate: c.followUpDate, lastContactDate: c.lastContactDate,
}));
return json({
date: todayStr,
pipeline: {
total: apps.length,
byStatus,
conversionRate: applied > 0 ? `${Math.round((offers / applied) * 100)}% appliedβoffer` : "no data",
},
followUps: {
count: followUps.length,
overdue: followUps.filter((f) => f.overdue).length,
items: followUps,
},
staleApplications: {
count: stale.length,
items: stale,
},
networkFollowUps: {
overdueCount: networkOverdue.length,
contacts: networkOverdue,
},
newJobResults: {
searchesRun: searchResults.length,
totalNewJobs: searchResults.reduce((s, r) => s + r.newCount, 0),
bySearch: searchResults,
},
instructions: "Present this as a morning briefing. Lead with urgent items (overdue follow-ups, stale apps, network follow-ups). " +
"Then show new job results from saved searches. End with pipeline health. " +
"Give concrete action items: 'Send follow-up to X', 'Withdraw stale app at Y', 'Reach out to Z', 'Check out 3 new postings'.",
});
}),
}),
// =========================================================================
// JOB COMPARISON
// =========================================================================
(0, sdk_1.tool)({
name: "compare_jobs",
description: (0, sdk_1.text) `
Side-by-side comparison of 2β5 tracked applications.
Compares salary, location, status, fit indicators, legitimacy, contacts,
interview progress, and any other tracked data.
Pass application IDs to compare.
`,
parameters: {
applicationIds: zod_1.z.array(zod_1.z.string()).min(2).max(5)
.describe("Array of 2β5 application IDs to compare"),
},
implementation: safe_impl("compare_jobs", async ({ applicationIds }) => {
const db = await loadDB(dataDir());
const apps = applicationIds.map((id) => {
const app = db.applications.find((a) => a.id === id);
if (!app)
throw new Error(`Application ID '${id}' not found.`);
return app;
});
const comparison = apps.map((a) => ({
id: a.id,
company: a.company,
role: a.role,
location: a.location,
country: a.country,
isInternational: a.isInternational,
status: a.status,
salary: a.salary || "not specified",
totalComp: a.totalComp || "not calculated",
appliedDate: a.appliedDate || "not applied",
interviewRounds: a.interviewNotes.length,
lastInterviewOutcome: a.interviewNotes.length > 0
? a.interviewNotes[a.interviewNotes.length - 1].outcome
: "n/a",
contactCount: a.contacts.length,
legitimacyScore: a.legitimacyScore >= 0 ? a.legitimacyScore : "not checked",
workPermitStatus: a.workPermitStatus,
nextStep: a.nextStep || "none",
followUpDate: a.followUpDate || "not set",
}));
return json({
count: comparison.length,
jobs: comparison,
instructions: "Present this as a side-by-side comparison table. Highlight: " +
"(1) Which has the best compensation. " +
"(2) Which is furthest along in the pipeline. " +
"(3) Any red flags (low legitimacy, missing salary, no contacts). " +
"(4) A recommendation on which to prioritize and why.",
});
}),
}),
// =========================================================================
// BATCH OPERATIONS
// =========================================================================
(0, sdk_1.tool)({
name: "batch_update_applications",
description: (0, sdk_1.text) `
Bulk-update multiple applications at once. Useful for:
- Withdrawing all stale applications
- Marking multiple as rejected
- Cleaning up the pipeline
Pass an array of application IDs and the fields to update on all of them.
`,
parameters: {
applicationIds: zod_1.z.array(zod_1.z.string()).min(1)
.describe("Array of application IDs to update"),
status: zod_1.z.enum(["saved", "applied", "interview", "offer", "rejected", "withdrawn"]).optional()
.describe("New status for all selected applications"),
rejectionReason: zod_1.z.string().optional()
.describe("Rejection reason (when bulk-marking as rejected)"),
notes: zod_1.z.string().optional()
.describe("Notes to append (not replace) on all selected applications"),
},
implementation: safe_impl("batch_update_applications", async ({ applicationIds, status, rejectionReason, notes }) => {
if (!status && !rejectionReason && !notes)
throw new Error("Nothing to update. Provide at least one of: status, rejectionReason, notes.");
const db = await loadDB(dataDir());
const updated = [];
const notFound = [];
for (const id of applicationIds) {
const idx = db.applications.findIndex((a) => a.id === id);
if (idx === -1) {
notFound.push(id);
continue;
}
const app = db.applications[idx];
if (status && status !== app.status) {
app.statusHistory.push({ status, date: todayStr() });
app.status = status;
if (status === "applied")
stampAppliedDate(app);
}
if (rejectionReason)
app.rejectionReason = rejectionReason;
if (notes)
app.notes = app.notes ? `${app.notes}\n${notes}` : notes;
app.updatedAt = new Date().toISOString();
updated.push(`${app.company} β ${app.role}`);
}
await saveDB(dataDir(), db);
return json({
success: true,
updatedCount: updated.length,
updated,
notFound: notFound.length > 0 ? notFound : null,
changes: { status: status ?? "unchanged", rejectionReason: rejectionReason ?? "unchanged", notesAppended: !!notes },
});
}),
}),
// =========================================================================
// NETWORKING
// =========================================================================
(0, sdk_1.tool)({
name: "manage_network",
description: (0, sdk_1.text) `
Professional network tracker β referrers, hiring managers, alumni, ex-colleagues.
action: "add" β add a new contact
action: "update" β update contact fields by ID
action: "list" β list network with optional filters
action: "log" β log an interaction (updates lastContactDate)
action: "delete" β delete a contact by ID
action: "referrers" β find contacts who can refer you to a specific company
`,
parameters: {
action: zod_1.z.enum(["add", "update", "list", "log", "delete", "referrers"]).describe("Operation"),
id: zod_1.z.string().default("").describe("Contact ID (update/log/delete)"),
name: zod_1.z.string().default(""),
company: zod_1.z.string().default(""),
role: zod_1.z.string().default(""),
email: zod_1.z.string().default(""),
linkedIn: zod_1.z.string().default(""),
relationship: zod_1.z.enum(["cold", "warm", "strong", "referrer"]).optional(),
source: zod_1.z.string().default(""),
targetCompanies: zod_1.z.array(zod_1.z.string()).default([]),
notes: zod_1.z.string().default("").describe("For update/log: text to APPEND"),
tags: zod_1.z.array(zod_1.z.string()).default([]),
followUpDate: zod_1.z.string().default(""),
filterRelationship: zod_1.z.enum(["cold", "warm", "strong", "referrer", "all"]).default("all"),
filterCompany: zod_1.z.string().default(""),
filterTag: zod_1.z.string().default(""),
overdueOnly: zod_1.z.coerce.boolean().default(false),
search: zod_1.z.string().default(""),
summary: zod_1.z.string().default("").describe("What happened in the interaction (log action)"),
upgradeRelationship: zod_1.z.coerce.boolean().default(false),
followUpInDays: zod_1.z.coerce.number().int().min(0).max(90).default(0),
targetCompany: zod_1.z.string().default("").describe("Company to find referrers for (referrers action)"),
},
implementation: safe_impl("manage_network", async (params) => {
const db = await loadDB(dataDir());
if (params.action === "add") {
if (!params.name.trim())
throw new Error("name is required for action=add.");
const contact = {
id: makeId(), name: params.name, company: params.company, role: params.role,
email: params.email, linkedIn: params.linkedIn,
relationship: params.relationship ?? "cold", source: params.source,
targetCompanies: params.targetCompanies, lastContactDate: todayStr(),
followUpDate: params.followUpDate, notes: params.notes, tags: params.tags,
linkedApplicationIds: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
};
db.network.push(contact);
await saveDB(dataDir(), db);
return json({ success: true, contact });
}
if (params.action === "update") {
if (!params.id.trim())
throw new Error("id is required for action=update.");
const idx = db.network.findIndex((c) => c.id === params.id);
if (idx === -1)
throw new Error(`Network contact '${params.id}' not found.`);
const c = db.network[idx];
if (params.name)
c.name = params.name;
if (params.company)
c.company = params.company;
if (params.role)
c.role = params.role;
if (params.email)
c.email = params.email;
if (params.linkedIn)
c.linkedIn = params.linkedIn;
if (params.relationship)
c.relationship = params.relationship;
if (params.source)
c.source = params.source;
if (params.targetCompanies.length > 0)
c.targetCompanies = params.targetCompanies;
if (params.followUpDate)
c.followUpDate = params.followUpDate;
if (params.notes)
c.notes = c.notes ? `${c.notes}\n${params.notes}` : params.notes;
if (params.tags.length > 0)
c.tags = params.tags;
c.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, contact: c });
}
if (params.action === "list") {
let contacts = db.network;
if (params.filterRelationship !== "all")
contacts = contacts.filter((c) => c.relationship === params.filterRelationship);
if (params.filterCompany) {
const kw = params.filterCompany.toLowerCase();
contacts = contacts.filter((c) => c.company.toLowerCase().includes(kw));
}
if (params.filterTag) {
const kw = params.filterTag.toLowerCase();
contacts = contacts.filter((c) => c.tags.some((t) => t.toLowerCase().includes(kw)));
}
if (params.search) {
const kw = params.search.toLowerCase();
contacts = contacts.filter((c) => c.name.toLowerCase().includes(kw) || c.company.toLowerCase().includes(kw) || c.notes.toLowerCase().includes(kw));
}
const today = todayStr();
if (params.overdueOnly)
contacts = contacts.filter((c) => c.followUpDate && c.followUpDate <= today);
contacts = contacts.slice().sort((a, b) => { const ao = a.followUpDate && a.followUpDate <= today ? 0 : 1; const bo = b.followUpDate && b.followUpDate <= today ? 0 : 1; return ao !== bo ? ao - bo : (a.followUpDate && b.followUpDate ? a.followUpDate.localeCompare(b.followUpDate) : a.lastContactDate.localeCompare(b.lastContactDate)); });
const stats = { total: db.network.length, cold: db.network.filter((c) => c.relationship === "cold").length, warm: db.network.filter((c) => c.relationship === "warm").length, strong: db.network.filter((c) => c.relationship === "strong").length, referrer: db.network.filter((c) => c.relationship === "referrer").length, overdueFollowUps: db.network.filter((c) => c.followUpDate && c.followUpDate <= today).length };
return json({ stats, contacts: contacts.map((c) => ({ id: c.id, name: c.name, company: c.company, role: c.role, relationship: c.relationship, lastContactDate: c.lastContactDate, followUpDate: c.followUpDate || "not set", overdue: c.followUpDate ? c.followUpDate <= today : false, targetCompanies: c.targetCompanies, tags: c.tags })) });
}
if (params.action === "log") {
if (!params.id.trim())
throw new Error("id is required for action=log.");
if (!params.summary.trim())
throw new Error("summary is required for action=log.");
const idx = db.network.findIndex((c) => c.id === params.id);
if (idx === -1)
throw new Error(`Network contact '${params.id}' not found.`);
const c = db.network[idx];
const order = ["cold", "warm", "strong", "referrer"];
if (params.upgradeRelationship) {
const cur = order.indexOf(c.relationship);
if (cur < order.length - 1)
c.relationship = order[cur + 1];
}
c.lastContactDate = todayStr();
if (params.followUpInDays > 0) {
const d = new Date();
d.setDate(d.getDate() + params.followUpInDays);
c.followUpDate = d.toISOString().slice(0, 10);
}
c.notes = c.notes ? `${c.notes}\n[${todayStr()}] ${params.summary}` : `[${todayStr()}] ${params.summary}`;
c.updatedAt = new Date().toISOString();
await saveDB(dataDir(), db);
return json({ success: true, contact: c.name, relationship: c.relationship, followUpDate: c.followUpDate || "not set" });
}
if (params.action === "delete") {
if (!params.id.trim())
throw new Error("id is required for action=delete.");
const before = db.network.length;
db.network = db.network.filter((c) => c.id !== params.id);
if (db.network.length === before)
throw new Error(`Network contact '${params.id}' not found.`);
await saveDB(dataDir(), db);
return json({ success: true, deleted: params.id });
}
if (params.action === "referrers") {
const kw = (params.targetCompany || params.company).toLowerCase();
if (!kw)
throw new Error("targetCompany is required for action=referrers.");
const candidates = db.network.filter((c) => c.company.toLowerCase().includes(kw) ||
c.targetCompanies.some((t) => t.toLowerCase().includes(kw))).map((c) => ({
id: c.id, name: c.name, company: c.company, role: c.role,
relationship: c.relationship, email: c.email, linkedIn: c.linkedIn,
canRefer: c.company.toLowerCase().includes(kw),
connectedTo: c.targetCompanies.filter((t) => t.toLowerCase().includes(kw)),
}));
return json({ targetCompany: params.targetCompany, candidates, instructions: "Present referral options by relationship strength. For 'strong'/'referrer': suggest asking for a direct referral. For 'warm': suggest a catch-up first. For 'cold': suggest warming up with a value-add message before asking." });
}
throw new Error(`Unknown action: ${params.action}`);
}),
}),
];
return tools;
};
exports.toolsProvider = toolsProvider;