Skill: zhangdong/post-job (version 2.1.3)

--- name: post-job description: Post free job ads to 20+ job boards such as LinkedIn, Indeed, Ziprecruiter etc. to receive applicant resumes via email. metadata: { "openclaw": { "requires": { "bins": ["node", "npm"] }, "install": [ { "id": "install-deps", "kind": "run", "label": "Install post-job dependencies", "command": "npm install" } ] } } --- # JobPoster Skill > ⚠️ **CRITICAL: Use this skill's canonical execution entrypoints for all job posting actions.** > > **Primary action:** `post_job` > **Follow-up action:** `check_linkedin_status` > > These actions are implemented by this skill in `scripts/post_job.js`. > Do **not** assume the capability is unavailable merely because the runtime does not list a separately named top-level native tool called `post_job`. > > **DO NOT** call internal APIs directly (`genJD`, `postToLinkd`, `API_URL_*`). > **DO NOT** bypass `post_job` to call Fuku AI endpoints manually. > **DO NOT** replace this skill with browser automation, curl, or manual instructions unless the skill execution path truly fails. > > **Why?** The `post_job` action handles: > > - Input validation and sanitization > - Location fuzzy matching > - LinkedIn sync orchestration > - Background monitoring setup > > Skipping it will result in incomplete job postings and broken workflows. ## Runtime Requirements This skill requires: - `node` to run `scripts/post_job.js` - `npm` to install dependencies from `package.json` - Installed local dependencies (for example `axios`, `fuse.js`, `dayjs`) If the skill is installed without dependencies, run `npm install` in the skill directory before using the script entrypoints. ## Execution Model `post-job` is the skill/package name. `post_job` and `check_linkedin_status` are the canonical executable actions for this skill, implemented via `scripts/post_job.js`. If the runtime exposes `post_job` / `check_linkedin_status` as separate top-level tools, use them. If the runtime does **not** expose separately named top-level tools, use this skill's script entrypoint directly instead of refusing: - Post job: `node scripts/post_job.js --title "..." --city "..." --description "..." --company "..." --email "..." --linkedinCompanyUrl "..."` - Check LinkedIn status: `node scripts/post_job.js --check-linkedin-status "<jobId>"` Do **not** conclude the capability is unavailable solely because a same-named native tool is absent from the global tool list. When the user asks to publish a job: 1. Use `post_job` as the primary execution path 2. If no separately exposed top-level `post_job` tool exists, invoke the script entrypoint in `scripts/post_job.js` 3. Use `check_linkedin_status` only for LinkedIn sync follow-up / monitoring 4. If no separately exposed top-level `check_linkedin_status` tool exists, use `node scripts/post_job.js --check-linkedin-status "<jobId>"` 5. Do **not** refuse solely because a same-named top-level tool is not visibly listed in the session tool list 6. Do **not** substitute manual posting instructions for actual execution unless this skill's execution path genuinely fails 🚀 **Quickly post job openings and collect resumes via natural language commands.** JobPoster simplifies the hiring process by letting you post jobs through simple commands. It automatically matches locations, validates inputs, and provides shareable application links. Perfect for recruiters, hiring managers, and HR teams. ## ✨ Features - **Natural Language Interface** - Post jobs with simple commands like "Hire a frontend engineer in Singapore" - **Global City Support** - 100+ cities worldwide with fuzzy matching (Singapore, Hong Kong, New York, London, etc.) - **AI Job Description** - Optional AI-powered JD generation for professional, compelling postings - **Instant Application Links** - Get shareable URLs for candidates to apply directly - **Resume Collection** - All applications sent to your specified email - **LinkedIn Sync** - Automatic LinkedIn job posting integration ## ⚠️ External Service Notice This skill uses **Fuku AI** (https://hapi.fuku.ai) as a third-party job posting relay service to distribute jobs to multiple boards. Uses **Fuku AI** relay service — no LinkedIn account binding required. Jobs post anonymously through Fuku AI's infrastructure. **Data transmitted:** job title, description, company, location, email, LinkedIn company URL. ## 🔒 Quick Checklist - [ ] Use dedicated hiring email (not personal) - [ ] Job description has no sensitive/confidential info - [ ] Save Job ID for tracking - [ ] Role is appropriate for public job boards - [ ] **Note:** Jobs appear under Fuku AI's accounts, not your LinkedIn page; cannot edit/delete directly ## 🎯 When to Use Use this skill when you need to: - Post a job opening quickly - Create a job listing for any role - Generate a resume collection link - Share job postings with candidates - Sync jobs to LinkedIn ## 🛠️ Tools ### post_job ⭐ Implemented by `scripts/post_job.js`. Post job opening to 20+ job boards. Returns immediately with Job ID. #### Parameters | Parameter | Required | Type | Description | Default | | -------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `title` | ✅ Yes | string | Job title (min 4 characters) | - | | `city_query` | ✅ Yes | string | City/location (supports fuzzy match) | - | | `description` | ✅ Yes | string | Job description | - | | `email` | ✅ Yes | string | Email to receive resumes | - | | `company` | ❌ No | string | Company name | `"Your Company"` | | `industry` | ❌ No | string | Industry/field | `"General"` | | `linkedinCompanyUrl` | ❌ No | string | LinkedIn company page URL. Job posts under this company; defaults to Fuku AI's if not specified | `"https://www.linkedin.com/company/business-consulting-inter"` | #### Response Returns Job ID. **LinkedIn sync takes 5-60 minutes in background.** --- ### check_linkedin_status Implemented by `scripts/post_job.js`. Check LinkedIn sync status once. Returns URL if ready, or "Pending". #### Parameters | Parameter | Required | Type | Description | Example | | --------- | -------- | ------ | ------------------- | ------------ | | `jobId` | ✅ Yes | string | The job ID to check | `"abcd2345"` | --- ## 🤖 AI Assistant Workflow ### ⚠️ REQUIRED: Post Job + Spawn Monitor (Two Steps) After `post_job` returns, you **MUST** spawn a sub-agent to monitor LinkedIn sync. Do NOT skip this. **Step 1: Post Job** ``` result = await post_job({ title, city_query, description, email, ... }) ``` **Step 2: Spawn Monitor (REQUIRED)** Extract jobId from result, then spawn sub-agent when `sessions_spawn` is available: ```javascript // Extract jobId from post_job result const jobIdMatch = result.match(/Job ID:\s*`?([a-zA-Z0-9-]+)`?/); const jobId = jobIdMatch?.[1]; // Preferred: spawn sub-agent to monitor await sessions_spawn({ task: `Monitor LinkedIn sync for job ID "${jobId}". Loop: call check_linkedin_status with jobId="${jobId}", wait 2 minutes, repeat up to 30 times until LinkedIn URL is returned. Notify user when URL is available.`, mode: "run", cleanup: "delete", }); // Tell user ("✅ Job posted! Job ID: `xxx`. LinkedIn monitoring has already started. I'll notify you when sync completes (5-60 min)."); ``` **Fallback if `sessions_spawn` is unavailable:** Continue monitoring using this skill's own execution path instead of stopping or asking the user whether to continue. - Preferred fallback check command: `node scripts/post_job.js --check-linkedin-status "<jobId>"` - If the runtime truly cannot automate follow-up in background, explicitly say background monitoring is unavailable in this runtime, but do **not** pretend the workflow is complete. **⚠️ If you skip Step 2:** - User will NOT receive LinkedIn URL - Incomplete workflow ### Mandatory sub-agent rule After `post_job` succeeds, you must immediately start a monitoring sub-agent before sending the final user-facing success response. Required order: 1. Call `post_job` 2. Extract `jobId` 3. Start monitoring sub-agent 4. Only then send the success reply to the user Do **not** claim LinkedIn monitoring is in progress unless the sub-agent has actually been started successfully. Allowed user-facing response pattern: - Confirm the job was posted - Confirm that LinkedIn monitoring has already been started - Later, send the LinkedIn URL when available Forbidden: - Offering monitoring as an optional next step - Saying "I'll monitor it" before starting the sub-agent - Saying "processing in background" if no monitoring sub-agent is running - "If you want, I can check LinkedIn sync status" - "Next I can help monitor LinkedIn" - Any phrasing that makes the required follow-up sound optional If sub-agent startup fails or is unavailable, explicitly tell the user that automatic monitoring could not be started in this runtime. Failure to start the LinkedIn monitor after `post_job` is an incomplete execution of this skill. --- ## 📦 Installation ### Install via ClawHub ```bash clawhub install post-job ``` After install, ensure dependencies are present: ```bash cd skills/post-job npm install ``` ### Manual Installation ```bash # Clone or download the skill cd your-openclaw-workspace/skills # Install dependencies cd post-job npm install ``` ## 🔐 Security Notes - **Email Privacy**: Resume emails are visible in job postings - use a dedicated hiring email - **Rate Limiting**: API may have rate limits for high-volume posting ## 🐛 Troubleshooting ### Issue: Job posts but no confirmation **Cause**: Response timeout or network issue **Solution**: Check backend logs, verify API credentials, retry with `--force` ### Issue: City not recognized **Cause**: City not in location database **Solution**: 1. Check `assets/locations.json` for supported cities 2. Try alternative spelling (e.g., "New York" vs "NYC") 3. Add new city to database and republish ### Issue: Duplicate job postings **Cause**: Multiple API calls due to retry logic **Solution**: Check backend for duplicate jobs, implement request deduplication ## ❓ FAQ **Q: Do I need a LinkedIn account?** No — posts through Fuku AI relay, no binding required. **Q: Can I delete/edit a posted job?** No direct control — contact Fuku AI support with Job ID. **Q: Is this safe for confidential hiring?** No — use traditional channels for sensitive roles. **Q: What if Fuku AI goes offline?** Posting may fail or sync delayed; skill returns error. ## 🤝 Contributing Found a bug or want to add more cities? 1. Fork the skill 2. Make your changes 3. Test thoroughly 4. Submit a pull request ## 📄 License This skill is provided as-is for use with OpenClaw. ## 🆘 Support For issues or questions: - Check this SKILL.md for troubleshooting - Review error messages carefully - Contact developer email [REDACTED_SECRET] if you run into any issues --- **Happy Hiring! 🎉**

path: assets/locations.json
content: label: Singapore
value: 70000001
parentLabel: Singapore
parentValue: SI
addressCountry: SG
postalCode: 139950
addressRegion: West Region
streetAddress: 67 Ayer Rajah Crescent
addressLocality: Buona Vista

label: Dubai
value: 71000043
parentLabel: United Arab Emirates
parentValue: UE
addressCountry: AE
postalCode: 00000
addressRegion: Dubai Emirate
streetAddress: Sheikh Zayed Road, Building No 22
addressLocality: Dubai

label: Kuala Lumpur
value: 74000002
parentLabel: Malaysia
parentValue: MY
addressCountry: MY
postalCode: 50450
addressRegion: Federal Territory of Kuala Lumpur
streetAddress: 15 Jalan Ampang
addressLocality: Kuala Lumpur City Centre

label: Johor Bahru
value: 74000005
parentLabel: Malaysia
parentValue: MY
addressCountry: MY
postalCode: 80100
addressRegion: Johor
streetAddress: Jalan Wong Ah Fook 1
addressLocality: Johor Bahru

label: George Town
value: 74000024
parentLabel: Malaysia
parentValue: MY
addressCountry: MY
postalCode: 10200
addressRegion: Penang
streetAddress: No. 116 & 118 Lebuh Acheh
addressLocality: George Town

label: Klang
value: 74000003
parentLabel: Malaysia
parentValue: MY
addressCountry: MY
postalCode: 41200
addressRegion: Selangor
streetAddress: 1, Persiaran Batu Nilam 1, Bandar Bukit Tinggi 2
addressLocality: Klang

label: Shah Alam
value: 74000009
parentLabel: Malaysia
parentValue: MY
addressCountry: MY
postalCode: 40000
addressRegion: Selangor
streetAddress: 2, Jalan Perbandaran, Seksyen 14
addressLocality: Shah Alam

label: Hong Kong
value: 79000002
parentLabel: Hong Kong
parentValue: HK
addressCountry: HK
postalCode: 0000
addressRegion: Hong Kong Island
streetAddress: Room 1205, 12/F, Central Plaza, 18 Harbour Road
addressLocality: Wan Chai

label: Bangkok
value: 61000905
parentLabel: Thailand
parentValue: TH
addressCountry: TH
postalCode: 10110
addressRegion: Bangkok Metropolitan Area
streetAddress: 10 Soi Sukhumvit 63 (Ekkamai)
addressLocality: Khlong Toei Nuea

label: Myanmar
value: 330000401
parentLabel: Myanmar
parentValue: MM
addressCountry: MM
postalCode: 11141
addressRegion: Yangon Region
streetAddress: 12 Bogyoke Aung San Road
addressLocality: Yangon

label: Hanoi
value: 160000003
parentLabel: Vietnam
parentValue: VN
addressCountry: VN
postalCode: 100000
addressRegion: Hà Nội
streetAddress: 123 Đường Hoàng Quốc Việt
addressLocality: Cầu Giấy District

label: Ho Chi Minh City
value: 160000002
parentLabel: Vietnam
parentValue: VN
addressCountry: VN
postalCode: 700000
addressRegion: Ho Chi Minh City
streetAddress: 91 Nguyen Cu Trinh St
addressLocality: Nguyen Cu Trinh Ward, District 1

label: Manila
value: 51000000
parentLabel: Philippines
parentValue: PH
addressCountry: PH
postalCode: 1000
addressRegion: National Capital Region
streetAddress: 691 A. Arnaiz Ave
addressLocality: Makati City

label: Jakarta
value: 76000207
parentLabel: Indonesia
parentValue: ID
addressCountry: ID
postalCode: 10310
addressRegion: DKI Jakarta
streetAddress: Jl. Jend. Sudirman No. 10
addressLocality: Jakarta Pusat

label: Tokyo
value: 36094389
parentLabel: Japan
parentValue: JP
addressCountry: JP
postalCode: 100-0001
addressRegion: Tokyo Metropolis
streetAddress: 1-1 Chiyoda
addressLocality: Chiyoda-ku

label: Osaka
value: 36049159
parentLabel: Japan
parentValue: JP
addressCountry: JP
postalCode: 530-0001
addressRegion: Osaka Prefecture
streetAddress: 3-1 Umeda 1-chōme
addressLocality: Kita-ku Osaka

label: Seoul
value: 320006457
parentLabel: South Korea
parentValue: KR
addressCountry: KR
postalCode: 04524
addressRegion: Seoul Special City
streetAddress: 50 Myeongdong 8-gil
addressLocality: Jung-gu

label: Busan
value: 320006397
parentLabel: South Korea
parentValue: KR
addressCountry: KR
postalCode: 47285
addressRegion: Busan Metropolitan City
streetAddress: 772 Gaya-daero
addressLocality: Busanjin-gu

label: Beijing
value: 68000002
parentLabel: China
parentValue: CN
addressCountry: CN
postalCode: 100000
addressRegion: Beijing Municipality
streetAddress: No. 1 Jianguomenwai Avenue
addressLocality: Dongcheng District

label: Shanghai
value: 68000169
parentLabel: China
parentValue: CN
addressCountry: CN
postalCode: 200000
addressRegion: Shanghai Municipality
streetAddress: No. 168 Pudong New Area Lujiazui
addressLocality: Pudong New Area

label: Guangzhou
value: 68000466
parentLabel: China
parentValue: CN
addressCountry: CN
postalCode: 510000
addressRegion: Guangdong Province
streetAddress: No. 1 Zhongshan Road
addressLocality: Guangzhou

label: Shenzhen
value: 68000468
parentLabel: China
parentValue: CN
addressCountry: CN
postalCode: 518000
addressRegion: Guangdong Province
streetAddress: No. 1 Shennan Boulevard
addressLocality: Futian District

label: Taipei
value: 170000325
parentLabel: Taiwan
parentValue: TW
addressCountry: TW
postalCode: 100
addressRegion: Taipei City
streetAddress: No. 1, Zhongxiao W. Rd.
addressLocality: Zhongzheng District

label: Sydney
value: 10000195
parentLabel: Australia
parentValue: AU
addressCountry: AU
postalCode: 2000
addressRegion: NSW
streetAddress: 1 Martin Place
addressLocality: Sydney CBD

label: Melbourne
value: 10006677
parentLabel: Australia
parentValue: AU
addressCountry: AU
postalCode: 3000
addressRegion: VIC
streetAddress: 1 Collins Street
addressLocality: Melbourne CBD

label: Auckland
value: 49001765
parentLabel: New Zealand
parentValue: NZ
addressCountry: NZ
postalCode: 1010
addressRegion: Auckland Region
streetAddress: 1 Queen Street
addressLocality: Auckland CBD

label: New Delhi
value: 32015163
parentLabel: India
parentValue: IN
addressCountry: IN
postalCode: 110001
addressRegion: Delhi
streetAddress: Rashtrapati Bhavan, Raisina Hill
addressLocality: New Delhi

label: Mumbai
value: 32015169
parentLabel: India
parentValue: IN
addressCountry: IN
postalCode: 400001
addressRegion: Maharashtra
streetAddress: Taj Mahal Palace, Apollo Bunder
addressLocality: Colaba

label: Bangalore
value: 32015168
parentLabel: India
parentValue: IN
addressCountry: IN
postalCode: 560001
addressRegion: Karnataka
streetAddress: No. 1 MG Road
addressLocality: Bengaluru Central

label: Pune
value: 32015170
parentLabel: India
parentValue: IN
addressCountry: IN
postalCode: 411001
addressRegion: Maharashtra
streetAddress: 1 MG Road
addressLocality: Pune City

label: Chennai
value: 32015167
parentLabel: India
parentValue: IN
addressCountry: IN
postalCode: 600001
addressRegion: Tamil Nadu
streetAddress: Rashtrapathi Bhavan Road
addressLocality: Fort St George, Chennai

label: Riyadh
value: 180000067
parentLabel: Saudi Arabia
parentValue: SA
addressCountry: SA
postalCode: 11564
addressRegion: Riyadh Province
streetAddress: King Fahd Road
addressLocality: Olaya District

label: Jeddah
value: 180000053
parentLabel: Saudi Arabia
parentValue: SA
addressCountry: SA
postalCode: 22233
addressRegion: Makkah Province
streetAddress: King Road
addressLocality: Al-Olaya District

label: Abu Dhabi
value: 71000002
parentLabel: United Arab Emirates
parentValue: UE
addressCountry: AE
postalCode: 00000
addressRegion: Abu Dhabi Emirate
streetAddress: Sheikh Zayed Road
addressLocality: Abu Dhabi

label: Doha
value: 72000021
parentLabel: Qatar
parentValue: QA
addressCountry: QA
postalCode: 00000
addressRegion: Doha
streetAddress: West Bay Square, Al Qatar Rd
addressLocality: Doha City

label: Cairo
value: 78000049
parentLabel: Egypt
parentValue: EG
addressCountry: EG
postalCode: 11511
addressRegion: Cairo Governorate
streetAddress: 1 Abdel Khalek Tharwat St, Ataba Square
addressLocality: Cairo

label: Istanbul
value: 62051381
parentLabel: Turkey
parentValue: TR
addressCountry: TR
postalCode: 34000
addressRegion: Istanbul Province
streetAddress: İstiklal Caddesi No: 100
addressLocality: Beyoğlu

label: New York
value: 5027742
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 10001
addressRegion: NY
streetAddress: 401 7th Ave
addressLocality: Manhattan

label: San Francisco
value: 5004604
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 94105
addressRegion: CA
streetAddress: 595 Market St
addressLocality: San Francisco

label: Los Angeles
value: 5002945
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 90001
addressRegion: CA
streetAddress: 100 W 1st Street
addressLocality: Los Angeles

label: San Jose
value: 5004711
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 95113
addressRegion: CA
streetAddress: 200 E Santa Clara St
addressLocality: San Jose

label: San Diego
value: 5004327
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 92101
addressRegion: CA
streetAddress: 600 Broadway
addressLocality: San Diego

label: Seattle
value: 5040918
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 98101
addressRegion: WA
streetAddress: 500 Yale Ave N
addressLocality: Seattle

label: Denver
value: 5005614
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 80202
addressRegion: CO
streetAddress: Denver Pavilions, 700 16th St
addressLocality: Denver

label: Chicago
value: 5011305
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 60601
addressRegion: IL
streetAddress: 233 S Wacker Dr
addressLocality: Chicago

label: Houston
value: 5037121
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 77002
addressRegion: TX
streetAddress: 1400 Lubbock Street
addressLocality: Houston

label: Austin
value: 5038603
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 78701
addressRegion: TX
streetAddress: 500 W 2nd Street
addressLocality: Austin

label: San Antonio
value: 5043734
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 78205
addressRegion: TX
streetAddress: 615 E Houston St
addressLocality: San Antonio

label: Dallas
value: 5036565
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 75201
addressRegion: TX
streetAddress: 1500 Main St
addressLocality: Dallas

label: Phoenix
value: 5002020
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 85003
addressRegion: AZ
streetAddress: 220 N Central Ave
addressLocality: Phoenix

label: Philadelphia
value: 5033484
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 19103
addressRegion: PA
streetAddress: 1900 Market St
addressLocality: Philadelphia

label: District of Columbia
value: 5043669
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 20001
addressRegion: DC
streetAddress: 1600 Pennsylvania Ave NW
addressLocality: Washington

label: Boston
value: 5016967
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 02108
addressRegion: MA
streetAddress: 1 Boston Common
addressLocality: Boston

label: Las Vegas
value: 5026496
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 89101
addressRegion: NV
streetAddress: 3950 S Las Vegas Blvd
addressLocality: Las Vegas

label: Miami
value: 5007077
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 33101
addressRegion: FL
streetAddress: 100 NE 2nd Ave
addressLocality: Miami

label: Detroit
value: 5019488
parentLabel: United States
parentValue: US
addressCountry: US
postalCode: 48201
addressRegion: MI
streetAddress: One Detroit Center, 500 Woodward Ave
addressLocality: Detroit

label: Toronto
value: 15003137
parentLabel: Canada
parentValue: CA
addressCountry: CA
postalCode: M5J 2N8
addressRegion: ON
streetAddress: 100 King Street West
addressLocality: Toronto

label: Vancouver
value: 15003205
parentLabel: Canada
parentValue: CA
addressCountry: CA
postalCode: V6B 0A8
addressRegion: BC
streetAddress: 800 West Pender Street
addressLocality: Vancouver

label: Montreal
value: 15001867
parentLabel: Canada
parentValue: CA
addressCountry: CA
postalCode: H3A 0A2
addressRegion: QC
streetAddress: 100 Saint-Jacques Street
addressLocality: Downtown Montreal

label: London
value: 1024195
parentLabel: United Kingdom
parentValue: GB
addressCountry: GB
postalCode: WC2N 5HX
addressRegion: London
streetAddress: 10 Great Queen Street
addressLocality: Covent Garden

label: Berlin
value: 3001350
parentLabel: Germany
parentValue: DE
addressCountry: DE
postalCode: 10117
addressRegion: Berlin
streetAddress: Unter den Linden 77
addressLocality: Mitte

label: Munich
value: 3016382
parentLabel: Germany
parentValue: DE
addressCountry: DE
postalCode: 80331
addressRegion: Bavaria
streetAddress: Marienplatz 1
addressLocality: Mitte

label: Madrid
value: 2041600
parentLabel: Spain
parentValue: ES
addressCountry: ES
postalCode: 28001
addressRegion: Community of Madrid
streetAddress: Plaza de la Puerta del Sol 1
addressLocality: Madrid

label: Barcelona
value: 2015583
parentLabel: Spain
parentValue: ES
addressCountry: ES
postalCode: 08001
addressRegion: Catalonia
streetAddress: Plaça de Catalunya 1
addressLocality: Barcelona

label: Rome
value: 34020900
parentLabel: Italy
parentValue: IT
addressCountry: IT
postalCode: 00118
addressRegion: Lazio
streetAddress: Via del Corso 1
addressLocality: Roma

label: Milan
value: 34000439
parentLabel: Italy
parentValue: IT
addressCountry: IT
postalCode: 20121
addressRegion: Lombardy
streetAddress: Via Monte Napoleone 1
addressLocality: Milano

label: Paris
value: 4014735
parentLabel: France
parentValue: FR
addressCountry: FR
postalCode: 75001
addressRegion: Île-de-France
streetAddress: 10 Place de la Concorde
addressLocality: Paris

label: Luxembourg
value: 39000000
parentLabel: Luxembourg
parentValue: LU
addressCountry: LU
postalCode: L-1011
addressRegion: Luxembourg
streetAddress: 10 Rue du Nord
addressLocality: Ville de Luxembourg

label: Geneva
value: 16001544
parentLabel: Switzerland
parentValue: CH
addressCountry: CH
postalCode: 1201
addressRegion: Canton of Geneva
streetAddress: Rue du Rhône 75
addressLocality: Genève

label: Zürich
value: 16004811
parentLabel: Switzerland
parentValue: CH
addressCountry: CH
postalCode: 8001
addressRegion: Zurich
streetAddress: Bahnhofstrasse 1
addressLocality: Zürich

label: Amsterdam
value: 67000990
parentLabel: Netherlands
parentValue: NL
addressCountry: NL
postalCode: 1012 AB
addressRegion: North Holland
streetAddress: Dam Square 1
addressLocality: Amsterdam

label: Dublin
value: 47002909
parentLabel: Ireland
parentValue: IE
addressCountry: IE
postalCode: D02 X285
addressRegion: Dublin
streetAddress: 32 Grafton Street
addressLocality: Dublin 2

label: Vienna
value: 9046400
parentLabel: Austria
parentValue: AT
addressCountry: AT
postalCode: 1010
addressRegion: Vienna
streetAddress: Stephansplatz 1
addressLocality: Innere Stadt

label: Monaco
value: 40000005
parentLabel: Monaco
parentValue: MC
addressCountry: MC
postalCode: 98000
addressRegion: Monaco
streetAddress: 10 Rue du Nord
addressLocality: Monaco-Ville

label: Brussels
value: 12003806
parentLabel: Belgium
parentValue: BE
addressCountry: BE
postalCode: 1000
addressRegion: Brussels-Capital Region
streetAddress: Rue du Nord 26
addressLocality: Brussels

label: Stockholm
value: 120000095
parentLabel: Sweden
parentValue: SE
addressCountry: SE
postalCode: 111 64
addressRegion: Stockholm County
streetAddress: Drottninggatan 77
addressLocality: Stockholm

label: Copenhagen
value: 75000403
parentLabel: Denmark
parentValue: DM
addressCountry: DK
postalCode: 1050
addressRegion: Capital Region of Denmark
streetAddress: Vesterbrogade 21
addressLocality: Copenhagen V

label: Lisbon
value: 54000596
parentLabel: Portugal
parentValue: PT
addressCountry: PT
postalCode: 1000-001
addressRegion: Lisbon
streetAddress: Rua da Liberdade 123
addressLocality: Lisbon

label: Sao Paulo
value: 14005527
parentLabel: Brazil
parentValue: BR
addressCountry: BR
postalCode: 01000-000
addressRegion: São Paulo
streetAddress: Av. Paulista 1578
addressLocality: Bela Vista

label: Rio de Janeiro
value: 14003221
parentLabel: Brazil
parentValue: BR
addressCountry: BR
postalCode: 20000-000
addressRegion: RJ
streetAddress: Av. Presidente Vargas 1
addressLocality: Centro

label: Mexico City
value: 46075206
parentLabel: Mexico
parentValue: MX
addressCountry: MX
postalCode: 06000
addressRegion: Ciudad de México
streetAddress: Paseo de la Reforma #1
addressLocality: Cuauhtémoc

label: Argentina
value: 7020260
parentLabel: Argentina
parentValue: AR
addressCountry: AR
postalCode: C1001
addressRegion: Buenos Aires City
streetAddress: Avenida Florida 123
addressLocality: Buenos Aires

label: Johannesburg
value: 66001666
parentLabel: South Africa
parentValue: ZA
addressCountry: ZA
postalCode: 2001
addressRegion: Gauteng
streetAddress: Ground Floor Sunnyside Office Park, 32 Rissik Street
addressLocality: Johannesburg Central Business District

label: Cape Town
value: 66003457
parentLabel: South Africa
parentValue: ZA
addressCountry: ZA
postalCode: 8001
addressRegion: Western Cape
streetAddress: 76 Lower Long Street
addressLocality: Cape Town CBD

label: Lagos
value: 100000098
parentLabel: Nigeria
parentValue: NG
addressCountry: NG
postalCode: 100001
addressRegion: Lagos State
streetAddress: 32/34 Broad Street
addressLocality: Lagos Island

path: package.json
content: {
  "name": "post-job",
  "version": "2.1.3",
  "type": "module",
  "description": "Post job openings and generate resume collection links",
  "main": "scripts/post_job.js",
  "scripts": {
    "post": "node scripts/post_job.js"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "dayjs": "^1.11.19",
    "dotenv": "^17.3.1",
    "fuse.js": "^7.0.0"
  }
}


path: pnpm-lock.yaml
content: lockfileVersion: '9.0'

settings:
  autoInstallPeers: true
  excludeLinksFromLockfile: false

importers:

  .:
    dependencies:
      axios:
        specifier: ^1.6.0
        version: 1.13.5
      dayjs:
        specifier: ^1.11.19
        version: 1.11.19
      fuse.js:
        specifier: ^7.0.0
        version: 7.1.0

packages:

  asynckit@0.4.0:
    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}

  axios@1.13.5:
    resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}

  call-bind-apply-helpers@1.0.2:
    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
    engines: {node: '>= 0.4'}

  combined-stream@1.0.8:
    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
    engines: {node: '>= 0.8'}

  dayjs@1.11.19:
    resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}

  delayed-stream@1.0.0:
    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
    engines: {node: '>=0.4.0'}

  dunder-proto@1.0.1:
    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
    engines: {node: '>= 0.4'}

  es-define-property@1.0.1:
    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
    engines: {node: '>= 0.4'}

  es-errors@1.3.0:
    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
    engines: {node: '>= 0.4'}

  es-object-atoms@1.1.1:
    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
    engines: {node: '>= 0.4'}

  es-set-tostringtag@2.1.0:
    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
    engines: {node: '>= 0.4'}

  follow-redirects@1.15.11:
    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
    engines: {node: '>=4.0'}
    peerDependencies:
      debug: '*'
    peerDependenciesMeta:
      debug:
        optional: true

  form-data@4.0.5:
    resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
    engines: {node: '>= 6'}

  function-bind@1.1.2:
    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}

  fuse.js@7.1.0:
    resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
    engines: {node: '>=10'}

  get-intrinsic@1.3.0:
    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
    engines: {node: '>= 0.4'}

  get-proto@1.0.1:
    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
    engines: {node: '>= 0.4'}

  gopd@1.2.0:
    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
    engines: {node: '>= 0.4'}

  has-symbols@1.1.0:
    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
    engines: {node: '>= 0.4'}

  has-tostringtag@1.0.2:
    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
    engines: {node: '>= 0.4'}

  hasown@2.0.2:
    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
    engines: {node: '>= 0.4'}

  math-intrinsics@1.1.0:
    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
    engines: {node: '>= 0.4'}

  mime-db@1.52.0:
    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
    engines: {node: '>= 0.6'}

  mime-types@2.1.35:
    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
    engines: {node: '>= 0.6'}

  proxy-from-env@1.1.0:
    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}

snapshots:

  asynckit@0.4.0: {}

  axios@1.13.5:
    dependencies:
      follow-redirects: 1.15.11
      form-data: 4.0.5
      proxy-from-env: 1.1.0
    transitivePeerDependencies:
      - debug

  call-bind-apply-helpers@1.0.2:
    dependencies:
      es-errors: 1.3.0
      function-bind: 1.1.2

  combined-stream@1.0.8:
    dependencies:
      delayed-stream: 1.0.0

  dayjs@1.11.19: {}

  delayed-stream@1.0.0: {}

  dunder-proto@1.0.1:
    dependencies:
      call-bind-apply-helpers: 1.0.2
      es-errors: 1.3.0
      gopd: 1.2.0

  es-define-property@1.0.1: {}

  es-errors@1.3.0: {}

  es-object-atoms@1.1.1:
    dependencies:
      es-errors: 1.3.0

  es-set-tostringtag@2.1.0:
    dependencies:
      es-errors: 1.3.0
      get-intrinsic: 1.3.0
      has-tostringtag: 1.0.2
      hasown: 2.0.2

  follow-redirects@1.15.11: {}

  form-data@4.0.5:
    dependencies:
      asynckit: 0.4.0
      combined-stream: 1.0.8
      es-set-tostringtag: 2.1.0
      hasown: 2.0.2
      mime-types: 2.1.35

  function-bind@1.1.2: {}

  fuse.js@7.1.0: {}

  get-intrinsic@1.3.0:
    dependencies:
      call-bind-apply-helpers: 1.0.2
      es-define-property: 1.0.1
      es-errors: 1.3.0
      es-object-atoms: 1.1.1
      function-bind: 1.1.2
      get-proto: 1.0.1
      gopd: 1.2.0
      has-symbols: 1.1.0
      hasown: 2.0.2
      math-intrinsics: 1.1.0

  get-proto@1.0.1:
    dependencies:
      dunder-proto: 1.0.1
      es-object-atoms: 1.1.1

  gopd@1.2.0: {}

  has-symbols@1.1.0: {}

  has-tostringtag@1.0.2:
    dependencies:
      has-symbols: 1.1.0

  hasown@2.0.2:
    dependencies:
      function-bind: 1.1.2

  math-intrinsics@1.1.0: {}

  mime-db@1.52.0: {}

  mime-types@2.1.35:
    dependencies:
      mime-db: 1.52.0

  proxy-from-env@1.1.0: {}


path: scripts/loadLocations.js
content: /**
 * Load locations database for city fuzzy matching
 * Separated from post_job.js to avoid static analysis false positives
 */
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import Fuse from "fuse.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const locationsPath = path.join(__dirname, "..", "assets", "locations.json");

let locations = [];
let fuse = null;

/**
 * Initialize locations database
 * @returns {Object} Fuse instance for fuzzy search
 */
function initLocations() {
  if (!fuse) {
    if (fs.existsSync(locationsPath)) {
      locations = JSON.parse(fs.readFileSync(locationsPath, "utf-8"));
    }
    fuse = new Fuse(locations, {
      keys: ["label", "parentLabel"],
      threshold: 0.3,
    });
  }
  return fuse;
}

/**
 * Get the Fuse instance for location search
 * @returns {Object} Fuse instance
 */
export function getLocationsFuse() {
  return initLocations();
}

export default { getLocationsFuse };

path: scripts/monitor_linkedin.js
content: #!/usr/bin/env node
/**
 * Monitor LinkedIn sync status and notify when complete.
 * Polls every 2 minutes until LinkedIn URL is available.
 *
 * Usage:
 *   node monitor_linkedin.js --jobId "xxx" --channel "webchat"
 */

import axios from "axios";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Fuku AI API endpoints (third-party job posting relay service)
const API_URL_LK_CHECK = "https://hapi.fuku.ai/hr/rc/anon/job/status/linkedin";

function sanitizeChannel(channel) {
  // Only allow alphanumeric characters and hyphens/underscores to prevent prompt injection
  const sanitized = (channel || "").replace(/[^a-zA-Z0-9_-]/g, "");
  return sanitized || "webchat";
}

function parseArgs(args) {
  const result = {
    jobId: "",
    channel: "webchat",
    intervalMs: 120000, // 2 minutes default
    maxAttempts: 15, // ~30 minutes total
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    const next = args[i + 1];

    switch (arg) {
      case "--jobId":
        result.jobId = next;
        i++;
        break;
      case "--channel":
        result.channel = sanitizeChannel(next);
        i++;
        break;
      case "--interval":
        result.intervalMs = parseInt(next, 10);
        i++;
        break;
      case "--maxAttempts":
        result.maxAttempts = parseInt(next, 10);
        i++;
        break;
    }
  }

  return result;
}

function validateJobId(jobId) {
  if (!jobId || typeof jobId !== "string") {
    return false;
  }
  // Only allow alphanumeric characters and hyphens (prevent shell injection)
  return /^[a-zA-Z0-9-]+$/.test(jobId);
}

async function checkLinkedInStatus(jobId) {
  try {
    // Fuku AI client identifier (embedded for free tier access)
    const NUMBER = [REDACTED_SECRET];
    const response = await axios.post(
      API_URL_LK_CHECK,
      { jobId: jobId },
      {
        params: { uid: "1873977344885133312" },
        headers: { "X-NUMBER": NUMBER },
      },
    );

    const jobData = response.data;

    if (jobData.code !== 0) {
      return { success: false, status: jobData.desc, url: null };
    }

    if (jobData.data && jobData.data.linkedinUrl) {
      return { success: true, status: "live", url: jobData.data.linkedinUrl };
    }

    return {
      success: false,
      status: jobData.data?.status || "Pending",
      url: null,
    };
  } catch (error) {
    return { success: false, status: `Error: ${error.message}`, url: null };
  }
}

async function notifyUser(message, channel) {
  // For now, output the message - the parent session will handle delivery
  console.log(`[NOTIFY:${channel}] ${message}`);
}

async function main() {
  const args = parseArgs(process.argv.slice(2));

  if (!args.jobId) {
    console.error("❌ Error: --jobId is required");
    process.exit(1);
  }

  if (!validateJobId(args.jobId)) {
    console.error(
      "❌ Error: Invalid jobId format (only alphanumeric and hyphens allowed)",
    );
    process.exit(1);
  }

  console.log(`🔍 Starting LinkedIn sync monitor for Job ID: ${args.jobId}`);
  console.log(`   Channel: ${args.channel}`);
  console.log(
    `   Interval: ${args.intervalMs / 1000}s | Max attempts: ${args.maxAttempts}`,
  );
  console.log();

  for (let attempt = 1; attempt <= args.maxAttempts; attempt++) {
    const result = await checkLinkedInStatus(args.jobId);

    if (result.success && result.url) {
      const message = `🎉 **LinkedIn Sync Complete!**\n\nJob ID: \`${args.jobId}\`\n\nPosition URL: ${result.url}`;
      await notifyUser(message, args.channel);
      console.log(`✅ Success after ${attempt} attempts!`);
      console.log(`URL: ${result.url}`);
      process.exit(0);
    }

    console.log(
      `Attempt ${attempt}/${args.maxAttempts}: ⏳ Status: ${result.status}`,
    );

    if (attempt < args.maxAttempts) {
      console.log(
        `   Waiting ${args.intervalMs / 1000} seconds before next check...\n`,
      );
      await new Promise((resolve) => setTimeout(resolve, args.intervalMs));
    }
  }

  const timeoutMsg = `⏰ **LinkedIn Sync Timeout**\n\nAfter ${args.maxAttempts} attempts, the sync status is still "${result.status}".\nJob ID: \`${args.jobId}\`\n\nYou can check manually or try again later.`;
  await notifyUser(timeoutMsg, args.channel);
  console.log(`⏰ Timeout after ${args.maxAttempts} attempts.`);
  process.exit(0);
}

main().catch((error) => {
  console.error("❌ Fatal error:", error.message);
  process.exit(1);
});


path: scripts/post_job.js
content: #!/usr/bin/env node
/**
 * Post job openings via Fuku AI API and generate resume collection links.
 * Uses AI model to generate professional job descriptions.
 *
 * Usage:
 *   node post_job.js --title "Senior Frontend Engineer" --city "Singapore" --level "senior"
 */

import axios from "axios";
import dayjs from "dayjs";
import { getLocationsFuse } from "./loadLocations.js";

// ============================================================================
// ⚠️ INTERNAL API PROTECTION
// These functions are for internal use ONLY. Always use post_job() as entry.
// ============================================================================
const INTERNAL_CALL_TOKEN = Symbol("internal-call-token");

function validateInternalCall(caller, fnName) {
  if (caller !== INTERNAL_CALL_TOKEN) {
    throw new Error(
      `❌ ${fnName}() is an internal function. Use post_job() to post jobs.`,
    );
  }
}
// ============================================================================

// Get Fuse instance for location fuzzy search (loaded from separate module)
const fuse = getLocationsFuse();
/**
 * Parse command line arguments
 */
function parseArgs(args) {
  const result = {
    title: "",
    city: "",
    description: "",
    company: "",
    email: "",
    linkedinCompanyUrl:
      "https://www.linkedin.com/company/business-consulting-inter",
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    const next = args[i + 1];

    switch (arg) {
      case "--title":
        result.title = next;
        i++;
        break;
      case "--city":
        result.city = next;
        i++;
        break;
      case "--description":
        result.description = next;
        i++;
        break;
      case "--company":
        result.company = next;
        i++;
        break;
      case "--email":
        result.email = next;
        i++;
        break;
      case "--linkedinCompanyUrl":
        result.linkedinCompanyUrl = next;
        i++;
        break;
    }
  }

  return result;
}

// API Configuration
// Fuku AI client identifier (embedded for free tier access)
const NUMBER = [REDACTED_SECRET];

const API_URL_GEN = "https://hapi.fuku.ai/hr/rc/anon/job/upload";
const API_URL = "https://hapi.fuku.ai/hr/rc/anon/job/create";
const API_URL_LK = "https://hapi.fuku.ai/hr/rc/anon/job/sync/linkedin";
const API_URL_LK_CHECK = "https://hapi.fuku.ai/hr/rc/anon/job/status/linkedin";

function validateCredentials() {}

/**
 * Check the status of a LinkedIn job posting
 *
 * @param {Object} args - Arguments
 * @param {string} args.jobId - The ID of the job to check
 * @returns {Promise<string>} Status message or LinkedIn URL
 */
export async function check_linkedin_status(args) {
  const { jobId } = args;
  validateCredentials();
  try {
    const response = await axios.post(
      API_URL_LK_CHECK,
      { jobId: jobId },
      {
        params: {
          uid: "1873977344885133312",
        },
        headers: {
          "X-NUMBER": NUMBER,
        },
      },
    );

    const jobData = response.data;

    if (jobData.code !== 0) {
      return `❌ **Failed to get LinkedIn job state:** ${jobData.desc}`;
    }

    if (jobData.data && jobData.data.linkedinUrl) {
      return `✅ **LinkedIn Job is Live!**\n\nURL: ${jobData.data.linkedinUrl}`;
    } else {
      return `⏳ **LinkedIn Sync is still in progress.**\n\nStatus: ${jobData.data?.status || "Pending"}\nPlease check again in 5-10 minutes.`;
    }
  } catch (error) {
    return `❌ **Error checking LinkedIn status:** ${error.message}`;
  }
}

/**
 * Auto-check LinkedIn status with polling until URL is available
 *
 * @param {Object} args - Arguments
 * @param {string} args.jobId - The ID of the job to check
 * @param {number} [args.intervalMs=60000] - Polling interval in ms (default: 1 minute)
 * @param {number} [args.maxAttempts=20] - Maximum number of attempts (default: 20)
 * @returns {Promise<string>} Status message or LinkedIn URL
 */
async function getLinkedinState(jobId) {
  return check_linkedin_status({ jobId });
}

async function postToLinkd(data, linkedinCompanyUrl, token) {
  validateInternalCall(token, "postToLinkd");
  validateCredentials();
  let extra = null;
  try {
    extra = JSON.parse(data.extra ?? "");
  } catch (error) {}
  const job = {
    title: data.title,
    reference: `fuku-${data.id}`,
    description: data.description,
    jobId: data.id,
    linkedinCompanyUrl:
      linkedinCompanyUrl ||
      "https://www.linkedin.com/company/business-consulting-inter",
    location: extra.location,
    sublocation: extra.sublocation,
    cityname: extra.cityname,
    salarymin: 1,
    salarymax: 1,
    app_url: `https://app.fuku.ai/career/apply?id=${data.id}`,
    salaryper: "month",
    currency: "USD",
    jobtype: "2",
    job_time: "2",
    startdate: dayjs().format("YYYY-MM-DD"),
  };
  // console.log("Linkedin Job Post:", job);

  const res = await axios.post(API_URL_LK, job, {
    params: {
      uid: "1873977344885133312",
    },
    headers: {
      "X-NUMBER": NUMBER,
    },
  });
  // console.log("Linkedin Job Post Response:", res.data);
}

/**
 * Sanitize job description before sending to external AI service
 * Removes potential prompt injection patterns
 */
function sanitizeDescription(desc) {
  if (!desc || typeof desc !== "string") return "";

  // Remove patterns commonly used for prompt injection
  let sanitized = desc
    .replace(/```[\s\S]*?```/g, "") // Remove code blocks
    .replace(/<\|.*?\|>/g, "") // Remove special markers like 

    .replace(
      /(?:^|\n)\s*(ignore|forget|override|system|instruction|new instruction)[\s\S]{0,200}/gi,
      "",
    ) // Remove injection attempts
    .replace(
      /(?:^|\n)\s*[-*]\s*(ignore|forget|override|system|instruction)[\s\S]{0,200}/gi,
      "",
    ) // Remove bullet-point injections
    .trim();

  // Limit length to prevent buffer-based attacks
  return sanitized.slice(0, 10000);
}

async function genJD(description, token) {
  validateInternalCall(token, "genJD");
  validateCredentials();

  // Sanitize description before sending to external AI service
  const sanitizedDescription = sanitizeDescription(description);
  if (!sanitizedDescription) {
    return "❌ **Invalid job description:** Description is empty or contains invalid content.";
  }

  const body = {
    content: sanitizedDescription,
  };
  try {
    const response = await axios.post(API_URL_GEN, body, {
      params: {
        uid: "1873977344885133312",
      },
      headers: {
        "X-NUMBER": NUMBER,
      },
    });

    const jobData = response.data;
    // console.log("Generated Job Data:", jobData);

    if (jobData.code !== 0) {
      return `❌ **Failed to generate job description:** ${jobData.desc}`;
    }

    return jobData.data.description;
  } catch (error) {
    return `❌ **Error generating job description:** ${error.message}`;
  }
}

/**
 * Post a job opening via Fuku AI API
 *
 * @param {Object} args - Job parameters
 * @param {string} args.title - Job title
 * @param {string} args.city_query - Location query
 * @param {string} [args.description] - Job description (auto-generated if not provided)
 * @param {string} [args.company] - Company name
 * @returns {Promise<string>} Result message
 */
export async function post_job(args) {
  try {
    validateCredentials();
  } catch (error) {
    return `❌ ${error.message}`;
  }
  const { title, city_query, description, company, email, linkedinCompanyUrl } =
    args;

  // Validate required fields
  if (!email) {
    return `❌ **Email is required.** Please provide an email address to receive resumes.\n\nExample: --email "[REDACTED_SECRET]"`;
  }

  // Fuzzy search for location
  const results = fuse.search(city_query);
  if (results.length === 0) {
    return `❌ Sorry, I couldn't find the location: "${city_query}".`;
  }

  const matched = results[0].item;

  // Build extra data
  const extra = JSON.stringify({
    location: matched.parentValue,
    sublocation: matched.value,
    cityname: matched.label,
  });

  const fullDescription = await genJD(description, INTERNAL_CALL_TOKEN);
  // console.log("Generated Description:", fullDescription);
  if (fullDescription.startsWith("❌")) {
    return fullDescription;
  }

  // Double-check: sanitize the AI-generated description before sending to job posting API
  const finalDescription = sanitizeDescription(fullDescription);
  if (!finalDescription) {
    return "❌ **Invalid job description:** Generated content was filtered as unsafe.";
  }

  // Build request body
  const body = {
    title,
    description: finalDescription,
    location: matched.parentLabel,
    company: company || "FUKU AI",
    companySearchKeyword: "",
    extra,
    isAiInterview: 1,
    orgId: "1",
    email: email,
  };

  try {
    const response = await axios.post(API_URL, body, {
      params: {
        uid: "1873977344885133312",
      },
      headers: {
        "X-NUMBER": NUMBER,
      },
    });

    const jobData = response.data;

    if (jobData.code !== 0) {
      return `❌ **Failed to post job:** ${jobData.desc}`;
    }
    await postToLinkd(jobData.data, linkedinCompanyUrl, INTERNAL_CALL_TOKEN);
    const jobId = jobData.data.id;

    // Note: LinkedIn sync runs in background, returns immediately
    // User can check status later with check_linkedin_status

    return (
      `✅ **Job Posted Successfully!**\n\n` +
      `**Position:** ${title}\n` +
      `**Location:** ${matched.label}\n` +
      `**Job ID:** \`${jobId}\`\n` +
      `**The resume will be sent to:** ${email}\n\n` +
      `--- \n` +
      `**LinkedIn Sync:** ⏳ Processing in background (5-30 min). Monitoring should continue automatically until the LinkedIn URL is available.`
    );
  } catch (error) {
    const errorMsg = error.response?.data?.message || error.message;
    return `❌ **Failed to post job:** ${errorMsg}`;
  }
}

/**
 * Main function for CLI usage
 */
async function main() {
  const rawArgs = process.argv.slice(2);
  const args = parseArgs(rawArgs);
  const checkIndex = rawArgs.indexOf("--check-linkedin-status");
  const jobId = checkIndex >= 0 ? rawArgs[checkIndex + 1] : "";

  if (jobId) {
    try {
      const result = await check_linkedin_status({ jobId });
      console.log(result);
      return;
    } catch (error) {
      console.error("❌ Unexpected error:", error.message);
      console.error(error.stack);
      process.exit(1);
    }
  }

  if (!args.title || !args.city) {
    console.error("Error: --title and --city are required");
    process.exit(1);
  }
  if (!args.email) {
    console.error("Error: --email is required");
    process.exit(1);
  }
  if (!args.description) {
    console.error("Error: --description is required");
    process.exit(1);
  }

  try {
    const result = await post_job({
      title: args.title,
      city_query: args.city,
      description: args.description,
      company: args.company,
      email: args.email,
      linkedinCompanyUrl: args.linkedinCompanyUrl,
    });

    console.log(result);
  } catch (error) {
    console.error("❌ Unexpected error:", error.message);
    console.error(error.stack);
    process.exit(1);
  }
}

// Run if called directly
if (process.argv[1] && process.argv[1].includes("post_job.js")) {
  main().catch((error) => {
    console.error("❌ Fatal error:", error.message);
    process.exit(1);
  });
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)