Kairos
καιρός — the right moment to act
Kairos is a Rust TUI tool that automates the job application workflow for the Australian market.
Search across platforms, analyze JDs with AI, tailor your resume per role, and track every application — all from your terminal.
Features
- Multi-Platform Search — Seek.com.au, Indeed AU, LinkedIn
- LLM-Powered Analysis — Claude analyzes JDs, extracts requirements, scores match against your profile
- Smart Resume Tailoring — Auto-rewrite resume sections per JD with anti-hallucination guards
- Beautiful TUI — ratatui-based, keyboard-driven, async-friendly
- Privacy First — All data local in SQLite, API keys never leave your machine
- Australia Focus — Built for AU market, expandable to US/UK/CA/NZ
Semi-Automatic by Design
Kairos is intentionally semi-automatic. It handles the grunt work but keeps you in control:
- You review every resume modification before saving
- You click the final submit button in your browser
- You decide which jobs to pursue based on match scores
Quick Start
cargo install --path crates/kairos-tui
kairos init
kairos resume import resume.tex
kairos # launch TUI
Overview
The Problem
Applying for jobs is repetitive and time-consuming:
- Search across multiple platforms (Seek, Indeed, LinkedIn)
- Read each JD and decide if it’s a good fit
- Customize your resume for each application
- Submit the application
- Track everything in a spreadsheet
Kairos automates steps 1-4 and replaces the spreadsheet with a proper tracking system.
How It Works
flowchart TD
A[Configure Profile + Import Resume] --> B[Search across platforms]
B --> C[Analyze JDs with Claude AI]
C --> D[Tailor resume per JD]
D --> E[Review diff + Approve]
E --> F[Browser opens → You submit]
F --> G[Track in Dashboard]
Target Platforms
| Platform | Region | Method |
|---|---|---|
| Seek.com.au | Australia, NZ | HTTP scraping (SSR) |
| Indeed | AU, US, UK, CA | Headless browser (JS required) |
| Global | Public listing scraping |
Tech Stack
Every dependency was researched and validated. Here’s what we use and why.
Core
| Crate | Version | Purpose | Why This One |
|---|---|---|---|
| tokio | latest | Async runtime | Industry standard, full features |
| ratatui | 0.30 | TUI framework | 19.1K stars, only mature option, Component Architecture |
| crossterm | latest | Terminal backend | ratatui default, cross-platform |
| rusqlite | 0.39 | Database | Bundled SQLite, zero external deps, ideal for CLI |
| refinery | latest | DB migrations | Clean integration with rusqlite |
TUI Components
| Crate | Purpose |
|---|---|
| rat-widget | Form widgets (text input, date, number, checkbox, tabs) |
| tui-textarea | Multi-line text editing |
| tui-scrollview | Scrollable content views |
| throbber-widgets-tui | Loading spinners for async operations |
| similar | Text diff for resume comparison |
Networking & Scraping
| Crate | Version | Purpose | Why |
|---|---|---|---|
| reqwest | 0.13 | HTTP client | 412M downloads, rustls-tls |
| scraper | 0.26 | HTML parsing | CSS selectors, built on html5ever |
| chromiumoxide | 0.9 | Headless browser | CDP protocol, async, needed for Indeed |
LLM Integration
| Crate | Version | Purpose | Why |
|---|---|---|---|
| rig-core | 0.33 | LLM framework | 6.6K stars, native Anthropic support, structured output, streaming |
Why rig-core over raw HTTP?
- Multi-provider support (Claude, GPT, Gemini) — switch models without code changes
- Structured output with Serde — deserialize LLM responses directly into Rust structs
- Tool calling support — for potential future agent workflows
- Streaming responses — show LLM output in real-time in the TUI
Alternatives considered
| Alternative | Verdict |
|---|---|
Raw reqwest to Anthropic API | Works but reinvents serialization, retry, streaming |
anthropic-ai-sdk (37K downloads) | Low adoption, uncertain maintenance |
async-anthropic (32K downloads) | Stale since May 2025 |
genai 0.6 beta | Still in beta, less mature than rig |
Error Handling
| Crate | Layer | Purpose |
|---|---|---|
| thiserror | Library crates | Custom error types per crate |
| color-eyre | Binary crate | Beautiful error reports, terminal-aware |
Key Decisions
rusqlite vs sqlx
bundledfeature = SQLite compiled into binary, zero runtime deps- No compile-time query checking needed for this scale
- Faster builds (no proc macros)
- Note: rusqlite and sqlx cannot coexist in the same binary (libsqlite3-sys conflict)
ratatui vs cursive
- Ecosystem: 2,800+ dependent crates vs cursive’s shrinking community
- Control: immediate-mode rendering gives pixel-perfect layouts
- Async: first-class tokio integration
chromiumoxide vs fantoccini
- Direct CDP access (can intercept network, modify headers)
- Async-native (tokio)
- Better anti-detection capabilities than WebDriver-based tools
Getting Started
WIP — Kairos is under active development. This page describes the planned setup flow.
Prerequisites
- Rust toolchain (1.80+)
- A Claude API key from Anthropic Console
- Chrome/Chromium (for Indeed scraping via headless browser)
Installation
git clone https://github.com/Misoto22/kairos.git
cd kairos
cargo install --path crates/kairos-tui
First-Time Setup
kairos init
This interactive wizard will:
- Create config at
~/.config/kairos/config.toml - Ask for your Claude API key (stored locally, never transmitted elsewhere)
- Set up your profile (name, location, target roles)
- Initialize the SQLite database at
~/.local/share/kairos/kairos.db
Import Your Resume
kairos resume import ~/resume.tex
Supports LaTeX (.tex) and Markdown (.md) formats. The parser splits your resume into sections and marks which ones are tailorable by the LLM.
Tailorable sections (LLM can modify):
- Summary / Professional Profile
- Technologies / Skills
- Work experience bullet points
Protected sections (never modified):
- Education, dates, company names
- Contact information, references
Launch the TUI
kairos
Use number keys 1-5 to navigate between views.
Configuration
Edit ~/.config/kairos/config.toml directly or use the Config view in the TUI.
[profile]
name = "Your Name"
location = "Perth WA"
country = "AU"
target_roles = ["Software Engineer", "Backend Developer"]
[api]
claude_api_key = "" # or set KAIROS_CLAUDE_API_KEY env var
claude_model = "claude-sonnet-4-20250514"
[search.defaults]
platforms = ["seek", "indeed"]
location = "Perth"
job_type = "fulltime"
salary_min = 70000
posted_within_days = 14
Tip: API keys can also be set via environment variable
KAIROS_CLAUDE_API_KEY, which takes precedence over the config file.
Workflow
The complete Kairos workflow in one diagram:
flowchart TD
subgraph Setup["Setup (one-time)"]
Init[kairos init] --> Import[Import resume]
end
subgraph Search["Search"]
Criteria[Configure criteria] --> Scrape[Scrape platforms]
Scrape --> Store[Store in SQLite]
end
subgraph Analyze["Analyze"]
Store --> LLM1[Claude analyzes JD]
LLM1 --> Score[Match score 0.0-1.0]
Score --> Filter[Filter by score]
end
subgraph Tailor["Tailor"]
Filter --> LLM2[Claude rewrites resume]
LLM2 --> Diff[Show diff]
Diff --> Approve{Approve?}
Approve -->|Yes| Save[Save tailored version]
Approve -->|No| Discard[Discard]
end
subgraph Apply["Apply"]
Save --> Open[Open browser]
Open --> Submit[You submit manually]
Submit --> Track[Track status]
end
Setup --> Search
Step-by-Step
| Step | What Kairos Does | What You Do |
|---|---|---|
| Search | Scrapes Seek/Indeed/LinkedIn | Set criteria |
| Analyze | Calls Claude to parse JD + score match | Review scores |
| Tailor | Rewrites resume sections per JD | Review diff, approve/reject |
| Apply | Opens application URL in browser | Fill form, click submit |
| Track | Records status in SQLite | Update status as it progresses |
Search Jobs
The Search view lets you configure criteria and scrape jobs from multiple platforms simultaneously.
Usage
In the TUI, press 2 to open Search view.
Search Criteria
| Field | Description | Example |
|---|---|---|
| Keywords | Role titles or skills | software engineer, rust developer |
| Location | City or region | Perth, Melbourne, Sydney |
| Platforms | Target job boards | seek, indeed, linkedin |
| Salary Min | Minimum salary (AUD) | 70000 |
| Job Type | Employment type | fulltime, parttime, contract |
| Posted Within | Days since posted | 7, 14, 30 |
| Exclude Companies | Companies to skip | ["Acme Corp"] |
| Exclude Keywords | Keywords to filter out | ["senior", "lead"] |
How It Works
sequenceDiagram
participant U as User
participant TUI as Kairos TUI
participant S as Seek Adapter
participant I as Indeed Adapter
participant DB as SQLite
U->>TUI: Configure search criteria
TUI->>S: search(criteria)
TUI->>I: search(criteria)
S-->>TUI: Vec of Jobs
I-->>TUI: Vec of Jobs
TUI->>DB: save_jobs(jobs)
TUI->>U: Display job list (deduplicated)
Platform-Specific Notes
Seek.com.au
- Server-side rendered pages with embedded JSON
- Rate limit: 2-5 second delay between requests
- ~20 results per page, max ~550 per query
- Most reliable scraping target
Indeed AU
- Requires headless browser (Cloudflare protection)
- Randomized delays (3-8 seconds) with jitter
- User-Agent rotation per session
- May occasionally require CAPTCHA solving (manual fallback)
- Public job listings (no login required)
- Moderate anti-bot measures
- Falls back to “open in browser” if blocked
Deduplication
Jobs found on multiple platforms are deduplicated by company name + job title similarity. The first instance is kept, with cross-references to other platform URLs.
Analyze JD
After searching, use the Analyze step to have Claude AI parse each job description and compute a match score against your profile.
Usage
In the TUI, select a job from the list and press a to analyze.
What Gets Extracted
| Field | Description |
|---|---|
| Required Skills | Must-have technical skills |
| Preferred Skills | Nice-to-have skills |
| Years Experience | Minimum years required |
| Key Responsibilities | Main duties of the role |
| ATS Keywords | Keywords to include in your resume |
| Match Score | 0.0 - 1.0 match against your profile |
| Match Reasoning | Why this score was given |
Match Scoring
flowchart LR
JD[Job Description] --> LLM[Claude AI]
Profile[Your Profile] --> LLM
LLM --> Analysis[JdAnalysis]
Analysis --> Score[Match Score]
Analysis --> Keywords[ATS Keywords]
Analysis --> Skills[Skill Gaps]
The match score considers:
- Skill overlap — how many required/preferred skills you have
- Experience level — your years vs. what they ask for
- Location compatibility — remote, on-site, visa requirements
- Role alignment — how well responsibilities match your background
Cost
Each analysis calls Claude API (~500-1500 tokens per JD):
- ~$0.003-0.01 per job analyzed
- Batch of 50 jobs ~ $0.15-0.50
The TUI shows estimated cost before batch analysis.
Tailor Resume
The Tailor step uses Claude AI to rewrite your resume sections based on the specific JD, with strict guards against hallucination.
Usage
In the TUI, select an analyzed job and press t to tailor.
How It Works
flowchart TD
Base[Base Resume] --> Split[Split into Sections]
JD[JD Analysis] --> Prompt[Tailoring Prompt]
Split --> |Tailorable sections only| Prompt
Prompt --> LLM[Claude AI]
LLM --> Modified[Modified Sections]
Modified --> Diff[Side-by-Side Diff]
Diff --> |User approves| Save[Save Tailored Version]
Diff --> |User rejects| Discard[Discard]
Split --> |Protected sections| Merge[Merge]
Save --> Merge
Merge --> Final[Complete Tailored Resume]
Anti-Hallucination Guards
The LLM prompt enforces strict rules:
- Only rephrase, reorder, or emphasize existing content
- Never invent skills, experiences, or achievements
- Never modify: Education, dates, company names, contact info
- Only modify: Summary, Technologies/Skills, experience bullet points
The system works by:
- Sending only
tailorable: truesections to Claude - Including the JD analysis (keywords, required skills) as context
- Asking Claude to optimize for ATS keyword matching while preserving truthfulness
- Setting temperature to 0.3 for controlled creativity
The Diff View
After tailoring, the TUI shows a side-by-side diff:
+-- Original ----------------+-- Tailored -------------------+
| | |
| Experienced software | Experienced software |
| developer with expertise | developer with deep expertise |
| in Rust and Python. | in Rust, async systems, and |
| | cloud-native Python services. |
| | |
| - Built CLI tools in Rust | - Built high-performance CLI |
| | tools in Rust with async |
| | I/O and error handling |
+-----------------------------+-------------------------------+
| [y] Approve [n] Reject [e] Edit manually [r] Retry |
+------------------------------------------------------------+
Resume Versions
- Base resume is never modified
- Tailored versions only store the changed sections
- Full resume is reconstructed by merging base + tailored sections
- Export to LaTeX or PDF: select job and press
xto export
Apply
The Apply step opens the job application page in your browser. Kairos prepares everything — you make the final call.
Usage
In the TUI, select a tailored job and press Enter to apply.
Workflow
flowchart LR
Ready[Resume Tailored] --> Open[Open Browser]
Open --> Page[Application Page]
Page --> User[You Submit Manually]
User --> Confirm[Mark as Submitted in TUI]
- Kairos checks that a tailored resume exists and is approved
- Opens the job URL in your default browser
- You fill in the application form and upload the tailored resume
- Back in the TUI, confirm submission and status updates to “Submitted”
Why Semi-Automatic?
- Respect for employers: No mass bot applications
- Quality control: You review every application
- Platform TOS: Automated submission violates most platforms’ terms
- Your judgment: Some fields need human context (cover letter, salary, visa questions)
Application Tracking
After applying, track status in the Dashboard:
| Status | Meaning |
|---|---|
| Prepared | Resume tailored, ready to submit |
| Submitted | You confirmed submission |
| Acknowledged | Company acknowledged receipt |
| Interview | Interview scheduled |
| Rejected | Application rejected |
| Offer | Received an offer |
Follow-Up Reminders
- Default follow-up: 2 weeks after submission
- Kairos highlights overdue follow-ups in the Dashboard
Architecture Overview
Kairos follows Clean Architecture with a Cargo workspace separating concerns into independent crates.
High-Level Architecture
graph TB
subgraph Binary Layer
TUI["kairos-tui\nratatui + crossterm"]
end
subgraph Application Layer
Core["kairos-core\nEntities + Traits + Services"]
end
subgraph Infrastructure Layer
Platform["kairos-platform\nSeek / Indeed / LinkedIn"]
LLM["kairos-llm\nrig-core + Claude"]
DB["kairos-db\nrusqlite + SQLite"]
end
TUI --> Core
Platform --> Core
LLM --> Core
DB --> Core
Dependency Flow
kairos-tui (binary)
+-- kairos-core (domain: entities, traits, errors)
+-- kairos-platform (implements JobSearchService)
| +-- kairos-core
+-- kairos-llm (implements JdAnalysisService, ResumeTailorService)
| +-- kairos-core
+-- kairos-db (implements *Repository traits)
+-- kairos-core
Rule: Infrastructure crates depend on kairos-core for trait definitions. kairos-core depends on nothing — it’s the innermost layer.
Crate Responsibilities
| Crate | Responsibility | Key Trait |
|---|---|---|
| kairos-core | Domain entities, service traits, error types | All trait definitions |
| kairos-tui | Terminal UI, event loop, user interaction | — (binary) |
| kairos-platform | Job scraping from Seek/Indeed/LinkedIn | JobSearchService |
| kairos-llm | JD analysis + resume tailoring via Claude | JdAnalysisService, ResumeTailorService |
| kairos-db | SQLite persistence | JobRepository, ResumeRepository, ApplicationRepository |
Async Architecture
flowchart LR
subgraph Main Thread
EventLoop["Event Loop\ntokio::select!"]
Render[Render TUI]
end
subgraph Background Tasks
Scrape["Scrape Jobs\ntokio::spawn"]
Analyze["Analyze JD\ntokio::spawn"]
Tailor["Tailor Resume\ntokio::spawn"]
end
Input[Keyboard Input] --> EventLoop
Channel[mpsc Channel] --> EventLoop
EventLoop --> Render
EventLoop --> Scrape
EventLoop --> Analyze
EventLoop --> Tailor
Scrape --> Channel
Analyze --> Channel
Tailor --> Channel
All I/O-heavy operations (scraping, LLM calls) run as tokio::spawn tasks. Results flow back through an mpsc channel so the TUI never blocks.
Project Structure
Workspace Layout
kairos/
+-- Cargo.toml # Workspace root
+-- config.example.toml
+-- docs/ # mdbook documentation
|
+-- crates/
+-- kairos-tui/ # Binary: TUI application
| +-- Cargo.toml
| +-- src/
| +-- main.rs # Entry point, terminal setup
| +-- app.rs # App state, router, DI wiring
| +-- event.rs # Event loop (input + async results)
| +-- action.rs # Action enum
| +-- theme.rs # Color theme definitions
| +-- components/
| +-- mod.rs
| +-- dashboard.rs # Home: application status overview
| +-- search.rs # Search criteria form
| +-- job_list.rs # Sortable, filterable job table
| +-- job_detail.rs # JD text + analysis results
| +-- resume_diff.rs # Side-by-side resume diff
| +-- resume_import.rs
| +-- config_editor.rs
| +-- status_bar.rs # Keyboard shortcuts hint
| +-- popup.rs # Confirmation dialogs
|
+-- kairos-core/ # Domain layer (no external deps)
| +-- Cargo.toml
| +-- src/
| +-- lib.rs
| +-- error.rs # DomainError (thiserror)
| +-- entity/
| | +-- mod.rs
| | +-- job.rs # Job, JdAnalysis, Platform
| | +-- resume.rs # Resume, ResumeSection, TailoredResume
| | +-- application.rs # Application, ApplicationStatus
| | +-- user_profile.rs # UserProfile, Country
| | +-- search_criteria.rs
| +-- repository/
| | +-- mod.rs
| | +-- job_repository.rs
| | +-- resume_repository.rs
| | +-- application_repository.rs
| +-- service/
| +-- mod.rs
| +-- job_search_service.rs
| +-- jd_analysis_service.rs
| +-- resume_tailor_service.rs
| +-- application_service.rs
|
+-- kairos-platform/ # Platform adapters
| +-- Cargo.toml
| +-- src/
| +-- lib.rs
| +-- error.rs
| +-- rate_limiter.rs
| +-- http_client.rs
| +-- seek/
| | +-- mod.rs
| | +-- client.rs # impl JobSearchService
| | +-- parser.rs
| | +-- types.rs
| +-- indeed/
| | +-- mod.rs, client.rs, parser.rs, types.rs
| +-- linkedin/
| +-- mod.rs, client.rs, parser.rs, types.rs
|
+-- kairos-llm/ # LLM integration
| +-- Cargo.toml
| +-- src/
| +-- lib.rs
| +-- error.rs
| +-- analyzer.rs # impl JdAnalysisService
| +-- tailor.rs # impl ResumeTailorService
| +-- prompt/
| +-- mod.rs
| +-- jd_analysis.rs
| +-- resume_tailoring.rs
|
+-- kairos-db/ # Persistence layer
+-- Cargo.toml
+-- migrations/
| +-- V1__initial.sql
+-- src/
+-- lib.rs
+-- error.rs
+-- repository/
+-- mod.rs
+-- sqlite_job_repo.rs
+-- sqlite_resume_repo.rs
+-- sqlite_application_repo.rs
Crate Dependency Graph
graph TD
TUI[kairos-tui] --> Core[kairos-core]
TUI --> Platform[kairos-platform]
TUI --> LLM[kairos-llm]
TUI --> DB[kairos-db]
Platform --> Core
LLM --> Core
DB --> Core
Design Principles
kairos-corehas zero infrastructure dependencies — onlythiserror,chrono,uuid,serde,async-trait- One concept per file — each file owns one struct/trait
- Traits in core, implementations in infrastructure — ports-and-adapters pattern
- All I/O is async — via tokio
- Error types per crate — each crate has its own
error.rswiththiserror
Data Model
Entity Relationship
erDiagram
Job ||--o| JdAnalysis : analyzed
Job ||--o{ Application : applied
Resume ||--o{ TailoredResume : tailored
Resume ||--|{ ResumeSection : contains
TailoredResume ||--|{ TailoredSection : contains
TailoredResume }o--|| Job : for
Application }o--o| TailoredResume : uses
Job {
uuid id PK
string platform
string platform_job_id
string title
string company
string location
int salary_min
int salary_max
string url
text description_raw
string status
datetime scraped_at
}
JdAnalysis {
uuid job_id FK
json required_skills
json preferred_skills
int years_experience
json keywords
float match_score
text match_reasoning
}
Resume {
uuid id PK
bool is_base
string format
}
ResumeSection {
uuid id PK
uuid resume_id FK
string section_type
text content
bool tailorable
}
TailoredResume {
uuid id PK
uuid base_resume_id FK
uuid job_id FK
text diff_summary
bool approved
}
Application {
uuid id PK
uuid job_id FK
uuid tailored_resume_id FK
string status
datetime applied_at
}
Key Entities
Job
The central entity. Tracks a job posting from discovery through application.
#![allow(unused)]
fn main() {
pub enum Platform { Seek, LinkedIn, Indeed }
pub enum JobStatus {
New, // Just scraped
Analyzed, // JD analyzed by LLM
Matched, // User marked as interesting
Skipped, // User passed
Applied, // Submitted
Rejected, // Got rejection
Interview, // Interview scheduled
Offer, // Received offer
}
}
JdAnalysis
LLM-generated analysis attached 1:1 to a Job. Key fields:
required_skills/preferred_skills— extracted from JDkeywords— ATS keywords to include in resumematch_score— 0.0 to 1.0, computed against UserProfilematch_reasoning— human-readable explanation
Resume & TailoredResume
Base resume imported once and split into sections. Each section marked tailorable: true/false.
Tailored resumes only store modified sections — unchanged sections inherit from base at render time.
#![allow(unused)]
fn main() {
pub enum SectionType {
Summary, // tailorable
Education, // protected
WorkExperience, // tailorable (bullets only)
Internship, // tailorable (bullets only)
Projects, // tailorable
Technologies, // tailorable
Extracurricular, // protected
References, // protected
}
}
Application
Tracks the lifecycle from preparation to outcome.
#![allow(unused)]
fn main() {
pub enum ApplicationStatus {
Prepared, // Resume tailored, ready
Submitted, // User confirmed
Acknowledged, // Company acknowledged
Interview, // Scheduled
Rejected,
Offer,
}
}
SQLite Schema
All JSON arrays (skills, keywords) stored as TEXT with serde_json. UUIDs as TEXT. Dates in ISO 8601.
See crates/kairos-db/migrations/V1__initial.sql for the full schema.
Platform Adapters
Each job platform is implemented as an adapter behind the JobSearchService trait.
Trait Definition
#![allow(unused)]
fn main() {
#[async_trait]
pub trait JobSearchService: Send + Sync {
fn platform(&self) -> Platform;
async fn search(&self, criteria: &SearchCriteria) -> Result<Vec<Job>, DomainError>;
async fn fetch_details(&self, job: &Job) -> Result<Job, DomainError>;
}
}
Adapter Architecture
graph LR
subgraph kairos-core
Trait[JobSearchService trait]
end
subgraph kairos-platform
Seek[SeekClient]
Indeed[IndeedClient]
LinkedIn[LinkedInClient]
RL[RateLimiter]
HC[PlatformHttpClient]
end
Seek --> Trait
Indeed --> Trait
LinkedIn --> Trait
Seek --> HC
Indeed --> HC
LinkedIn --> HC
HC --> RL
Shared Infrastructure
RateLimiter
Enforces randomized delays between requests:
- Seek: 2-5s
- Indeed: 3-8s (more aggressive anti-bot)
- LinkedIn: 2-6s
PlatformHttpClient
Wraps reqwest::Client with:
- Rate limiting
- User-Agent rotation (pool of realistic browser UAs)
- Retry with exponential backoff (3 attempts)
- Configurable proxy support
Seek.com.au
Method: reqwest + scraper (HTTP + HTML parsing)
flowchart LR
Search[Search URL] --> HTTP[reqwest GET]
HTTP --> HTML[HTML Response]
HTML --> Parse["scraper: extract JSON"]
Parse --> Struct[SeekJobListing]
Struct --> Map[Map to Job entity]
- Server-side rendered with job data embedded as JSON in HTML
- Pagination: 20 results per page
- Key selector:
<script type="application/json">tag
Indeed AU
Method: chromiumoxide (headless Chrome via CDP)
flowchart LR
Search[Search URL] --> Chrome[chromiumoxide]
Chrome --> Wait[Wait for render]
Wait --> DOM[Extract from DOM]
DOM --> Parse[Parse listings]
Parse --> Map[Map to Job entity]
- Requires JavaScript rendering (Cloudflare protection)
- Anti-detection: disable automation flags, randomized viewport, human-like scrolling
- Slower (~5-10s per page)
Method: reqwest + scraper (public job listings)
- Public job search pages, no login required
- Fallback: if blocked (403/429), suggests user browse manually
- Jobs can be added by pasting URL
Adding a New Platform
- Create module under
kairos-platform/src/ - Implement
JobSearchServicetrait - Add variant to
Platformenum inkairos-core - Register adapter in
kairos-tui/src/app.rs
No existing code needs to change.
LLM Integration
Kairos uses rig-core to communicate with Claude AI for two core functions: JD analysis and resume tailoring.
Architecture
graph TB
subgraph "kairos-core (traits)"
T1[JdAnalysisService]
T2[ResumeTailorService]
end
subgraph "kairos-llm (implementations)"
A[ClaudeAnalyzer]
T[ClaudeTailor]
P1[JD Analysis Prompts]
P2[Resume Tailoring Prompts]
RC[rig-core Client]
end
A --> T1
T --> T2
A --> P1
T --> P2
A --> RC
T --> RC
RC --> API[Claude API]
JD Analysis
Prompt Strategy
Temperature: 0.0 (deterministic extraction)
The prompt instructs Claude to:
- Extract structured data from the JD text
- Identify required vs preferred skills
- Extract ATS keywords
- Compare against the user’s profile
- Compute a match score (0.0 - 1.0) with reasoning
Output is parsed as JSON matching JdAnalysis struct via rig-core’s structured output.
Resume Tailoring
Anti-Hallucination Rules (enforced in system prompt)
- Only rephrase, reorder, or emphasize existing content
- Never invent skills, experiences, achievements, or metrics
- Never change company names, dates, job titles, education details
- Only modify sections marked as tailorable
- Preserve the original format (LaTeX or Markdown)
Temperature: 0.3 (controlled creativity)
Validation
After LLM response, a validation step checks:
- No new company names or job titles appeared
- No new degree or institution names
- Dates haven’t changed
- Section types match what was sent
If validation fails, retry with stricter prompt or flag to user.
Output Flow
flowchart TD
Input["Base Resume Sections\n+ JD Analysis"] --> LLM[Claude via rig-core]
LLM --> Raw[Raw LLM Response]
Raw --> Parse[Parse modified sections]
Parse --> Validate[Validate: no fabricated content]
Validate --> |Pass| Diff[Generate diff]
Validate --> |Fail| Retry[Retry with stricter prompt]
Diff --> User[Show to user for approval]
Cost
| Operation | Tokens (approx) | Cost at Sonnet |
|---|---|---|
| JD Analysis | 500-1,500 | ~$0.003-0.01 |
| Resume Tailoring | 1,000-3,000 | ~$0.01-0.03 |
Model Configuration
Default: claude-sonnet-4-20250514
[api]
claude_model = "claude-sonnet-4-20250514"
rig-core’s multi-provider support allows switching to OpenAI or Gemini in the future.
TUI Design
Kairos uses ratatui 0.30 with the Component Architecture pattern.
Layout
+-------------------------------------------------------------+
| Kairos Perth, AU |
+--------+----------------------------------------------------+
| | |
| 1 Home | |
| 2 Search Active View |
| 3 Jobs (content area) |
| 4 Resume |
| 5 Status |
| | |
+--------+----------------------------------------------------+
| q Quit | 1-5 Navigate | / Search | ? Help | Loading... |
+-------------------------------------------------------------+
Views
1. Dashboard (Home)
Pipeline overview + recent activity + overdue follow-ups.
2. Search
Form-based search criteria configuration with platform selection.
3. Job List
Sortable, filterable table with columns: Score, Title, Company, Salary, Status.
4. Resume
Resume management: import, view base, view tailored versions, side-by-side diff.
5. Status
Application tracking: all applications with status, applied date, notes.
Component Architecture
graph TD
App[App State + Router]
App --> Dashboard
App --> Search
App --> JobList[Job List]
App --> JobDetail[Job Detail]
App --> ResumeDiff[Resume Diff]
App --> StatusView[Status View]
App --> StatusBar[Status Bar]
App --> Popup
Each component implements:
#![allow(unused)]
fn main() {
pub trait Component {
fn handle_event(&mut self, event: &Event) -> Option<Action>;
fn update(&mut self, action: &Action);
fn render(&self, frame: &mut Frame, area: Rect);
}
}
Event Loop
flowchart TD
Start[Start] --> Init[Initialize Terminal]
Init --> Loop{Main Loop}
Loop --> Select["tokio::select!"]
Select --> |Keyboard| HandleInput[Handle Input]
Select --> |Channel msg| HandleAsync[Handle Async Result]
Select --> |Tick| Render[Render Frame]
HandleInput --> Dispatch[Dispatch Action]
HandleAsync --> Dispatch
Dispatch --> Update[Update Component State]
Update --> Render
Render --> Loop
Loop --> |quit| Cleanup[Restore Terminal]
Keyboard Navigation
| Key | Action |
|---|---|
1-5 | Switch views |
j/k or arrows | Navigate lists |
Enter | Select / confirm |
/ | Search / filter |
a | Analyze selected job |
t | Tailor resume for job |
Space | Apply (open browser) |
s | Sort |
f | Filter |
q | Quit / back |
? | Help overlay |
Theme
Catppuccin Mocha inspired palette:
| Element | Color |
|---|---|
| Primary (headers, selected) | Blue #4a9eff |
| Success (applied, offer) | Green #51cf66 |
| Warning (follow-up) | Yellow #ffd43b |
| Error (rejected) | Red #ff6b6b |
| Accent (match score) | Purple #cc5de8 |
| Background | Dark #1e1e2e |
| Text | Light gray #cdd6f4 |
Implementation Roadmap
Phase Overview
gantt
title Kairos Implementation Phases
dateFormat YYYY-MM-DD
axisFormat %b %d
section Phase 1 Skeleton
Workspace + Entities + Traits :p1a, 2026-03-27, 2d
Config + SQLite + Migrations :p1b, after p1a, 1d
TUI shell with navigation :p1c, after p1a, 2d
section Phase 2 Seek Search
SeekClient + Parser :p2a, after p1c, 3d
SQLite Job Repository :p2b, after p1b, 1d
Search View + Job List View :p2c, after p2a, 2d
section Phase 3 LLM Analysis
rig-core Claude Client :p3a, after p2c, 2d
JD Analysis + Match Scoring :p3b, after p3a, 2d
Job Detail View :p3c, after p3b, 1d
section Phase 4 Resume Tailoring
LaTeX Parser :p4a, after p3c, 2d
Tailoring Prompts :p4b, after p4a, 3d
Resume Diff View :p4c, after p4b, 2d
section Phase 5 Apply and Track
Apply Workflow + Browser Open :p5a, after p4c, 1d
Dashboard + Status View :p5b, after p5a, 2d
Indeed + LinkedIn Adapters :p5c, after p5a, 3d
section Phase 6 Polish
Error Handling + Edge Cases :p6a, after p5c, 2d
Theme + UX Polish :p6b, after p6a, 2d
Phase Details
Phase 1: Skeleton
Goal: Compilable workspace with navigable TUI shell.
- Init cargo workspace with all 5 crates
- Define all entities in
kairos-core - Define all traits (Repository + Service)
- Define error types per crate
- Config loading (TOML + XDG paths)
- SQLite setup + migration V1
- TUI event loop + view navigation
- Empty component shells + status bar
Verify: cargo build succeeds, TUI launches, can switch views.
Phase 2: Seek Search
Goal: Search Seek.com.au and display results in TUI.
- SeekClient with HTTP + HTML parsing
- Rate limiter with randomized delays
- SqliteJobRepository
- Search view (form with criteria)
- Job list view (sortable table)
- Async task dispatch (search -> spawn -> channel -> update)
Verify: Enter criteria -> see Seek jobs in table.
Phase 3: LLM Analysis
Goal: Analyze JDs with Claude and show match scores.
- rig-core client setup
- JD analysis prompt templates
- Analyzer implementation
- Job detail view with analysis
- Match score column in job list
- Batch analyze action
Verify: Select job -> press a -> see analysis with score.
Phase 4: Resume Tailoring
Goal: Import resume, tailor per JD, show diff.
- LaTeX section parser
- Resume import flow
- SqliteResumeRepository
- Tailoring prompts with anti-hallucination
- Tailor implementation with validation
- Side-by-side diff view
- Approve/reject flow
Verify: Import resume -> select job -> t -> see diff -> approve.
Phase 5: Apply + Track
Goal: Complete semi-automatic workflow.
- Apply action (open browser)
- SqliteApplicationRepository
- Dashboard view (pipeline overview)
- Status view (application tracking)
- Indeed adapter (chromiumoxide)
- LinkedIn adapter
Verify: Full flow: search -> analyze -> tailor -> apply -> track.
Phase 6: Polish
Goal: Production-quality UX.
- Graceful error handling in all views
- Loading spinners for async operations
- Catppuccin Mocha theme finalization
- Help overlay (
?key) - Empty states, network failures, API rate limits
- Resume export (PDF/LaTeX)
Definition of Done
- All crates build with
cargo build cargo clippypasses with no warnings- Core workflow works end-to-end
- At least Seek adapter is fully functional
- Resume tailoring produces valid LaTeX output
- TUI is responsive and doesn’t block on async operations
- API keys handled securely (env var or config, never logged)