diff --git a/.gitignore b/.gitignore index 4376aec..a4cba82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ +.env # Runtime data — never commit data/jobs_queue.json diff --git a/job_applier.mjs b/job_applier.mjs index 0c29bf3..25fbdce 100644 --- a/job_applier.mjs +++ b/job_applier.mjs @@ -1,4 +1,6 @@ #!/usr/bin/env node +import { loadEnv } from './lib/env.mjs'; +loadEnv(); // load .env before anything else /** * job_applier.mjs — claw-apply Job Applier * Reads jobs queue and applies using the appropriate handler per apply_type diff --git a/job_searcher.mjs b/job_searcher.mjs index 1612771..1661d3a 100644 --- a/job_searcher.mjs +++ b/job_searcher.mjs @@ -4,6 +4,10 @@ * Searches LinkedIn + Wellfound and populates the jobs queue * Run via cron or manually: node job_searcher.mjs */ +import { loadEnv } from './lib/env.mjs'; +loadEnv(); // load .env before anything else +/** + */ import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; diff --git a/lib/env.mjs b/lib/env.mjs new file mode 100644 index 0000000..417adaa --- /dev/null +++ b/lib/env.mjs @@ -0,0 +1,27 @@ +/** + * env.mjs — Load .env file if present + * Reads key=value pairs from .env and sets process.env for any missing vars. + * Never overwrites vars already set in the environment. + */ +import { existsSync, readFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const envPath = resolve(__dir, '../.env'); + +export function loadEnv() { + if (!existsSync(envPath)) return; + const lines = readFileSync(envPath, 'utf8').split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq === -1) continue; + const key = trimmed.slice(0, eq).trim(); + const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, ''); + if (key && !(key in process.env)) { + process.env[key] = val; + } + } +} diff --git a/setup.mjs b/setup.mjs index bd3fa68..0f091d3 100644 --- a/setup.mjs +++ b/setup.mjs @@ -4,10 +4,12 @@ * Verifies config, tests logins, registers cron jobs * Run once after configuring: node setup.mjs */ -import { existsSync, mkdirSync } from 'fs'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; import { loadConfig } from './lib/queue.mjs'; +import { loadEnv } from './lib/env.mjs'; +loadEnv(); const __dir = dirname(fileURLToPath(import.meta.url)); @@ -46,6 +48,16 @@ async function main() { mkdirSync(resolve(__dir, 'data'), { recursive: true }); console.log('\n✅ Data directory ready'); + // Write .env file with API keys (gitignored — never embedded in cron payloads) + const envPath = resolve(__dir, '.env'); + const envLines = []; + if (process.env.KERNEL_API_KEY) envLines.push(`KERNEL_API_KEY=${process.env.KERNEL_API_KEY}`); + if (process.env.ANTHROPIC_API_KEY) envLines.push(`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`); + if (envLines.length) { + writeFileSync(envPath, envLines.join('\n') + '\n', { mode: 0o600 }); + console.log('✅ API keys written to .env (mode 600, gitignored)'); + } + // Test Telegram if (settings.notifications?.bot_token && settings.notifications?.telegram_user_id) { const { sendTelegram } = await import('./lib/notify.mjs'); @@ -88,9 +100,8 @@ async function main() { const telegramUserId = settings.notifications?.telegram_user_id; const searchSchedule = settings.schedules?.search || '0 * * * *'; const applySchedule = settings.schedules?.apply || '0 */6 * * *'; - const kernelApiKey = process.env.KERNEL_API_KEY; - const anthropicApiKey = process.env.ANTHROPIC_API_KEY || ''; + // API keys are loaded from .env at runtime — never embedded in cron payloads if (gatewayToken && telegramUserId) { console.log('\nRegistering cron jobs...'); const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${gatewayToken}` }; @@ -100,7 +111,7 @@ async function main() { schedule: { kind: 'cron', expr: searchSchedule, tz: settings.timezone || 'America/Los_Angeles' }, payload: { kind: 'agentTurn', - message: `Run the claw-apply job searcher: \`cd ${__dir} && KERNEL_API_KEY=${kernelApiKey}${anthropicApiKey ? ` ANTHROPIC_API_KEY=${anthropicApiKey}` : ''} node job_searcher.mjs 2>&1\`. Report how many new jobs were found.`, + message: `Run the claw-apply job searcher: \`cd ${__dir} && node job_searcher.mjs 2>&1\`. Report how many new jobs were found.`, timeoutSeconds: 0, }, delivery: { mode: 'announce', to: telegramUserId }, @@ -113,7 +124,7 @@ async function main() { schedule: { kind: 'cron', expr: applySchedule, tz: settings.timezone || 'America/Los_Angeles' }, payload: { kind: 'agentTurn', - message: `Run the claw-apply job applier: \`cd ${__dir} && KERNEL_API_KEY=${kernelApiKey}${anthropicApiKey ? ` ANTHROPIC_API_KEY=${anthropicApiKey}` : ''} node job_applier.mjs 2>&1\`. Report results.`, + message: `Run the claw-apply job applier: \`cd ${__dir} && node job_applier.mjs 2>&1\`. Report results.`, timeoutSeconds: 0, }, delivery: { mode: 'announce', to: telegramUserId },