# Architecture for Job Portal Search Skills and CLI Tool Pattern in AI Job Search

> Explore the modular, skill-based architecture for AI job search portals. Discover how CLI tools and TypeScript drive efficient job skill searching.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: architecture
- Published: 2026-07-26

---

**The architecture employs a modular skill-based design where each job portal is encapsulated as a self-contained CLI tool under `.agents/skills/<portal-name>/`, driven by metadata in [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) and implemented in TypeScript using the Bun runtime.**

This repository implements a **skill-first architecture** that standardizes how AI agents interact with diverse job portals. By isolating each portal's logic into discrete, testable CLI modules, the system enables rapid integration of new sources while maintaining consistent invocation patterns across LinkedIn, Jobnet, Jobindex, and other platforms.

## Skill-First Architecture Overview

The repository organizes every job portal integration as a **skill** located under `.agents/skills/<portal-name>/`. This directory structure creates a thin-pointer architecture that separates metadata, execution logic, and testing into three distinct components.

### SKILL.md Metadata Declaration

Each skill begins with a [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file containing YAML front-matter that declares how the AI framework should invoke the tool. According to the source code in [`.agents/skills/linkedin-search/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/SKILL.md), the metadata specifies:

```yaml
---
name: linkedin-search
version: 1.0.0
description: >-
  Use this skill whenever the user wants to search for jobs …
context: fork
enabled: true
allowed-tools: Bash(bun run .agents/skills/linkedin-search/cli/src/cli.ts *)
---

```

The **`allowed-tools`** field is critical: it tells the core framework exactly how to execute the skill via Bash command. This declarative approach enables the agent to dynamically select and invoke the correct portal based on user intent, without hard-coding integration logic.

### CLI Directory Structure

Inside each skill directory, the `cli/` folder contains a zero-dependency TypeScript implementation designed to run exclusively with **Bun**. The structure follows a consistent pattern across all portals:

- [`cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/cli.ts) – Entry point for flag parsing and command dispatch
- `cli/src/commands/` – Individual command modules ([`search.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/search.ts), [`detail.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/detail.ts))
- [`cli/src/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/helpers.ts) – Shared utilities for HTTP requests and formatting
- `cli/tests/` – Unit tests validating behavior and error handling

This self-contained design requires no `npm install` or external runtime dependencies, making skills lightweight and portable across environments.

## The CLI Tool Pattern Anatomy

The CLI tool pattern follows a repeatable four-layer architecture that standardizes how commands are parsed, executed, and output. This pattern is implemented identically across LinkedIn, Jobnet, and other portal skills.

### Entry Point and Flag Parsing (cli.ts)

The executable entry point in [`cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/cli.ts) handles argument validation and command routing. As implemented in [`.agents/skills/linkedin-search/cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/src/cli.ts), the main function follows this flow:

```typescript
#!/usr/bin/env bun
import { runSearch, type SearchOpts } from "./commands/search.js"
import { runDetail, type DetailOpts } from "./commands/detail.js"

function parseFlags(argv: string[]): Flags { /* … */ }

async function main(): Promise<number> {
  const argv = process.argv.slice(2)
  const flags = parseFlags(argv)
  const cmd = (flags._ as string[])[0]

  if (cmd === "search") {
    const opts: SearchOpts = { /* validated flags */ }
    return runSearch(opts)
  }

  if (cmd === "detail") {
    const id = (flags._ as string[])[1]
    const opts: DetailOpts = { /* validated flags */ }
    return runDetail(opts)
  }

  // Unknown command → error JSON on stderr
}

```

Key architectural decisions include:
- **Custom flag parsing** – A lightweight parser supporting long (`--location`) and short (`-l`) aliases without external libraries
- **Exit code discipline** – Returns `0` for success, `1` for any failure, enabling reliable shell scripting
- **Validation logic** – Required arguments (like `--location` for searches) are validated before dispatching to command modules

### Command Modules (search.ts, detail.ts)

Each sub-command lives in its own module under `cli/src/commands/`, exposing a single `run<Command>(opts)` function that returns `Promise<number>`. The **search** command in [`.agents/skills/jobnet-search/cli/src/commands/search.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/jobnet-search/cli/src/commands/search.ts) builds the portal-specific query URL, performs HTTP requests, and handles pagination.

The **detail** command in [`.agents/skills/linkedin-search/cli/src/commands/detail.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/src/commands/detail.ts) fetches individual job postings by ID and extracts structured fields like title, company, and application links. These modules encapsulate all portal-specific logic, ensuring that changes to one portal's API do not affect others.

### Shared Helpers and Utilities

Common functionality resides in [`cli/src/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/helpers.ts), providing:
- `fetchJson(url, headers)` – HTTP wrapper with exponential back-off for 429/5xx responses
- `parseHtml(html)` – Lightweight DOM parsing for extracting job cards (e.g., `data-entity-urn="urn:li:jobPosting:<id>"`)
- `formatTable(data)` – ASCII table generation for human-readable output

These utilities ensure consistent behavior across portals while handling platform-specific quirks like rate limiting or HTML structure variations.

### Error Handling and Output Formats

The CLI pattern enforces strict error handling: all errors are emitted as JSON objects to **stderr** with a `code` field (e.g., `NO_LOCATION`, `FETCH_ERROR`). This standardized error format allows the AI agent to programmatically handle failures.

Output supports three formats controlled by the `--format` flag:
- `json` (default) – Machine-readable structured data
- `table` – ASCII formatted for terminal display
- `plain` – Human-readable text without formatting

## Implementation Examples

### LinkedIn Search Skill

The LinkedIn skill demonstrates the full pattern implementation. To search for data engineering roles:

```bash
bun run .agents/skills/linkedin-search/cli/src/cli.ts search \
  -q "data engineer" \
  -l "Bengaluru, Karnataka, India" \
  --jobage 30 \
  --format table

```

Retrieving specific job details uses the detail command:

```bash
bun run .agents/skills/linkedin-search/cli/src/cli.ts detail \
  4426311357 \
  --format plain

```

### Jobnet Search Skill

The Danish Jobnet portal follows the identical architecture with portal-specific parameters:

```bash
bun run .agents/skills/jobnet-search/cli/src/cli.ts search \
  --search-string "sygeplejerske" \
  --work-hours FullTime \
  --duration Permanent \
  --order BestMatch \
  --format table

```

The command modules in [`.agents/skills/jobnet-search/cli/src/commands/search.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/jobnet-search/cli/src/commands/search.ts) translate these flags into Jobnet's proprietary query parameters while maintaining the same CLI interface.

## Extending the Architecture

Adding a new job portal requires only three steps:

1. Create the directory structure: `mkdir -p .agents/skills/<portal>-search/cli/src/commands`
2. Write [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) with the `allowed-tools` pointing to the new CLI entry point
3. Implement [`cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli.ts) and command modules following the established pattern of `runSearch()` and `runDetail()` functions

Because each skill is self-contained and uses only Bun's standard library, new portals can be developed and tested independently without modifying the core framework.

## Summary

- The **skill-first architecture** isolates each job portal under `.agents/skills/<portal-name>/` with metadata in [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) and executable logic in `cli/`
- **CLI tools follow a uniform pattern**: entry point ([`cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli.ts)), command modules ([`commands/search.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/commands/search.ts), [`commands/detail.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/commands/detail.ts)), and shared helpers
- **Zero-dependency design** using Bun ensures portability without `npm install` requirements
- **Standardized error handling** writes JSON to stderr with specific error codes, while output supports `json`, `table`, and `plain` formats
- **Extensible structure** allows new portals to be added by creating a directory, declaring metadata, and implementing portal-specific HTTP logic in the command modules

## Frequently Asked Questions

### How does the AI agent know which skill to invoke for a job search?

The agent reads the [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file in each skill directory, specifically the `description` and context fields, to determine relevance. The `allowed-tools` field provides the exact Bash command (e.g., `bun run .agents/skills/linkedin-search/cli/src/cli.ts`) needed to execute the skill, enabling dynamic tool selection based on user intent and portal availability.

### Why does the CLI pattern use Bun instead of Node.js?

The CLI tools use **Bun** to achieve a zero-dependency runtime environment. Unlike Node.js applications that require `npm install` to download dependencies, Bun bundles the TypeScript execution environment natively. This design keeps each skill lightweight and ensures immediate execution without package management overhead, critical for AI agent toolchains that invoke commands ephemerally.

### What happens when a job portal API returns an error or rate limit?

The helper utilities in [`cli/src/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/cli/src/helpers.ts) implement exponential back-off for HTTP 429 and 5xx status codes. If the request ultimately fails, the command module writes a structured JSON error object to **stderr** containing a machine-readable `code` field (such as `FETCH_ERROR` or `RATE_LIMITED`) and exits with code `1`. This allows calling scripts to detect failures programmatically and handle retries or fallbacks appropriately.

### How are new skills tested to ensure they follow the CLI pattern?

Each skill includes a `cli/tests/` directory with unit tests (e.g., [`search.test.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/search.test.ts)) that validate flag parsing, argument validation, and response handling. The repository's CI workflow ([`.github/workflows/ci.yml`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.github/workflows/ci.yml)) runs these tests across all skills to enforce consistency in error handling, output formatting, and exit codes, ensuring that new portals adhere to the established architectural contract before integration.