# Job Portal Search CLI Tools Architecture: Modular Design for the ai-job-search Repository

> Explore the modular six-layer architecture of ai-job-search CLI tools. Discover how standardized commands and data transformation enable seamless integration for efficient job portal searching.

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

---

**The job portal search CLI tools in ai-job-search implement a zero-dependency, six-layer architecture where each portal skill in `.agents/skills/<portal>/cli` exposes standardized commands via [`src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/cli.ts), transforms raw data through [`helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/helpers.ts), and returns unified JSON consumed by the `.claude/skills/job-scraper` orchestrator.**

The ai-job-search repository by MadsLorentzen provides a specialized framework for aggregating job listings across disparate portals through a consistent command-line interface. Understanding the job portal search CLI tools architecture reveals how individual skills for LinkedIn, Jobbank, Jobindex, and government portals integrate into a unified workflow. Each portal follows a strict structural convention that enables the Claude orchestrator to treat every source identically, regardless of whether the underlying implementation uses API calls or HTML scraping.

## The Six-Layer Skill Architecture

Each job portal skill resides in `.agents/skills/<portal-name>/cli` and implements a consistent six-layer pattern:

### 1. CLI Entry Point ([`src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/cli.ts))

The bootstrap layer handles argument parsing and command dispatch. 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 code creates a `createCLI` instance, registers available sub-commands, and executes `cli.run()` to dispatch operations like `search` or `detail` according to the source code lines 7-18.

### 2. Commands Layer (`src/commands/*.ts`)

Concrete actions that interact with portal APIs or scrape HTML live in separate command files. Each skill exports specific functions—`search`, `detail`, `occupations`, and `suggestions`—from individual files such as [`linkedin-search/cli/src/commands/search.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/linkedin-search/cli/src/commands/search.ts). This separation ensures that CLI logic remains distinct from portal-specific implementation details.

### 3. Data Transformation ([`helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/helpers.ts))

Raw HTTP responses undergo normalization in [`helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/helpers.ts) files. Each skill contains a dedicated helper module—for example, [`.agents/skills/linkedin-search/cli/src/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/src/helpers.ts)—that parses job cards or API payloads and maps them to a unified JSON shape containing `id`, `title`, `company`, `location`, `url`, and `publishedAt` fields.

### 4. Output Formatting ([`src/format.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/format.ts))

The `formatResult` utility in [`src/format.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/format.ts) renders unified results as **JSON**, **table**, or **plain** text based on the `--format` flag. This layer is reused across every command within a skill, ensuring consistent presentation regardless of the data source.

### 5. Package Configuration and Testing

Each skill declares its entry point and scripts in [`package.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/package.json). For instance, [`.agents/skills/linkedin-search/cli/package.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/package.json) defines `start`, `test`, and `typecheck` scripts, and specifies the CLI entry file. Testing relies on `bun test`, with [`tests/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/helpers.ts) providing a `runCLI` wrapper for smoke-testing commands against live queries and verifying JSON error handling.

### 6. Skill Metadata Contract ([`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md))

The integration contract resides in [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) at the skill root—such as [`.agents/skills/linkedin-search/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/SKILL.md). This file contains front-matter describing the portal, usage examples, and the critical `allowed-tools` declaration (lines 14-15) that tells the Claude orchestrator exactly which commands it may invoke.

## Integration with the Job Scraper Orchestrator

The `.claude/skills/job-scraper` orchestrator treats all portal skills as interchangeable modules through a standardized invocation pattern. According to the source code, the orchestrator invokes a skill via `Bun` by executing `bun run .agents/skills/<portal>/cli/src/cli.ts search …` as declared in each skill's `allowed-tools` contract.

The integration follows a three-stage pipeline:

1. **Invocation**: The orchestrator calls the CLI tool with the appropriate arguments, receiving JSON output via stdout.
2. **Normalization**: It reads the unified JSON structure—where `search` returns an array of job objects and `detail` returns a rich job description—deduplicating results across multiple portals.
3. **Consumption**: Down-stream commands such as `/apply`, `/rank`, and `/interview` operate on the consolidated data completely agnostic of the original portal source.

This design ensures that adding LinkedIn, Jobnet, or niche government portals requires no changes to the orchestrator logic.

## Scaffolding New Portal Skills

The repository includes an `/add-portal` command that automates the creation of new skills. When invoked with a target URL, such as `/add-portal https://example.com/jobs`, the generator scaffolds a complete `.agents/skills/<name>/cli` tree including [`src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/cli.ts), [`src/commands/search.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/commands/search.ts), [`src/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/src/helpers.ts), [`package.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/package.json), and [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md). The scaffold automatically follows the six-layer architecture, runs a live test query to verify functionality, and registers the skill for immediate use by the orchestrator.

## Code Examples

Running a search against the LinkedIn skill:

```bash
cd .agents/skills/linkedin-search/cli
bun run src/cli.ts search -q "data engineer" -l "Berlin, Germany" -n 10 --format table

```

Fetching detailed information for a specific job ID:

```bash
bun run src/cli.ts detail 4300011451 --format json

```

Using a skill programmatically from the orchestrator (Python example):

```python
import subprocess, json

def run_linkedin_search(query, location):
    result = subprocess.run(
        ["bun", "run", ".agents/skills/linkedin-search/cli/src/cli.ts", "search",
         "-q", query, "-l", location, "--format", "json"],
        capture_output=True, text=True, check=True
    )
    return json.loads(result.stdout)

jobs = run_linkedin_search("backend engineer", "Copenhagen, Denmark")
print(jobs[0]["title"], jobs[0]["company"])

```

Scaffolding a new portal automatically:

```bash
/add-portal https://example.com/jobs

# Creates: .agents/skills/example-search/cli/ with full layer structure

```

## Key Implementation Files

| File Path | Role |
|-----------|------|
| [`.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) | CLI bootstrap and command registration |
| [`.agents/skills/linkedin-search/cli/src/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/src/helpers.ts) | HTML parsing and result normalization |
| [`.agents/skills/linkedin-search/cli/src/format.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/src/format.ts) | Output formatting utility (`formatResult`) |
| [`.agents/skills/linkedin-search/cli/package.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/package.json) | NPM metadata and script definitions |
| [`.agents/skills/linkedin-search/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/SKILL.md) | Skill contract and `allowed-tools` declaration |
| [`.agents/skills/jobnet-search/cli/src/cli.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/jobnet-search/cli/src/cli.ts) | Multi-command layout example for government portals |
| [`.claude/skills/job-scraper/SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/skills/job-scraper/SKILL.md) | Orchestrator configuration calling individual CLI skills |

## Summary

- The job portal search CLI tools architecture relies on a **six-layer pattern** (CLI, commands, helpers, formatting, tests, metadata) implemented in `.agents/skills/<portal>/cli`.
- Each skill is **zero-dependency** for core functionality, requiring only **Bun** to execute.
- A **unified JSON contract** ensures that `search` returns standardized job arrays and `detail` returns rich descriptions, enabling cross-portal deduplication.
- The `.claude/skills/job-scraper` orchestrator invokes skills via `bun run` commands declared in each [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file, treating all portals as interchangeable modules.
- The **`/add-portal`** command generates fully functional new skills following the established architecture, automatically registering them for orchestrator use.

## Frequently Asked Questions

### How does the orchestrator invoke individual portal skills?

The orchestrator invokes skills by executing `bun run` against the specific CLI entry point—for example, `bun run .agents/skills/linkedin-search/cli/src/cli.ts search …`—as defined in the `allowed-tools` section of each skill's [`SKILL.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/SKILL.md) file. This subprocess-based approach captures stdout JSON output while maintaining strict isolation between the orchestrator and portal-specific implementations.

### What is the unified JSON contract between skills and the orchestrator?

The contract specifies that the `search` command must return an array of objects containing `id`, `title`, `company`, `location`, `url`, and `publishedAt` fields, while the `detail` command returns a single rich job description object. This standardization allows the job-scraper to aggregate results from LinkedIn, Jobbank, government portals, and custom sources without parsing portal-specific formats.

### How do I add support for a new job portal?

Use the `/add-portal` command followed by the target URL, such as `/add-portal https://example.com/jobs`. This scaffold creates a complete skill directory at `.agents/skills/<name>/cli` with the six-layer architecture, runs a live test query to verify functionality, and updates the orchestrator's available tools list automatically.

### What testing framework validates the CLI tools?

All skills use **Bun**'s built-in test runner (`bun test`), configured in [`package.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/package.json). The [`tests/helpers.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tests/helpers.ts) file in each skill directory provides a `runCLI` wrapper function that smoke-tests commands against live queries and validates JSON error handling, ensuring reliability before the orchestrator consumes the skill.