add-api-route
Add a new JobPilot API route or module the standard way - contracts schema, controller, service, response schema, optional web hook. Use for "add an endpoint",…
Search Upwork through the official Upwork MCP, smart-filter out low-quality clients, rank survivors by fit, and save them to the campaign as recommendations. Never submits a proposal.
$ npx -y skills add suxrobGM/jobpilot --skill upwork-search --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/upwork-searchContext preview
The summary Claude sees to decide when to auto-load this skill.
Search Upwork through the official Upwork MCP, smart-filter out low-quality clients, rank survivors by fit, and save them to the campaign as recommendations. Never submits a proposal.
name: upwork-search description: Search Upwork through the official Upwork MCP, smart-filter out low-quality clients, rank survivors by fit, and save them to the campaign as recommendations. Never submits a proposal. argument-hint: "<job_keywords> --board upwork.com [--max-jobs N] [--campaign <campaign-id>]"
Find Upwork jobs the user can win - qualify on fit **and** client quality, drop the junk, and save the keepers to the campaign for review. **Recommend only: never submit a proposal here.** The user drafts a proposal (the `upwork-proposal` skill, launched per job from the campaign page) and sends it with the `upwork-submit` skill.
1. Follow `../_shared/setup.md` (`$JOBPILOT_API` is injected by the terminal). 2. Follow `../_shared/upwork-mcp.md`: confirm the Upwork tools are connected and resolve `ORG_UID` once. Not connected → stop with the message that doc gives. There is no browser fallback. 3. Parse and strip the flags; the rest is the free-text query.
sets how many cursor round trips you make.
`source:"search"`, `status:"in_progress"` campaign on the query, else create one (a `source:"search"` create requires `config.resumeId` - default to the profile's `primaryResumeId`). 4. Resolve the board: `curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/job-boards" | jq '.[] | select(.domain=="upwork.com")'`. No row → abort: "Upwork is not configured. Add it on /boards." If a `--campaign` was given, first command it to `failed` with `POST /api/campaigns/<id>/status {"status":"failed"}`.
Extract the role and skills, and the preferences the search can filter on directly: hourly vs fixed, budget or rate floor, client location, weekly hours, project length. If vague, ask before searching.
Push every preference you extracted into the search parameters rather than filtering afterwards. Always set `verified_payment_only: true` and `proposals_max: 49` - both are hard skips downstream, so paying for those rows wastes a page.
Two sources, merged into one pool and deduped on job id.
1. **Recommendations** - `find_jobs` action `smart_search`. This is Upwork's own recommender and already knows the profile, so pass no query and no skills, only the hard filters from Phase 1. Use `mode: "best_match"`. Add `days_posted` when the user asked for fresh work. 2. **Keyword search** - `find_jobs` action `search`. Use `title` (1-3 words) when the user named a role, since every returned job then carries those words in its title. Use `query` only for a broad topic. They cannot be combined, and `sort: "relevance"` silently drops `title`, `skills` and `category`, so leave `sort` unset.
Split `--max-jobs` across the two sources. Page with the cursor only while `hasNextPage` or `hasMore` is true, repeating the same filters each time.
Location: pass `location` (a country or region name, e.g. `"United States"`) when the user wants it. A value outside Upwork's vocabulary returns an empty page rather than an error, so on zero results read `empty_result_note` and retry with the spelling it names.
Drop any row whose `applied` or `is_applied` is true - the user already applied through Upwork.
Run the applied-check (`../_shared/campaign-flow.md`) with the client name as the company. `.applied` → record the default already-applied skip, then skip the rest.
Score the client from the **search row alone**, before opening the posting. Every hard rule reads a row field, so a client that fails one costs a single call instead of a full job description pulled into context.
CLIENT='{ "paymentVerified": true, "totalSpent": 12000, "rating": 4.9,
"reviewsCount": 24, "proposalsCount": 7, "postedHoursAgo": 6, "jobType": "hourly" }'
QUALITY=$(curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X POST "$JOBPILOT_API/api/upwork/client-quality" \
-H 'content-type: application/json' -d "$(jq -n --argjson c "$CLIENT" '{client:$c}')")
CLIENT_VERDICT=$(echo "$QUALITY" | jq -r '.verdict') # good | caution | skipField sources: `paymentVerified` from `verification_status`, `totalSpent` from `total_spent`, `reviewsCount` from `total_reviews`, `proposalsCount` from `proposal_count`, `postedHoursAgo` computed from `created_date` or `published_date`. Omit any you cannot read - every field is optional and a missing one degrades to neutral. `rating` is the score **freelancers gave this client**, so a low one is a warning about the client, not a sign they are unsuccessful.
The scorer hard-skips unverified payment, 50+ proposals, and unproven-plus-unverified clients. If `CLIENT_VERDICT == "skip"`, create the Job as `pending`, record `.skipReason` through `/jobs/<key>/result`, and move on - don't open the posting and don't score fit.
Survivors only. Call `find_jobs` action `get` with the row's numeric `id` for three things: the full description, `client_record` (the hire count the search row cannot show), and `connects_cost`, which the proposal step needs later. Keep only the fields the digest and the client block use - do not carry the whole payload forward.
Add `clientHires` from `client_record` to `CLIENT` and score once more. The hire count is 20% of the score, so this second verdict is the one to trust and its `qualityScore` the one to save. A `skip` here is the soft floor rather than a hard rule; handle it exactly as above.
Build the digest (`../_shared/digest-schema.md`) from the posting you already fetched; always populate `skills`. Score inline - the MCP returns the full descri
An AI agent that applies to jobs for you, on the Claude or Codex subscription you already have.
Repo: suxrobGM/jobpilot
Add a new JobPilot API route or module the standard way - contracts schema, controller, service, response schema, optional web hook. Use for "add an endpoint",…
Review and clean up the given file(s)/folder(s)/module(s): rate organization, find dead code, duplication, coupling, over-engineering, deep nesting, structural…
Create and apply a Prisma migration safely against the remote (tunneled) PostgreSQL. Use for schema changes - "create a migration", "apply migrations", "add a…
Add knip to a TypeScript project and use it to remove dead exports, collapse pass-through barrels, and narrow every export to what another file actually…
Bump the unified JobPilot version (host + plugin), update the changelog, commit, and tag a new release
Rebuild the .NET terminal host, restart it, and wait for /healthz. Use after any C# change under apps/terminal, or when asked to "restart the terminal host".