feat: PM2 as canonical scheduler — SKILL.md updated, setup.mjs simplified, OpenClaw gateway cron removed
This commit is contained in:
66
SKILL.md
66
SKILL.md
@@ -71,22 +71,59 @@ cp config/search_config.example.json config/search_config.json
|
|||||||
|
|
||||||
**`search_config.json`** — keywords, platforms, location filters, salary filters, exclusions
|
**`search_config.json`** — keywords, platforms, location filters, salary filters, exclusions
|
||||||
|
|
||||||
### 4. Verify
|
### 4. Create .env
|
||||||
|
|
||||||
|
Create a `.env` file in the project root (gitignored — never commit this):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export KERNEL_API_KEY=your_kernel_api_key
|
KERNEL_API_KEY=your_kernel_api_key
|
||||||
export ANTHROPIC_API_KEY=your_anthropic_api_key # optional, for AI keywords
|
ANTHROPIC_API_KEY=your_anthropic_api_key # optional, for AI keywords
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
node setup.mjs
|
node setup.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
Setup will:
|
Setup will:
|
||||||
- Validate all config files
|
- Validate all config files
|
||||||
|
- Write `.env` (mode 600) if API keys are set
|
||||||
- Send a Telegram test message
|
- Send a Telegram test message
|
||||||
- Test LinkedIn + Wellfound logins
|
- Test LinkedIn + Wellfound logins
|
||||||
- Auto-register cron jobs (if `OPENCLAW_GATEWAY_URL` + `OPENCLAW_GATEWAY_TOKEN` are set)
|
|
||||||
|
|
||||||
### 5. Run manually
|
### 6. Schedule with PM2
|
||||||
|
|
||||||
|
PM2 is a Node.js process manager that runs the searcher and applier as proper system daemons — no SIGTERM issues, survives reboots.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install PM2
|
||||||
|
npm install -g pm2
|
||||||
|
|
||||||
|
# Start both jobs (searcher runs immediately + hourly; applier stopped by default)
|
||||||
|
pm2 start ecosystem.config.cjs
|
||||||
|
pm2 stop claw-applier # keep applier off until you're ready
|
||||||
|
|
||||||
|
# Survive reboots
|
||||||
|
pm2 save
|
||||||
|
pm2 startup # follow the printed command (requires sudo)
|
||||||
|
```
|
||||||
|
|
||||||
|
**PM2 cheatsheet:**
|
||||||
|
```bash
|
||||||
|
pm2 list # show all jobs + status
|
||||||
|
pm2 logs claw-searcher # tail searcher logs
|
||||||
|
pm2 logs claw-applier # tail applier logs
|
||||||
|
pm2 start claw-applier # enable applier
|
||||||
|
pm2 stop claw-applier # pause applier
|
||||||
|
pm2 restart claw-searcher # restart searcher now
|
||||||
|
```
|
||||||
|
|
||||||
|
Schedules (set in `ecosystem.config.cjs`):
|
||||||
|
- **Searcher**: `0 * * * *` (hourly)
|
||||||
|
- **Applier**: `0 */6 * * *` (every 6h) — stopped by default, start when ready
|
||||||
|
|
||||||
|
### 7. Run manually
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node job_searcher.mjs # search now
|
node job_searcher.mjs # search now
|
||||||
@@ -95,25 +132,6 @@ node job_applier.mjs # apply now
|
|||||||
node status.mjs # show queue + run status
|
node status.mjs # show queue + run status
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Schedule
|
|
||||||
|
|
||||||
**Via OpenClaw** — set env vars before running `setup.mjs`:
|
|
||||||
```bash
|
|
||||||
export OPENCLAW_GATEWAY_URL=http://localhost:3000
|
|
||||||
export OPENCLAW_GATEWAY_TOKEN=your_gateway_token
|
|
||||||
node setup.mjs # registers crons automatically
|
|
||||||
```
|
|
||||||
|
|
||||||
Crons registered:
|
|
||||||
- **Searcher**: `0 * * * *` (hourly), no timeout, enabled by default
|
|
||||||
- **Applier**: `0 */6 * * *` (every 6h), no timeout, **disabled by default** — enable when ready
|
|
||||||
|
|
||||||
**Via system cron** — add to crontab:
|
|
||||||
```
|
|
||||||
0 * * * * cd /path/to/claw-apply && KERNEL_API_KEY=xxx node job_searcher.mjs >> /tmp/searcher.log 2>&1
|
|
||||||
0 */6 * * * cd /path/to/claw-apply && KERNEL_API_KEY=xxx node job_applier.mjs >> /tmp/applier.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
**Search** — runs your keyword searches on LinkedIn and Wellfound, paginates through results, inline-classifies each job (Easy Apply vs external ATS), filters exclusions, deduplicates, and queues new jobs. First run searches 90 days back; subsequent runs search 2 days.
|
**Search** — runs your keyword searches on LinkedIn and Wellfound, paginates through results, inline-classifies each job (Easy Apply vs external ATS), filters exclusions, deduplicates, and queues new jobs. First run searches 90 days back; subsequent runs search 2 days.
|
||||||
|
|||||||
74
setup.mjs
74
setup.mjs
@@ -4,7 +4,7 @@
|
|||||||
* Verifies config, tests logins, registers cron jobs
|
* Verifies config, tests logins, registers cron jobs
|
||||||
* Run once after configuring: node setup.mjs
|
* Run once after configuring: node setup.mjs
|
||||||
*/
|
*/
|
||||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
|
||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { loadConfig } from './lib/queue.mjs';
|
import { loadConfig } from './lib/queue.mjs';
|
||||||
@@ -94,70 +94,16 @@ async function main() {
|
|||||||
await wfBrowser?.browser?.close().catch(() => {});
|
await wfBrowser?.browser?.close().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register cron jobs via OpenClaw gateway
|
|
||||||
const gatewayUrl = process.env.OPENCLAW_GATEWAY_URL || 'http://localhost:3000';
|
|
||||||
const gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
||||||
const telegramUserId = settings.notifications?.telegram_user_id;
|
|
||||||
const searchSchedule = settings.schedules?.search || '0 * * * *';
|
|
||||||
const applySchedule = settings.schedules?.apply || '0 */6 * * *';
|
|
||||||
|
|
||||||
// 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}` };
|
|
||||||
|
|
||||||
const searchJob = {
|
|
||||||
name: 'claw-apply: job searcher',
|
|
||||||
schedule: { kind: 'cron', expr: searchSchedule, tz: settings.timezone || 'America/Los_Angeles' },
|
|
||||||
payload: {
|
|
||||||
kind: 'agentTurn',
|
|
||||||
message: `Launch the claw-apply job searcher as a detached background process: \`cd ${__dir} && nohup node job_searcher.mjs >> /tmp/claw-searcher.log 2>&1 & echo $!\`. Confirm the PID and that it started successfully.`,
|
|
||||||
timeoutSeconds: 30,
|
|
||||||
},
|
|
||||||
delivery: { mode: 'announce', to: telegramUserId },
|
|
||||||
sessionTarget: 'isolated',
|
|
||||||
enabled: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyJob = {
|
|
||||||
name: 'claw-apply: job applier',
|
|
||||||
schedule: { kind: 'cron', expr: applySchedule, tz: settings.timezone || 'America/Los_Angeles' },
|
|
||||||
payload: {
|
|
||||||
kind: 'agentTurn',
|
|
||||||
message: `Launch the claw-apply job applier as a detached background process: \`cd ${__dir} && nohup node job_applier.mjs >> /tmp/claw-applier.log 2>&1 & echo $!\`. Confirm the PID and that it started successfully.`,
|
|
||||||
timeoutSeconds: 30,
|
|
||||||
},
|
|
||||||
delivery: { mode: 'announce', to: telegramUserId },
|
|
||||||
sessionTarget: 'isolated',
|
|
||||||
enabled: false, // disabled by default — enable when ready
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sRes = await fetch(`${gatewayUrl}/api/cron/jobs`, { method: 'POST', headers, body: JSON.stringify(searchJob) });
|
|
||||||
const sData = await sRes.json();
|
|
||||||
console.log(sRes.ok ? ` ✅ Searcher cron registered (ID: ${sData.id})` : ` ❌ Searcher cron failed: ${JSON.stringify(sData)}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` ❌ Searcher cron error: ${e.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const aRes = await fetch(`${gatewayUrl}/api/cron/jobs`, { method: 'POST', headers, body: JSON.stringify(applyJob) });
|
|
||||||
const aData = await aRes.json();
|
|
||||||
console.log(aRes.ok ? ` ✅ Applier cron registered, disabled (ID: ${aData.id}) — enable when ready` : ` ❌ Applier cron failed: ${JSON.stringify(aData)}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(` ❌ Applier cron error: ${e.message}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('\n⚠️ Skipping cron registration — set OPENCLAW_GATEWAY_URL and OPENCLAW_GATEWAY_TOKEN to auto-register.');
|
|
||||||
console.log(' Or register manually with these schedules:');
|
|
||||||
console.log(` Search: ${searchSchedule} (timeoutSeconds: 0)`);
|
|
||||||
console.log(` Apply: ${applySchedule} (timeoutSeconds: 0, start disabled)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎉 Setup complete. claw-apply is ready.');
|
console.log('\n🎉 Setup complete. claw-apply is ready.');
|
||||||
console.log('\nTo run manually:');
|
console.log('\nNext: start the scheduler with PM2:');
|
||||||
console.log(' node job_searcher.mjs — search now');
|
console.log(' npm install -g pm2');
|
||||||
console.log(' node job_applier.mjs — apply now');
|
console.log(' pm2 start ecosystem.config.cjs');
|
||||||
|
console.log(' pm2 stop claw-applier # keep off until ready to apply');
|
||||||
|
console.log(' pm2 save && pm2 startup # survive reboots');
|
||||||
|
console.log('\nRun manually:');
|
||||||
|
console.log(' node job_searcher.mjs — search now');
|
||||||
|
console.log(' node job_applier.mjs — apply now');
|
||||||
|
console.log(' node status.mjs — queue + run status');
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(e => { console.error('Setup error:', e.message); process.exit(1); });
|
main().catch(e => { console.error('Setup error:', e.message); process.exit(1); });
|
||||||
|
|||||||
Reference in New Issue
Block a user