Command line
37 verbs, one entry point. Three of them are the whole
daily loop: karani run fetches and filters for free, karani qualify spends a model call on what survived, and karani console
lays out what is ready to send.
Every verb returns 0 when all is well and non-zero when it could
not do what you asked, so each one composes into a shell chain.
Long operations print a machine-readable summary line
(fetched=N inserted=N passed=N) because something is usually
reading the log.
Billed verbs say so in their description. Every one of them obeys
the monthly budget in [budget]: past the cap, a billed provider
is swapped for the local model and the pass says once that it
happened, rather than failing or overspending (ADR 0019).
Setup
Run once, then when the hunt changes.
karani init
The interactive setup. Writes karani.toml: what to hunt, which model runs each task, your positioning, your declared legal facts. Secrets are never written here — they live in .env.
karani init [--yes]Options
| Option | Description |
|---|---|
--yes | Accept all defaults. |
karani config
karani config [{check}]Show the resolved configuration.
Arguments
| Argument | Description |
|---|---|
action | One of check. Default: check. |
karani infra
karani infra [--profile PROFILE] {up,down}Manage karani’s docker infra.
Arguments
| Argument | Description |
|---|---|
action | One of up, down. |
Options
| Option | Description |
|---|---|
--profile PROFILE | Compose profile, e.g. local-llm. |
Examples
# Postgres with pgvector on localhost:5433
karani infra up
# and Ollama on localhost:11434
karani infra up --profile local-llmkarani hunt
Schedules the continuous hunt. Installs the launchd agents: the hourly pass, the twice-daily summaries, and an out-of-process watchdog that is deliberately nothing but sh and curl so it cannot die the same way the pipeline does (ADR 0018).
karani hunt [-h]karani unschedule
karani unschedule [-h]Remove the scheduled hunt.
The hunt
Fetching postings and deciding which deserve a model call. run and qualify are the two tiers (ADR 0001); hourly is both plus delivery, which is what the scheduler actually runs.
karani run
The deterministic tier, and the only one that is free. It fetches every configured board and feed, classifies each posting’s role and seniority, runs the pre-filter, de-duplicates across sources, stores what survives and closes what has gone stale. No model is called. About 95% of a pass ends here.
karani run [-h]Examples
# one deterministic pass over every source
karani runkarani qualify
The LLM tier. Top-scored pending rows go to a model with your resume, your positioning and your past verdicts as few-shot examples, and come back with a fit score, a three-way verdict, evidence-backed strengths and gaps with mitigations. Billed unless [llm.qualify] points at a local model, which the reference config does. Idempotent per resume hash: change your resume and the next run re-judges, leave it alone and a second run costs nothing.
karani qualify [--limit LIMIT] [--concurrency CONCURRENCY] [--provider {None,openrouter,anthropic,local}] [--model MODEL] [--resume RESUME] [--agent]Options
| Option | Description |
|---|---|
--limit LIMIT | Default: 50. |
--concurrency CONCURRENCY | Default: 3. |
--provider PROVIDER | One of None, openrouter, anthropic, local. |
--model MODEL | — |
--resume RESUME | Default: data/resume.md. |
--agent | Tool-using agent loop (OpenRouter only). |
Examples
# judge the top 50 pending rows
karani qualify --limit 50
# on a local model, at zero token cost
karani qualify --provider local --model qwen3:4b
# let the qualifier gather evidence first; 5-10x the cost per row
karani qualify --agent --limit 5karani refilter
Re-runs the pre-filter over every active row. This is what makes changing the rules a config edit rather than a re-ingest: edit karani.toml, run this, and newly-passing rows queue for the next qualify pass.
karani refilter [-h]karani discover
karani discover [--limit LIMIT]Probe unpromoted companies for ATS presence.
Options
| Option | Description |
|---|---|
--limit LIMIT | Default: 10. |
karani sweep
karani sweep [--days DAYS]Mark stale jobs inactive.
Options
| Option | Description |
|---|---|
--days DAYS | Default: 10. |
karani hourly
One full pass: ingest, qualify the arrivals, draft packs for new top-fit roles, deliver review cards. This is what the scheduler runs. It runs under a hard deadline, because a pass that cannot finish silences every pass after it (ADR 0025).
karani hourly [--loop [LOOP]]Options
| Option | Description |
|---|---|
--loop LOOP | — |
karani autopilot
karani autopilot [--min-fit MIN_FIT] [--max-drafts MAX_DRAFTS]Draft packs for top-fit roles and deliver to Slack for review.
Options
| Option | Description |
|---|---|
--min-fit MIN_FIT | — |
--max-drafts MAX_DRAFTS | — |
Reviewing and applying
The daily loop: see what came in, build the pack, send it yourself, record that you did.
karani actions
karani actions [--review-limit REVIEW_LIMIT] [--follow-up-days FOLLOW_UP_DAYS]Prioritized worklist: review, draft, submit, follow up.
Options
| Option | Description |
|---|---|
--review-limit REVIEW_LIMIT | Default: 10. |
--follow-up-days FOLLOW_UP_DAYS | Default: 7. |
karani digest
karani digest [--limit LIMIT] [--format {text,md,html}] [--output OUTPUT]Render top qualified roles.
Options
| Option | Description |
|---|---|
--limit LIMIT | Default: 20. |
--format FORMAT | One of text, md, html. Default: text. |
--output OUTPUT | Write to file instead of stdout. |
Examples
# render the shortlist to a file
karani digest --format html --output data/digest.html
# the top five, for a terminal
karani digest --format text --limit 5karani draft
The pack factory: a tailored resume, a cover letter in your voice, and answers to the application’s questions. Every claim about you is a selection over your own profile records with the citation verified before it ships — an unverifiable claim is dropped, not repaired (CLAUDE.md 4.13). Billed.
karani draft [--provider {None,openrouter,anthropic,local}] [--model MODEL] [--resume RESUME] [--output OUTPUT] job_idArguments
| Argument | Description |
|---|---|
job_id | — |
Options
| Option | Description |
|---|---|
--provider PROVIDER | One of None, openrouter, anthropic, local. |
--model MODEL | — |
--resume RESUME | Default: data/resume.md. |
--output OUTPUT | Output path; default drafts/<id>_<company>__<title>.md. |
Examples
# build the pack for one job
karani draft 1482
# use a stronger model for drafting than for bulk qualification
karani draft 1482 --provider openrouterkarani console
Builds the submission console: one self-contained HTML file with every pack waiting to be sent. Per job, the apply link, presigned links to the rendered documents, and a field-by-field answer sheet with a copy button on each field — answering that job’s real questions, read from the ATS’s own public API. Free: no model is called. It works on a phone, which is where the queue actually gets cleared.
karani console [--limit LIMIT] [--output OUTPUT] [--refresh]Options
| Option | Description |
|---|---|
--limit LIMIT | Default: 50. |
--output OUTPUT | Default data/submission_console.html. |
--refresh | Re-read each job’s questions from the ATS. |
Examples
# build the console from every pack in drafting or ready
karani console
# re-read each job's application questions from its ATS
karani console --refreshkarani submitted
Records what you actually sent: status, timestamp, and a snapshot of the artifact version that went out. That write-back is what arms the follow-up logic and gives the conversion funnel its labels, so skipping it costs you the measurement, not just the bookkeeping. The console prints the exact line to run.
karani submitted [--warm | --cold] [--via VIA] job_ids [job_ids ...]Arguments
| Argument | Description |
|---|---|
job_ids | — |
Options
| Option | Description |
|---|---|
--warm | Went through a warm contact or referral. |
--cold | Cold portal application. |
--via VIA | Console | portal | email | referral. Default: console. |
Examples
# record two applications sent through the portal
karani submitted 1482 1511 --cold
# record one sent through a referral; feeds the warm-vs-cold split
karani submitted 1523 --warmkarani verdict
Your reaction to a suggestion. This is the taste-calibration signal: it becomes a few-shot example in the next qualify prompt and a distilled fact in the memory ledger, so the pipeline’s judgement converges on yours without any retraining.
karani verdict job_id {apply,shortlist,later,skip,applied}Arguments
| Argument | Description |
|---|---|
job_id | — |
verdict | One of apply, shortlist, later, skip, applied. |
Examples
# agree with the suggestion
karani verdict 1482 apply
# disagree; the next qualify pass sees this
karani verdict 1523 skipTracking an application
The state machine, and what fires off it. Moving an application to screen or interview builds an interview pack by itself (ADR 0024).
karani status
karani status [--warm | --cold] job_id {applied,declined,drafting,ghosted,interview,new,offer,ready,rejected,screen}Set application state machine status.
Arguments
| Argument | Description |
|---|---|
job_id | — |
status | One of applied, declined, drafting, ghosted, interview, new, offer, ready, rejected, screen. |
Options
| Option | Description |
|---|---|
--warm | Application went through a warm contact. |
--cold | Cold portal application. |
Examples
# move it to applied, and say how
karani status 1482 applied --cold
# a reply came in; this also fires the interview prep pack
karani status 1511 screenkarani stage
karani stage [--notes NOTES] job_id stageLog an interview or application stage.
Arguments
| Argument | Description |
|---|---|
job_id | — |
stage | E.g. recruiter_screen, hiring_manager, technical, onsite. |
Options
| Option | Description |
|---|---|
--notes NOTES | Default: “. |
karani asked
karani asked [--stage STAGE] job_id questionLog an interview question into the question bank.
Arguments
| Argument | Description |
|---|---|
job_id | — |
question | — |
Options
| Option | Description |
|---|---|
--stage STAGE | Default: “. |
karani outcome
karani outcome job_id {declined,ghosted,offer,rejection,withdrew}Record final outcome.
Arguments
| Argument | Description |
|---|---|
job_id | — |
outcome | One of declined, ghosted, offer, rejection, withdrew. |
karani prep
An interview prep pack: a refreshed company dossier, likely questions derived from the qualification’s gap analysis, STAR stories mined from your own records with every citation verified, comp research derived from the posted band, and questions to ask. Usually you never type this — moving an application to screen or interview fires it. Billed.
karani prep [--stage {screen,interview}] [--provider {None,openrouter,anthropic,local}] [--model MODEL] [--resume RESUME] job_idArguments
| Argument | Description |
|---|---|
job_id | — |
Options
| Option | Description |
|---|---|
--stage STAGE | One of screen, interview. Default: interview. |
--provider PROVIDER | One of None, openrouter, anthropic, local. |
--model MODEL | — |
--resume RESUME | Default: data/resume.md. |
karani followup
karani followup [--provider {None,openrouter,anthropic,local}] [--model MODEL] job_idDraft a follow-up note for a silent application.
Arguments
| Argument | Description |
|---|---|
job_id | — |
Options
| Option | Description |
|---|---|
--provider PROVIDER | One of None, openrouter, anthropic, local. |
--model MODEL | — |
karani watch
Reads recent replies over a read-only Gmail scope, classifies each one, matches it to an application conservatively, advances the state machine only on a confident match, and drafts a reply for review. It cannot send, delete or mark anything read — the scope makes all three impossible, not the code.
karani watch [--days DAYS] [--limit LIMIT] [--no-slack] [--authorize]Options
| Option | Description |
|---|---|
--days DAYS | Lookback window. |
--limit LIMIT | Max messages to read. |
--no-slack | Print the pass instead of pushing it to Slack. |
--authorize | Run the one-time read-only OAuth consent flow. |
Examples
# once: consent to the read-only scope, print the refresh token
karani watch --authorize
# print the pass instead of pushing it
karani watch --no-slack
# a wider window
karani watch --days 30Measuring
Conversion, segmented and interval-reported (ADR 0022). A bare percentage over a handful of applications is a lie told with arithmetic, so these never print one.
karani stats
karani stats [-h]Print DB counts + pipeline funnel.
karani funnel
Conversion by positioning angle, variant, company, comp band and posting age, each rate with its interval. Fit bands are pooled only when one scorer produced every score, because a fit score means nothing across models: measured over 598 rows, one model’s mean was 38 and another’s 47 (ADR 0022).
karani funnel [-h]Examples
# conversion rates, each with its interval
karani funnel
# the derived weekly review over the same ledger
karani coach --window-days 7karani coach
karani coach [--window-days WINDOW_DAYS] [--slack] [--save SAVE] [--docs]Weekly review over the conversion ledger.
Options
| Option | Description |
|---|---|
--window-days WINDOW_DAYS | Default: 7. |
--slack | Also post the review as a Slack card. |
--save SAVE | Write the review markdown to this path. |
--docs | With —save, also render .docx/.pdf. |
karani variant
karani variant [--dimension {positioning,draft_prompt,resume_order,outreach}] [--description DESCRIPTION] [--guidance GUIDANCE] [--job JOB] {list,add,retire,assign} [name]Strategy variants: list, add, retire, assign.
Arguments
| Argument | Description |
|---|---|
action | One of list, add, retire, assign. |
name | — |
Options
| Option | Description |
|---|---|
--dimension DIMENSION | One of positioning, draft_prompt, resume_order, outreach. Default: positioning. |
--description DESCRIPTION | Default: “. |
--guidance GUIDANCE | What a drafter should actually do differently. Default: “. |
--job JOB | Job id, for variant assign. |
Memory and intel
What karani retains between passes.
karani remember
karani remember [--kind {company,outcome,preference,question,strategy}] [--company COMPANY] contentStore a fact in the memory layer.
Arguments
| Argument | Description |
|---|---|
content | — |
Options
| Option | Description |
|---|---|
--kind KIND | One of company, outcome, preference, question, strategy. Default: preference. |
--company COMPANY | — |
Examples
# bank what they actually asked
karani remember "Their screen asked about incident ownership" --kind question --company 'Northwind Robotics'
# a global preference; every later qualification recalls it
karani remember "Not considering crypto companies" --kind preferencekarani recall
karani recall [--kind {None,company,outcome,preference,question,strategy}] [--company COMPANY] [--limit LIMIT] queryQuery the memory layer.
Arguments
| Argument | Description |
|---|---|
query | — |
Options
| Option | Description |
|---|---|
--kind KIND | One of None, company, outcome, preference, question, strategy. |
--company COMPANY | — |
--limit LIMIT | Default: 5. |
karani reindex
karani reindex [-h]Rebuild the mem0 index from the memory ledger.
karani intel
karani intel [--refresh] companyCompany dossier from public probes (cached).
Arguments
| Argument | Description |
|---|---|
company | — |
Options
| Option | Description |
|---|---|
--refresh | — |
Other surfaces
The same pipeline, different front doors. All three are thin adapters over the same storage and runners.
karani mcp
karani mcp [-h]Serve the MCP server over stdio.
karani slack
karani slack [-h]Run the two-way Slack listener.
karani notify
karani notify [--kind {digest,actions}] [--limit LIMIT]Push digest or actions to Slack.
Options
| Option | Description |
|---|---|
--kind KIND | One of digest, actions. Default: digest. |
--limit LIMIT | Default: 10. |
karani notion
karani notion {init,sync} [parent_page_id]Mirror tracked applications to a Notion board.
Arguments
| Argument | Description |
|---|---|
action | One of init, sync. |
parent_page_id | For init: page id to create the database under. |