Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,34 @@ Add your Cloudflare Workers AI account ID and API token before running this agen

That means the instance runtime path is working and correctly refusing to bill the platform AI account. See [MCP Instance Runtime](docs/mcp-instance-runtime.md) for the full tool map, live test record, and OAuth troubleshooting.

### Local browser runner

Browser-capable agents use PAGS as the control-plane brain and a local runner as the tool executor. The first local runner package lives in `packages/browser-runner`.

```bash
pnpm --filter @proagentstore/browser-runner dev -- --port 49171 --token "$PAGS_RUNNER_TOKEN" --instance-id "$PAGS_INSTANCE_ID"
pnpm --filter @proagentstore/cli dev runner status --token "$PAGS_RUNNER_TOKEN" --instance-id "$PAGS_INSTANCE_ID"
pnpm --filter @proagentstore/cli dev runner task --type echo --input '{"ok":true}' --token "$PAGS_RUNNER_TOKEN" --instance-id "$PAGS_INSTANCE_ID"
```

When exposing the runner through a tunnel, start it with a token and instance binding, then register only the tunnel URL plus token with PAGS. Runtime registration is instance-scoped: PAGS stores the endpoint and encrypted runner token, then MCP/API proxy task calls to the runner with `X-PAGS-Instance-Id`.

```bash
pnpm --filter @proagentstore/cli dev runner register "$PAGS_INSTANCE_ID" \
--endpoint-url "$PAGS_RUNNER_ENDPOINT" \
--runner-token "$PAGS_RUNNER_TOKEN" \
--pags-token "$PAGS_TOKEN" \
--probe
pnpm --filter @proagentstore/cli dev runner runtime "$PAGS_INSTANCE_ID" --pags-token "$PAGS_TOKEN" --probe
pnpm --filter @proagentstore/cli dev runner run "$PAGS_INSTANCE_ID" --type echo --input '{"ok":true}' --pags-token "$PAGS_TOKEN"
```

```text
subscribe_agent -> register_instance_runtime -> instance_runtime_status(probe: true) -> run_instance_task -> approve_instance_task -> instance_task_events
```

The browser runtime MCP tools are `register_instance_runtime`, `instance_runtime_status`, `unregister_instance_runtime`, `run_instance_task`, `approve_instance_task`, `cancel_instance_task`, and `instance_task_events`.

### Skills and plugins

ProAgentStore publishes skills through platform-specific plugin marketplaces so users can find them from both Codex and Claude Code.
Expand Down
24 changes: 24 additions & 0 deletions agents/job-application-assistant/.github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.30.3
- uses: actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --no-frozen-lockfile
- run: pnpm typecheck
- run: pnpm test
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
3 changes: 3 additions & 0 deletions agents/job-application-assistant/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
.wrangler/
66 changes: 66 additions & 0 deletions agents/job-application-assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Job Application Assistant

A ProAgentStore agent that accepts a job URL, extracts the posting, prepares a tailored application packet, and submits only when the target exposes a simple safe form and the caller gives explicit confirmation.

## Endpoints

| Method | Path | Description |
|---|---|---|
| `GET` | `/` | Health check and endpoint list |
| `GET` | `/profile` | Read saved candidate profile |
| `PUT` | `/profile` | Save candidate profile fields |
| `POST` | `/applications` | Analyze a job URL and create an application packet |
| `POST` | `/run` | Alias for `/applications` for generic tool callers |
| `GET` | `/applications` | List recent application packets |
| `GET` | `/applications/:id` | Read one application packet |
| `POST` | `/applications/:id/submit` | Submit a safe basic HTML form after explicit confirmation |

## Create an application packet

```bash
curl -X POST https://job-application-assistant.proagentstore.online/applications \
-H "Content-Type: application/json" \
-d '{
"jobUrl": "https://example.com/jobs/senior-product-engineer",
"profile": {
"fullName": "Sam Candidate",
"email": "sam@example.com",
"phone": "+1 555 0100",
"linkedin": "https://linkedin.com/in/sam",
"portfolio": "https://sam.dev",
"resumeText": "Senior full-stack engineer...",
"location": "Remote"
},
"answers": {
"work authorization": "I am authorized to work in the United States.",
"salary": "$180k target total compensation"
}
}'
```

The response includes `draft.coverLetter`, `draft.shortPitch`, `draft.answers`, detected form fields, and `submission.ready`.

## Submit with confirmation

For simple job boards with a direct HTML form:

```bash
curl -X POST https://job-application-assistant.proagentstore.online/applications/app_123/submit \
-H "Content-Type: application/json" \
-d '{"confirmation":"submit app_123"}'
```

Submission is blocked when the page needs login, captcha, file upload, password fields, JavaScript-only flow, or multi-step browser work.

## Safety model

This agent does not silently send resume/contact data. It prepares the packet first, reports blockers, and requires the exact `submit <application-id>` confirmation before any external POST/GET submission attempt.

## Development

```bash
pnpm install
pnpm test
pnpm typecheck
pnpm dev
```
12 changes: 12 additions & 0 deletions agents/job-application-assistant/agent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "job-application-assistant",
"name": "Job Application Assistant",
"description": "Turns a job URL into a tailored application packet, checks whether the job can be safely submitted, and submits only after explicit confirmation.",
"storeType": "agent",
"category": "productivity",
"model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
"template": "api",
"serverConfig": {
"routes": ["job-application-assistant.proagentstore.online/*"]
}
}
21 changes: 21 additions & 0 deletions agents/job-application-assistant/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@proagentstore/job-application-assistant",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"hono": "^4.7.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250530.0",
"typescript": "^5.7.0",
"vitest": "^3.2.4",
"wrangler": "^4.0.0"
}
}
148 changes: 148 additions & 0 deletions agents/job-application-assistant/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { describe, expect, it } from "vitest";
import {
buildFallbackDraft,
buildSubmissionPlan,
buildSubmissionRequest,
extractJobPage,
mapProfileToFields,
normalizeProfile,
validateJobUrl,
validateProfile,
} from "./lib.js";

const profile = normalizeProfile({
fullName: "Sam Candidate",
email: "sam@example.com",
phone: "+1 555 0100",
linkedin: "https://linkedin.com/in/sam",
portfolio: "https://sam.dev",
location: "Remote",
resumeText:
"Built distributed TypeScript systems for high-volume workflow automation. Led product engineering teams shipping customer-facing tools. Improved application conversion with structured experiments.",
workAuthorization: "Authorized to work in the United States.",
salaryExpectations: "$180k target total compensation.",
});

describe("job URL and profile validation", () => {
it("accepts http and https job URLs only", () => {
expect(validateJobUrl("https://example.com/jobs/1")).toBe("https://example.com/jobs/1");
expect(() => validateJobUrl("ftp://example.com/jobs/1")).toThrow("http or https");
expect(() => validateJobUrl("not a url")).toThrow("valid URL");
});

it("requires candidate name and valid email", () => {
expect(validateProfile(profile)).toEqual([]);
expect(validateProfile(normalizeProfile({ email: "bad" }))).toEqual([
"profile.fullName is required",
"profile.email must be a valid email address",
]);
});
});

describe("job page extraction", () => {
it("extracts title, company, apply link, form fields, and text", () => {
const job = extractJobPage(
`<!doctype html>
<title>Senior Product Engineer</title>
<meta property="og:site_name" content="Acme">
<h1>Senior Product Engineer</h1>
<p>Build customer-facing workflow systems.</p>
<a href="/apply">Apply now</a>
<form action="/apply" method="post">
<input name="full_name" placeholder="Full name" required>
<input name="email" type="email" required>
<textarea name="cover_letter" placeholder="Cover letter"></textarea>
</form>`,
"https://jobs.example.com/roles/123",
);

expect(job.title).toBe("Senior Product Engineer");
expect(job.company).toBe("Acme");
expect(job.applyUrl).toBe("https://jobs.example.com/apply");
expect(job.forms[0].action).toBe("https://jobs.example.com/apply");
expect(job.forms[0].fields.map((field) => field.name)).toEqual([
"full_name",
"email",
"cover_letter",
]);
expect(job.descriptionText).toContain("Build customer-facing workflow systems");
});

it("flags blockers for captcha, login, file upload, and password fields", () => {
const job = extractJobPage(
`<h1>Role</h1>
<p>Please sign in and complete recaptcha.</p>
<form><input name="resume" type="file"><input name="password" type="password"></form>`,
"https://example.com/job",
);

expect(job.blockers).toContain("Captcha detected.");
expect(job.blockers).toContain("Login or account creation may be required.");
expect(job.blockers).toContain("File upload fields require manual review.");
expect(job.blockers).toContain("Password fields require manual review.");
});
});

describe("application preparation and submission planning", () => {
it("maps candidate fields into a safe application form", () => {
const job = extractJobPage(
`<h1>Senior Product Engineer</h1>
<form action="/apply" method="post">
<input name="first_name" placeholder="First name">
<input name="last_name" placeholder="Last name">
<input name="email" type="email">
<input name="linkedin" placeholder="LinkedIn">
<textarea name="why_this_role" placeholder="Why this role?"></textarea>
</form>`,
"https://jobs.example.com/roles/123",
);
const draft = buildFallbackDraft(job, profile);

expect(mapProfileToFields(job.forms[0].fields, profile, draft, {})).toMatchObject({
first_name: "Sam",
last_name: "Candidate",
email: "sam@example.com",
linkedin: "https://linkedin.com/in/sam",
why_this_role: draft.coverLetter,
});
});

it("requires exact confirmation before building a submission request", () => {
const job = extractJobPage(
`<h1>Senior Product Engineer</h1>
<form action="/apply" method="post">
<input name="full_name">
<input name="email">
</form>`,
"https://jobs.example.com/roles/123",
);
const draft = buildFallbackDraft(job, profile);
const plan = buildSubmissionPlan("app_123", job, profile, draft, {});

expect(plan.ready).toBe(true);
expect(plan.confirmationPhrase).toBe("submit app_123");
expect(() => buildSubmissionRequest(job.url, plan, "yes")).toThrow("confirmation");
const request = buildSubmissionRequest(job.url, plan, "submit app_123");
expect(request.url).toBe("https://jobs.example.com/apply");
expect(request.init.method).toBe("POST");
expect(request.fields).toMatchObject({
full_name: "Sam Candidate",
email: "sam@example.com",
});
});

it("blocks automatic submission when only unsafe forms are present", () => {
const job = extractJobPage(
`<h1>Senior Product Engineer</h1><form action="/apply"><input name="resume" type="file"></form>`,
"https://jobs.example.com/roles/123",
);
const draft = buildFallbackDraft(job, profile);
const plan = buildSubmissionPlan("app_123", job, profile, draft, {});

expect(plan.ready).toBe(false);
expect(plan.blockers).toContain("File upload fields require manual review.");
expect(() => buildSubmissionRequest(job.url, plan, "submit app_123")).toThrow(
"File upload",
);
});
});
Loading
Loading