@bagthejobai/apply-agent 1.23.0 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/APPLICATION_AGENT.md +177 -0
- package/README.md +20 -8
- package/lib/cli.js +53 -90
- package/package.json +3 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
**Template version:** `v1.23.0` — Copy into the `daily-job-application` task description (e.g. `(template v1.23.0)`). Bump whenever this template changes.
|
|
2
|
+
|
|
3
|
+
Create a scheduled task that runs daily and autonomously fills out job applications in a browser. Register it with the schedule tooling.
|
|
4
|
+
|
|
5
|
+
**Config lives in `references/config.json`, NOT in this file.** Keep this file byte-identical to the server template so updates are a clean re-paste. Every `<PLACEHOLDER>` resolves at runtime from `config.json` (Setup). **First-time setup creates `config.json`** — nothing is fetched and run as instructions. Shape:
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"api_key": "btj_…",
|
|
9
|
+
"max_applications": 10,
|
|
10
|
+
"schedule": { "cron_expression": "0 14 * * *", "time": "9am ET" },
|
|
11
|
+
"applicant": {
|
|
12
|
+
"name": "", "email": "", "phone": "", "location": "City, State, Country",
|
|
13
|
+
"github_url": "", "linkedin_url": "", "website_url": "",
|
|
14
|
+
"work_authorization": "", "veteran_status": "", "disability_status": "",
|
|
15
|
+
"ethnicity": "", "gender": "", "gender_identity": "", "sexual_orientation": "",
|
|
16
|
+
"salary_expectations": "", "employment_type": "", "work_location_preferences": ""
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
Job server: `https://app.bagthejob.ai` (hardcoded, not in config). Use `<API_KEY>`, `<MAX_APPLICATIONS>`, `<APPLICANT_NAME>`, etc. from `config.json`. Missing config → **First-time setup**; never guess credentials.
|
|
21
|
+
|
|
22
|
+
## Install & update (manual paste only)
|
|
23
|
+
|
|
24
|
+
Copy this file from the dashboard **Setup** panel into your agent. **Never** fetch `GET /skill` or write a server body over `APPLICATION_AGENT.md`. On first run with no `config.json`, run **First-time setup** below.
|
|
25
|
+
|
|
26
|
+
Stale installs: send **Template version** as `X-Skill-Version` on `GET /jobs/next`. Older than server → `426 Upgrade Required` — **stop**, tell the operator to re-paste from Setup, re-register the task, re-run. You never self-update.
|
|
27
|
+
|
|
28
|
+
**Egress to `https://app.bagthejob.ai` (API key auth):** targeting prefs; per-job `status` + `llm_notes`; custom screening **question text** only (never answers, never standard PII). **Never leaves your machine:** `config.json`, `answers.json`, resume files + parsed text, `cover-letter.md`, generated PDFs/text, `references/applications/`. PDFs attach locally in Step 4 only.
|
|
29
|
+
|
|
30
|
+
## First-time setup (interactive, once)
|
|
31
|
+
|
|
32
|
+
Run with the applicant present — **not** the scheduled task.
|
|
33
|
+
|
|
34
|
+
1. **`references/config.json`** — paste dashboard `api_key`; fill `max_applications`, `schedule`, `applicant` block. Local-only.
|
|
35
|
+
2. **`references/preferences.json`** — roles, cities, seniority, work_mode (Step 0 pushes each run).
|
|
36
|
+
3. **Resume** — `references/resume.{md,pdf,docx,txt,doc}` (any one; no conversion required). Local-only. Without it: apply still works but resume field blank, no PDFs.
|
|
37
|
+
4. **`references/answers.json`** — screening Q&A in applicant voice: `[{ "question", "answer", "category"? }]`. Find via `find /sessions/*/mnt/.skills/skills/job-application-assistant/references/answers.json 2>/dev/null | head -1`. Local-only.
|
|
38
|
+
5. **Optional `references/cover-letter.md`** — voice/tone source for generated letters/answers; **not** a facts source. Local-only.
|
|
39
|
+
6. **Register `daily-job-application`** started **disabled**; applicant enables manually.
|
|
40
|
+
|
|
41
|
+
**Task:** Name `daily-job-application`; Description `"Autonomously fill out job applications for <APPLICANT_NAME> in a loop"`; Schedule daily `<TIME>` (`<CRON_EXPRESSION>`) with jitter; Enabled: false initially.
|
|
42
|
+
|
|
43
|
+
**Task prompt / SKILL body** (fill placeholders):
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
You are an autonomous job application agent for <APPLICANT_NAME>. Loop until <MAX_APPLICATIONS>, `GET /jobs/next` returns `404`, or apply PATCH returns `402`.
|
|
48
|
+
|
|
49
|
+
Pre-authorized — **do not ask permission** for: navigating application URLs; filling fields; pasting cover letters; `file_upload` on resume/CV **only when** Step 3b set `documents_generated: true`; browser tools; PATCH/POST to the job server; new tabs. Never pause with "Should I proceed?"
|
|
50
|
+
|
|
51
|
+
**Forbidden:** Submit; closing tabs (`tabs_close_mcp`); reusing tabs across jobs; any resume/CV fill/upload/paste unless `documents_generated: true` (then `file_upload` Step 3b resume PDF only — never paste resume text).
|
|
52
|
+
|
|
53
|
+
## Preconditions (before Setup or API calls)
|
|
54
|
+
|
|
55
|
+
Requires **Claude in Chrome** in the **Claude desktop app** — the only path to fill forms (Step 4). Verify connector tools (e.g. `tabs_create_mcp`) exist; if not, **stop immediately** (no claims, no API). Step 3 ATS JSON is a fallback within a connector-enabled run only.
|
|
56
|
+
|
|
57
|
+
## Setup
|
|
58
|
+
|
|
59
|
+
1. Read `references/config.json` (`api_key`, `max_applications`, `applicant`). Missing `api_key` → stop; complete First-time setup.
|
|
60
|
+
2. Invoke skill `job-application-assistant` (tone, STAR answers).
|
|
61
|
+
3. **Resume:** `find /sessions/*/mnt/.skills/skills/job-application-assistant/references/resume.* 2>/dev/null` — precedence `md` > `pdf` > `docx` > `txt` > `doc`. Parse to **`parsed_resume_text`**; set `resume_available`/`resume_path`/`resume_format`. Parse: md/txt direct; pdf via Read tool; docx `unzip -p <path> word/document.xml` + strip tags; doc best-effort. Unusable → `resume_available: false`, note in `llm_notes`, never fabricate. Use parsed text for fit/letters/PDFs only — never paste into forms or send to server.
|
|
62
|
+
4. **Answer bank:** read `answers.json` (path find above). Verbatim match first (Step 4).
|
|
63
|
+
5. **Personality letter:** read `cover-letter.md` if present → `personality_letter_text` / `personality_letter_available`. Voice only — facts in letter are not resume facts.
|
|
64
|
+
6. `tabs_context_mcp` (createIfEmpty: true).
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
Base `https://app.bagthejob.ai`, `Authorization: Bearer <API_KEY>`. `401` without key. Only `"status": "applied"` PATCH is billable; `402` on apply when quota+credits exhausted → hard loop-stop. Optional `GET /me` for `remaining` / `promo_credits_remaining`; if both are `0`, stop before claiming.
|
|
69
|
+
|
|
70
|
+
## Applicant fields (`config.json` → form)
|
|
71
|
+
|
|
72
|
+
`name` · `email` · `phone` · `location` · `github_url` · `linkedin_url` · `website_url` · `work_authorization` · `veteran_status` · `disability_status` · `ethnicity` · `gender` · `gender_identity` · `sexual_orientation` · `salary_expectations` · `employment_type` · `work_location_preferences`. Empty EEO fields → blank or "prefer not to answer".
|
|
73
|
+
|
|
74
|
+
## Step 0: Sync targeting (once per run)
|
|
75
|
+
|
|
76
|
+
Push roles, cities, seniority, work_mode before the loop. **NULL never matches** — filtering seniority/work_mode/location excludes untagged postings; leave empty to admit NULLs (Step 3 fast-fail back-stops).
|
|
77
|
+
|
|
78
|
+
**`references/preferences.json`** is source of truth, except dashboard role edits: `GET /me/preferences` returns `source`. `"dashboard"` → adopt **roles only**, keep local cities/seniority/work_mode, PUT merged set. `"agent"` → use local file.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
GET https://app.bagthejob.ai/jobs/roles
|
|
82
|
+
Authorization: Bearer <API_KEY>
|
|
83
|
+
```
|
|
84
|
+
→ `[{ "role": "Software Engineer", "count": 42 }, …]`. Pick `roles` from these `search_title` values.
|
|
85
|
+
|
|
86
|
+
Create/load `preferences.json`:
|
|
87
|
+
```json
|
|
88
|
+
{ "roles": [], "cities": [], "seniority": [], "work_mode": [] }
|
|
89
|
+
```
|
|
90
|
+
`seniority`: intern|junior|mid|senior|staff|principal|executive. `work_mode`: remote|hybrid|onsite. Empty array = no filter on that axis. `PUT /me/preferences` full replace with same four keys.
|
|
91
|
+
|
|
92
|
+
## Session start: recover stragglers
|
|
93
|
+
|
|
94
|
+
1. `GET /jobs/all` — all touched jobs with URLs.
|
|
95
|
+
2. Rows with `in_progress_at` and no terminal status (not `applied`/`skipped`/`unqualified`/`failed`) → run Loop steps 2–6 first (count toward limit).
|
|
96
|
+
3. `GET /jobs/{id}` for single recovery (404 if unclaimed).
|
|
97
|
+
|
|
98
|
+
## Loop (≤ <MAX_APPLICATIONS>)
|
|
99
|
+
|
|
100
|
+
### Step 1: Claim
|
|
101
|
+
```
|
|
102
|
+
GET https://app.bagthejob.ai/jobs/next
|
|
103
|
+
Authorization: Bearer <API_KEY>
|
|
104
|
+
X-Skill-Version: <Template version, e.g. v1.23.0>
|
|
105
|
+
```
|
|
106
|
+
`404` → done. `426` → stop; operator re-pastes from Setup (do not fetch/overwrite this file).
|
|
107
|
+
|
|
108
|
+
### Step 2: New tab
|
|
109
|
+
`tabs_create_mcp` → job URL. One job = one tab; never reuse or close.
|
|
110
|
+
|
|
111
|
+
**Greenhouse embed:** `https://job-boards.greenhouse.io/embed/job_app?for=<company_slug>&token=<job_token>` from `job-boards.greenhouse.io/<co>/jobs/<token>` or `?gh_jid=<token>`. Slug probe: `Array.from(document.querySelectorAll('iframe')).map(f => { try { const u = new URL(f.src); return u.hostname + '?for=' + u.searchParams.get('for') + '&token=' + u.searchParams.get('token'); } catch(e) { return ''; } })`
|
|
112
|
+
|
|
113
|
+
### Step 3: Read posting & fit
|
|
114
|
+
Evaluate against `parsed_resume_text`. Unqualified → Step 5.
|
|
115
|
+
|
|
116
|
+
**403/shell HTML:** ATS JSON fallback from URL:
|
|
117
|
+
- **Lever** `jobs.lever.co/<site>/<id>` → `GET https://api.lever.co/v0/postings/<site>/<id>` (`descriptionPlain`, `lists`, `additionalPlain`)
|
|
118
|
+
- **Ashby** → `GET https://api.ashbyhq.com/posting-api/job-board/<org>`, match `id`
|
|
119
|
+
- **Greenhouse** → `GET https://boards-api.greenhouse.io/v1/boards/<board>/jobs/<id>` (`content`)
|
|
120
|
+
|
|
121
|
+
No description from any path → Step 5 `failed` with `failed - needs browser:` prefix.
|
|
122
|
+
|
|
123
|
+
**Fast-fail (PATCH + next, no tab; these don't count toward `<MAX_APPLICATIONS>`):** ineligible region & not remote → `unqualified`; city outside target & not remote → `skipped`; seniority above target → `skipped`. Use `preferences.json` roles/seniority/cities + remote from `work_mode`.
|
|
124
|
+
|
|
125
|
+
### Step 3b: Local PDFs (if `resume_available`)
|
|
126
|
+
After fit passes, before Step 4. Facts **only** from `parsed_resume_text`; contact from `applicant`; voice from `personality_letter_text` or skill guide (voice never adds facts). Cover letter: 3–4 paragraphs + signature. HTML→PDF in-agent, no network. Write `<references>/applications/<job_id>/{LastName}-Resume-{Company}.pdf` and `…-CoverLetter-{Company}.pdf`; stash paths. `documents_generated: true` **iff** both exist on disk — else `false`, note, continue. Overwrite on re-run. Crash-safe interim `applications/<job_id>.json` with paths + flag (non-fatal if write fails).
|
|
127
|
+
|
|
128
|
+
### Step 4: Fill form
|
|
129
|
+
Greenhouse: Step 2 embed URL. Fill contact + screening. Work-auth per `applicant`; target-region location → Yes + applicant city. Resume: `documents_generated` → `file_upload` resume PDF (`resume_uploaded` on success; on failure flag + `llm_notes`); else **do not touch** resume field. Cover letter: required **or** optional — always fill; prefer PDF `file_upload` when `documents_generated`, else paste tailored text. Answer bank first; else generate (voice from personality letter, facts from resume). **Do not Submit.**
|
|
130
|
+
|
|
131
|
+
**Field-type handling (Greenhouse):** **Dropdowns / react-select** (EEO — gender, ethnicity, veteran, disability — country, any `▼`-arrow widget): open the dropdown and click the option, as a user would. Never set the value programmatically — it looks applied but the component keeps its own state and submits **blank**. **Checkboxes:** read the current `checked` state first, then click **only if it is wrong** — the fill tools *toggle*, not set, so acting on an already-correct box flips it (e.g. unchecks a consent box that was already checked).
|
|
132
|
+
|
|
133
|
+
### Step 4b: POST custom questions
|
|
134
|
+
Every non-standard custom question, apply or skip:
|
|
135
|
+
```
|
|
136
|
+
POST https://app.bagthejob.ai/questions
|
|
137
|
+
Authorization: Bearer <API_KEY>
|
|
138
|
+
{ "job_id": <id>, "question": "<text>" }
|
|
139
|
+
```
|
|
140
|
+
**Skip posting:** name, email, phone, resume, cover letter, LinkedIn, GitHub, website, work auth, visa, location, salary, start date, EEO, referral source.
|
|
141
|
+
|
|
142
|
+
### Step 5: PATCH
|
|
143
|
+
```
|
|
144
|
+
PATCH https://app.bagthejob.ai/jobs/{id}/apply
|
|
145
|
+
{ "status": "<status>", "llm_notes": "<notes>" }
|
|
146
|
+
```
|
|
147
|
+
Body is only `status` + `llm_notes` (PDFs stay local).
|
|
148
|
+
|
|
149
|
+
- **`applied`** — filled, awaiting applicant submit (billable, shown **Applied**). Note resume attached or action required.
|
|
150
|
+
- **`failed`** — broken form or no description; unrenderable notes start **`failed - needs browser:`**
|
|
151
|
+
- **`unqualified`** / **`skipped`** — with reason. (The prior `"ready"` name still maps to `applied`.) Omitted `status` inferred from note prefix (unknown → `skipped`). `402` → stop loop.
|
|
152
|
+
|
|
153
|
+
### Step 5b: Local record (never sent)
|
|
154
|
+
Write `<references>/applications/<job_id>.json` after PATCH for every touched job. Non-fatal on failure. Shape:
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"job_id": 1234, "title": "", "company": "", "url": "", "application_url": "",
|
|
158
|
+
"snippet": "", "search_title": "", "status": "", "llm_notes": "",
|
|
159
|
+
"applied_at": "", "processed_at": "", "browser_tab": "",
|
|
160
|
+
"resume_action_required": true, "resume_uploaded": false,
|
|
161
|
+
"resume_format": null, "resume_pdf_path": null, "cover_letter_pdf_path": null,
|
|
162
|
+
"cover_letter": null, "documents_generated": false,
|
|
163
|
+
"form_fields": { "name": "", "email": "", "eeo": {} },
|
|
164
|
+
"screening_answers": [{ "question": "", "answer": "", "source": "answer_bank|generated" }],
|
|
165
|
+
"custom_questions": [], "fit_assessment": "", "flags": [],
|
|
166
|
+
"agent_run_id": "daily-job-application", "template_version": "v1.22.0",
|
|
167
|
+
"api_base_url": "https://app.bagthejob.ai"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
`documents_generated: true` only when both PDF paths exist on disk. `resume_action_required` is `false` only when `resume_uploaded` is `true`; otherwise `true` (flag the upload + any un-fillable item). `template_version` matches this file.
|
|
171
|
+
|
|
172
|
+
### Step 6: Confirm `applied_at`, then Step 1.
|
|
173
|
+
|
|
174
|
+
## Rules
|
|
175
|
+
- PATCH every touched job; POST every custom question; local record after each PATCH.
|
|
176
|
+
- **Source separation:** career facts → `parsed_resume_text` only; contact/logistics → `applicant`/`answers.json`; voice → `personality_letter_text`. Never invent facts.
|
|
177
|
+
- Never Submit; never close/reuse tabs; stop on limit, `404`, or `402`.
|
package/README.md
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
# @bagthejobai/apply-agent
|
|
2
2
|
|
|
3
|
-
A tiny command-line helper that
|
|
3
|
+
A tiny command-line helper that copies the [BagTheJob.ai](https://app.bagthejob.ai) application-agent skill to your clipboard so you can paste it into the Claude desktop app.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @bagthejobai/apply-agent setup # first-time install
|
|
7
|
-
npx @bagthejobai/apply-agent update #
|
|
7
|
+
npx @bagthejobai/apply-agent update # copy the latest to re-paste
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- Copies the **bundled** skill (`APPLICATION_AGENT.md`, shipped inside this package) to your clipboard — nothing is downloaded.
|
|
13
|
+
- Clipboard support: macOS `pbcopy`, Windows `clip`, Linux `wl-copy`/`xclip`/`xsel`.
|
|
14
14
|
- Prints short instructions for reviewing and pasting it.
|
|
15
15
|
|
|
16
|
+
`npx @bagthejobai/apply-agent@latest` always pulls the newest published skill.
|
|
17
|
+
|
|
16
18
|
## What it deliberately does NOT do
|
|
17
19
|
|
|
18
|
-
This is a **
|
|
20
|
+
This is a **paste helper, not an installer.** By design (see the project's issue #159 security model):
|
|
19
21
|
|
|
20
22
|
- It never writes the skill to a file on your machine.
|
|
21
23
|
- It never creates or edits `config.json`, `answers.json`, or your resume.
|
|
22
24
|
- It never registers a scheduled task or runs anything on your behalf.
|
|
23
25
|
|
|
24
|
-
**You** are the install step: review the skill you copied, then paste it into the Claude desktop app yourself. That human review is the security gate — nothing is
|
|
26
|
+
**You** are the install step: review the skill you copied, then paste it into the Claude desktop app yourself. That human review is the security gate — nothing is downloaded and silently executed.
|
|
25
27
|
|
|
26
28
|
## Options
|
|
27
29
|
|
|
28
30
|
| Flag | Meaning |
|
|
29
31
|
|------|---------|
|
|
30
|
-
| `--base-url <url>` | Fetch from another server (default `https://app.bagthejob.ai`). Also settable via `BTJ_BASE_URL`. |
|
|
31
32
|
| `--stdout` | Print the skill to the terminal instead of using the clipboard (headless/CI). |
|
|
32
33
|
| `-h`, `--help` | Show help. |
|
|
33
34
|
| `-v`, `--version` | Show the CLI version. |
|
|
@@ -36,4 +37,15 @@ If no clipboard tool is available, the CLI automatically prints the full skill b
|
|
|
36
37
|
|
|
37
38
|
## Versioning
|
|
38
39
|
|
|
39
|
-
The
|
|
40
|
+
The skill is **bundled** in the package, so it works offline and never depends on a server being up. The package's **major.minor** tracks the skill's `Template version` (e.g. `1.23.x` for skill `v1.23.0`), so the number you install signals which skill release it ships; the patch digit is free for CLI-only fixes. To get the newest skill, install `@latest`.
|
|
41
|
+
|
|
42
|
+
`APPLICATION_AGENT.md` here is a copy of the skill; the runtime source of truth (and the `426` staleness gate) lives in the BagTheJob server. The two are kept in sync at release time — the publish workflow asserts the bundled skill's version matches the release.
|
|
43
|
+
|
|
44
|
+
## Releasing
|
|
45
|
+
|
|
46
|
+
Publishing is automated by `.github/workflows/publish.yml`. To cut a release:
|
|
47
|
+
|
|
48
|
+
1. Update the bundled `APPLICATION_AGENT.md` to the current skill and set `package.json` `version` to match its `Template version` (without the `v`); merge to `main`.
|
|
49
|
+
2. Publish a **GitHub Release** tagged `v<version>` (e.g. `v1.23.1`).
|
|
50
|
+
|
|
51
|
+
The workflow refuses to publish unless the release tag matches `package.json` and the package's major.minor matches the **bundled** skill's `Template version`, then runs `npm publish` with the `NPM_TOKEN` repo secret (an npm **Automation** token, so it bypasses account 2FA). Patch releases (e.g. `1.23.0` → `1.23.1`) are available for CLI-only fixes within the same skill line.
|
package/lib/cli.js
CHANGED
|
@@ -1,81 +1,55 @@
|
|
|
1
|
-
// @bagthejobai/apply-agent — a
|
|
2
|
-
//
|
|
1
|
+
// @bagthejobai/apply-agent — a paste helper for the BagTheJob application-agent
|
|
2
|
+
// skill.
|
|
3
3
|
//
|
|
4
|
-
// What it does:
|
|
5
|
-
// your clipboard, then prints instructions for you to review
|
|
6
|
-
// the Claude desktop app yourself.
|
|
4
|
+
// What it does: copies the bundled skill (APPLICATION_AGENT.md, shipped inside
|
|
5
|
+
// this package) to your clipboard, then prints instructions for you to review
|
|
6
|
+
// and paste it into the Claude desktop app yourself.
|
|
7
7
|
//
|
|
8
8
|
// What it deliberately does NOT do (issue #159): it never writes the skill to a
|
|
9
9
|
// file, never creates `config.json`, and never registers a scheduled task. The
|
|
10
10
|
// human reviewing and pasting the skill is the security gate; this tool only
|
|
11
|
-
// moves text to your clipboard.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// moves text to your clipboard.
|
|
12
|
+
//
|
|
13
|
+
// The skill is bundled (not fetched): the package version tracks the skill's
|
|
14
|
+
// Template version, so `@bagthejobai/apply-agent@1.23.x` always ships skill
|
|
15
|
+
// v1.23.0. `npx @bagthejobai/apply-agent@latest` gets the newest.
|
|
14
16
|
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
15
19
|
import { spawn } from "node:child_process";
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
// The bundled skill sits at the package root, one level up from lib/.
|
|
22
|
+
const SKILL_PATH = fileURLToPath(new URL("../APPLICATION_AGENT.md", import.meta.url));
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export function resolveBaseUrl({ flag, env } = {}) {
|
|
23
|
-
const raw = (flag ?? env ?? DEFAULT_BASE_URL).trim();
|
|
24
|
-
const chosen = raw === "" ? DEFAULT_BASE_URL : raw;
|
|
25
|
-
return chosen.replace(/\/+$/, "");
|
|
26
|
-
}
|
|
24
|
+
const DASHBOARD_URL = "https://app.bagthejob.ai/dashboard";
|
|
25
|
+
const BEGIN_MARK = "─── BEGIN SKILL ───";
|
|
26
|
+
const END_MARK = "─── END SKILL ───";
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.name = "SkillFetchError";
|
|
34
|
-
}
|
|
28
|
+
// parseSkillVersion pulls the `**Template version:** `vX.Y.Z`` header out of the
|
|
29
|
+
// skill markdown. Returns null when the line is missing/malformed.
|
|
30
|
+
export function parseSkillVersion(content) {
|
|
31
|
+
const m = content.match(/^\*\*Template version:\*\*\s*`(v[0-9]+\.[0-9]+\.[0-9]+)`/m);
|
|
32
|
+
return m ? m[1] : null;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
const fail = (reason) =>
|
|
43
|
-
new SkillFetchError(
|
|
44
|
-
`Could not fetch the skill from ${url}: ${reason}. ` +
|
|
45
|
-
`Check your connection, or pass --base-url for a local server.`,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
let res;
|
|
35
|
+
// loadBundledSkill reads the packaged skill and its Template version. `read` is
|
|
36
|
+
// injectable for tests; it defaults to reading the bundled file. Throws a
|
|
37
|
+
// human-facing Error if the skill is missing or has no Template version line.
|
|
38
|
+
export function loadBundledSkill(read = () => readFileSync(SKILL_PATH, "utf8")) {
|
|
39
|
+
let content;
|
|
49
40
|
try {
|
|
50
|
-
|
|
51
|
-
method: "GET",
|
|
52
|
-
headers: { accept: "application/json" },
|
|
53
|
-
});
|
|
41
|
+
content = read();
|
|
54
42
|
} catch (err) {
|
|
55
|
-
throw
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw fail(`server returned HTTP ${res.status}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let body;
|
|
63
|
-
try {
|
|
64
|
-
body = await res.json();
|
|
65
|
-
} catch {
|
|
66
|
-
// A reverse proxy or captive portal can answer 200 with an HTML error page.
|
|
67
|
-
throw fail("response was not valid JSON (unexpected server or proxy page)");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const version = body?.version;
|
|
71
|
-
const content = body?.content;
|
|
72
|
-
if (typeof version !== "string" || version.trim() === "") {
|
|
73
|
-
throw fail("response is missing a 'version' field");
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Could not read the bundled skill (${err?.message || "read error"}). ` +
|
|
45
|
+
`Reinstall with: npx @bagthejobai/apply-agent@latest`,
|
|
46
|
+
);
|
|
74
47
|
}
|
|
75
|
-
|
|
76
|
-
|
|
48
|
+
const version = parseSkillVersion(content);
|
|
49
|
+
if (typeof content !== "string" || content.trim() === "" || !version) {
|
|
50
|
+
throw new Error("Bundled skill is missing or malformed (no Template version line).");
|
|
77
51
|
}
|
|
78
|
-
return { version
|
|
52
|
+
return { version, content };
|
|
79
53
|
}
|
|
80
54
|
|
|
81
55
|
// clipboardCommandFor returns an ordered list of candidate clipboard commands
|
|
@@ -122,24 +96,19 @@ function trySpawnCopy([cmd, ...args], text) {
|
|
|
122
96
|
});
|
|
123
97
|
}
|
|
124
98
|
|
|
125
|
-
const DASHBOARD_URL = "/dashboard";
|
|
126
|
-
const BEGIN_MARK = "─── BEGIN SKILL ───";
|
|
127
|
-
const END_MARK = "─── END SKILL ───";
|
|
128
|
-
|
|
129
99
|
// instructionsFor builds the human-facing message (pure — no I/O). On clipboard
|
|
130
100
|
// success the skill content is NOT echoed (it's already on the clipboard); on
|
|
131
101
|
// fallback the full content is fenced between BEGIN/END markers so the user can
|
|
132
102
|
// select it manually. The instructions always lead with "review the skill" —
|
|
133
103
|
// the #159 gate — and never reference any local file the tool would write.
|
|
134
|
-
export function instructionsFor(command, { version, copied, content,
|
|
135
|
-
const origin = baseUrl ?? DEFAULT_BASE_URL;
|
|
104
|
+
export function instructionsFor(command, { version, copied, content, stdout }) {
|
|
136
105
|
const lines = [];
|
|
137
106
|
const isUpdate = command === "update";
|
|
138
107
|
|
|
139
108
|
lines.push(
|
|
140
109
|
isUpdate
|
|
141
|
-
? `Latest skill Template version ${version}
|
|
142
|
-
: `
|
|
110
|
+
? `Latest bundled skill is Template version ${version}`
|
|
111
|
+
: `Bundled skill Template version ${version}`,
|
|
143
112
|
);
|
|
144
113
|
lines.push(
|
|
145
114
|
copied
|
|
@@ -164,44 +133,42 @@ export function instructionsFor(command, { version, copied, content, baseUrl, st
|
|
|
164
133
|
lines.push(" 3. Paste this skill over the existing task description — a clean replace.");
|
|
165
134
|
lines.push(" Your local config in references/ is untouched.");
|
|
166
135
|
lines.push(" 4. Re-register the task, then re-run it.");
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push("Tip: run `npx @bagthejobai/apply-agent@latest update` to ensure the newest skill.");
|
|
167
138
|
} else {
|
|
168
139
|
lines.push("This tool installs nothing. You are the install step:");
|
|
169
140
|
lines.push(" 1. Review the skill you just copied — you should always know what your agent runs.");
|
|
170
141
|
lines.push(" 2. Open the Claude desktop app (with its Chrome connector enabled).");
|
|
171
142
|
lines.push(" 3. Paste the skill into a new `daily-job-application` scheduled-task description.");
|
|
172
143
|
lines.push(` 4. On first run, Claude walks you through setup and asks for your API key`);
|
|
173
|
-
lines.push(` (create one on your dashboard: ${
|
|
144
|
+
lines.push(` (create one on your dashboard: ${DASHBOARD_URL}).`);
|
|
174
145
|
}
|
|
175
146
|
return lines.join("\n");
|
|
176
147
|
}
|
|
177
148
|
|
|
178
|
-
const USAGE = `apply-agent —
|
|
149
|
+
const USAGE = `apply-agent — copy the BagTheJob application-agent skill to your clipboard.
|
|
179
150
|
|
|
180
151
|
Usage:
|
|
181
|
-
npx @bagthejobai/apply-agent setup
|
|
182
|
-
npx @bagthejobai/apply-agent update
|
|
152
|
+
npx @bagthejobai/apply-agent setup Copy the bundled skill for first-time install
|
|
153
|
+
npx @bagthejobai/apply-agent update Copy the bundled skill to re-paste an update
|
|
183
154
|
|
|
184
155
|
Options:
|
|
185
|
-
--base-url <url> Override the server (default: ${DEFAULT_BASE_URL}; or set BTJ_BASE_URL)
|
|
186
156
|
--stdout Print the skill instead of using the clipboard (headless/CI)
|
|
187
157
|
-h, --help Show this help
|
|
188
158
|
-v, --version Show the CLI version
|
|
189
159
|
|
|
190
|
-
|
|
191
|
-
|
|
160
|
+
The skill is bundled in this package — nothing is downloaded. This tool never
|
|
161
|
+
writes files, never stores credentials, and never registers a task. It only
|
|
162
|
+
puts the skill on your clipboard for you to review and paste.`;
|
|
192
163
|
|
|
193
164
|
// parseArgs is a tiny flag parser (pure) — no dependency on a parsing library.
|
|
194
165
|
export function parseArgs(argv) {
|
|
195
|
-
const opts = { command: undefined,
|
|
196
|
-
for (
|
|
197
|
-
const a = argv[i];
|
|
166
|
+
const opts = { command: undefined, stdout: false, help: false, version: false };
|
|
167
|
+
for (const a of argv) {
|
|
198
168
|
if (a === "--help" || a === "-h") opts.help = true;
|
|
199
169
|
else if (a === "--version" || a === "-v") opts.version = true;
|
|
200
170
|
else if (a === "--stdout") opts.stdout = true;
|
|
201
|
-
else if (a === "--base-url") opts.baseUrlFlag = argv[++i];
|
|
202
|
-
else if (a.startsWith("--base-url=")) opts.baseUrlFlag = a.slice("--base-url=".length);
|
|
203
171
|
else if (!a.startsWith("-") && opts.command === undefined) opts.command = a;
|
|
204
|
-
else opts.command = opts.command ?? a;
|
|
205
172
|
}
|
|
206
173
|
return opts;
|
|
207
174
|
}
|
|
@@ -211,12 +178,11 @@ export function parseArgs(argv) {
|
|
|
211
178
|
export async function run(
|
|
212
179
|
argv,
|
|
213
180
|
{
|
|
214
|
-
|
|
181
|
+
skillLoader = loadBundledSkill,
|
|
215
182
|
platform = process.platform,
|
|
216
|
-
env = process.env,
|
|
217
183
|
out = (s) => process.stdout.write(s + "\n"),
|
|
218
184
|
err = (s) => process.stderr.write(s + "\n"),
|
|
219
|
-
pkgVersion = "0.
|
|
185
|
+
pkgVersion = "0.0.0",
|
|
220
186
|
} = {},
|
|
221
187
|
) {
|
|
222
188
|
const opts = parseArgs(argv);
|
|
@@ -235,11 +201,9 @@ export async function run(
|
|
|
235
201
|
return 1;
|
|
236
202
|
}
|
|
237
203
|
|
|
238
|
-
const baseUrl = resolveBaseUrl({ flag: opts.baseUrlFlag, env: env.BTJ_BASE_URL });
|
|
239
|
-
|
|
240
204
|
let skill;
|
|
241
205
|
try {
|
|
242
|
-
skill =
|
|
206
|
+
skill = skillLoader();
|
|
243
207
|
} catch (e) {
|
|
244
208
|
err(e.message);
|
|
245
209
|
return 1;
|
|
@@ -255,7 +219,6 @@ export async function run(
|
|
|
255
219
|
version: skill.version,
|
|
256
220
|
copied,
|
|
257
221
|
content: skill.content,
|
|
258
|
-
baseUrl,
|
|
259
222
|
stdout: opts.stdout,
|
|
260
223
|
}),
|
|
261
224
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bagthejobai/apply-agent",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "Fetch the latest BagTheJob application-agent skill to your clipboard for manual paste into Claude. Fetch-and-paste helper only — never installs, writes, or registers anything.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
|
+
"APPLICATION_AGENT.md",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
@@ -18,8 +19,7 @@
|
|
|
18
19
|
"homepage": "https://app.bagthejob.ai",
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/
|
|
22
|
-
"directory": "apply-agent-cli"
|
|
22
|
+
"url": "https://github.com/BagTheJob-Ai/apply-agent.git"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"bagthejob",
|