mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 16:46:24 +00:00
Add country-agnostic linkedin-search skill (#20)
A general-purpose, field-agnostic job-search skill built on LinkedIn's public jobs-guest endpoints. Works for any market out of the box — location is an explicit required flag (no country default). Zero runtime dependencies (bun only); search + detail commands. Includes a personal-use / Terms-of-Service note (automated access is against LinkedIn's ToS — keep volume low, non-commercial). (Pairs with the .gitignore fix in #21, which lets skills under .agents/ be tracked.) Co-authored-by: Akhil Tripathi <kodabear@Akhils-MacBook-Pro.local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Akhil Tripathi
Claude Opus 4.8
parent
46a9fdc66c
commit
d8f38fe766
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
SEARCH_URL,
|
||||
htmlFetch,
|
||||
parseJobCards,
|
||||
jobageToTPR,
|
||||
workTypeFlag,
|
||||
writeError,
|
||||
type JobCard,
|
||||
} from "../helpers.js"
|
||||
|
||||
export interface SearchOpts {
|
||||
query?: string
|
||||
location: string
|
||||
jobage: number
|
||||
remote?: string // "remote" | "hybrid" | "onsite"
|
||||
page: number
|
||||
limit?: number
|
||||
format: "json" | "table" | "plain"
|
||||
}
|
||||
|
||||
function buildUrl(opts: SearchOpts): string {
|
||||
const params = new URLSearchParams()
|
||||
if (opts.query) params.set("keywords", opts.query)
|
||||
if (opts.location) params.set("location", opts.location)
|
||||
const tpr = jobageToTPR(opts.jobage)
|
||||
if (tpr) params.set("f_TPR", tpr)
|
||||
const wt = workTypeFlag(opts.remote)
|
||||
if (wt) params.set("f_WT", wt)
|
||||
params.set("start", String((opts.page - 1) * 10))
|
||||
return `${SEARCH_URL}?${params.toString()}`
|
||||
}
|
||||
|
||||
function renderTable(cards: JobCard[]): string {
|
||||
if (cards.length === 0) return "No results."
|
||||
const rows = cards.map((c) => {
|
||||
const title = (c.title || "").slice(0, 42).padEnd(42)
|
||||
const company = (c.company || "—").slice(0, 26).padEnd(26)
|
||||
const loc = (c.location || "—").slice(0, 24).padEnd(24)
|
||||
const date = c.date || "—"
|
||||
return `${c.id.padEnd(11)} ${title} ${company} ${loc} ${date}`
|
||||
})
|
||||
const header =
|
||||
"ID".padEnd(11) +
|
||||
" " +
|
||||
"TITLE".padEnd(42) +
|
||||
" " +
|
||||
"COMPANY".padEnd(26) +
|
||||
" " +
|
||||
"LOCATION".padEnd(24) +
|
||||
" DATE"
|
||||
return [header, "-".repeat(header.length), ...rows].join("\n")
|
||||
}
|
||||
|
||||
export async function runSearch(opts: SearchOpts): Promise<number> {
|
||||
try {
|
||||
const html = await htmlFetch(buildUrl(opts))
|
||||
let cards = parseJobCards(html)
|
||||
if (opts.limit && opts.limit > 0) cards = cards.slice(0, opts.limit)
|
||||
|
||||
if (opts.format === "table") {
|
||||
process.stdout.write(renderTable(cards) + "\n")
|
||||
} else if (opts.format === "plain") {
|
||||
process.stdout.write(
|
||||
cards
|
||||
.map(
|
||||
(c) =>
|
||||
`${c.title}\n ${c.company || "—"} · ${c.location || "—"} · ${c.date || "—"}\n id: ${c.id}\n ${c.url}`,
|
||||
)
|
||||
.join("\n\n") + "\n",
|
||||
)
|
||||
} else {
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{ meta: { count: cards.length, page: opts.page }, results: cards },
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
)
|
||||
}
|
||||
return 0
|
||||
} catch (e) {
|
||||
writeError(e instanceof Error ? e.message : String(e), "SEARCH_FAILED")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user