@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.
@@ -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 fetches the current [BagTheJob.ai](https://app.bagthejob.ai) application-agent skill to your clipboard so you can paste it into the Claude desktop app.
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 # fetch the latest to re-paste
7
+ npx @bagthejobai/apply-agent update # copy the latest to re-paste
8
8
  ```
9
9
 
10
10
  ## What it does
11
11
 
12
- - Fetches the skill live from `GET https://app.bagthejob.ai/skill`.
13
- - Copies it to your clipboard (macOS `pbcopy`, Windows `clip`, Linux `wl-copy`/`xclip`/`xsel`).
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 **fetch-and-paste helper, not an installer.** By design (see the project's issue #159 security model):
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 fetched and silently executed.
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 CLI ships **no** skill content — it always fetches the live copy, so it can never go stale relative to the server. The package version is cut to match the skill's `Template version` at publish time (e.g. `1.23.0` for skill `v1.23.0`), so the number you install signals which skill release it shipped alongside. What you actually receive is always whatever the server currently serves.
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 fetch-and-paste helper for the BagTheJob
2
- // application-agent skill.
1
+ // @bagthejobai/apply-agent — a paste helper for the BagTheJob application-agent
2
+ // skill.
3
3
  //
4
- // What it does: fetches the current skill (`GET {base}/skill`) and puts it on
5
- // your clipboard, then prints instructions for you to review and paste it into
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. It vendors no skill content — the server copy
12
- // (go:embed-ed into btj-api, served at /skill) stays the single source of truth,
13
- // so the npm package can never drift from the deployed skill.
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
- export const DEFAULT_BASE_URL = "https://app.bagthejob.ai";
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
- // resolveBaseUrl picks the server to fetch from. Precedence: explicit --base-url
20
- // flag > BTJ_BASE_URL env > the production default. Trailing slashes are stripped
21
- // so `${base}/skill` never doubles up.
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
- // SkillFetchError carries a human-facing message; the CLI prints .message and
29
- // exits 1 without a stack trace.
30
- export class SkillFetchError extends Error {
31
- constructor(message) {
32
- super(message);
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
- // fetchSkill GETs {base}/skill and validates the {version, content} shape. It
38
- // throws SkillFetchError (never a raw network/JSON error) with a message that
39
- // names the URL and the reason, so a caller only has to print .message.
40
- export async function fetchSkill(baseUrl, fetchImpl = fetch) {
41
- const url = `${baseUrl}/skill`;
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
- res = await fetchImpl(url, {
51
- method: "GET",
52
- headers: { accept: "application/json" },
53
- });
41
+ content = read();
54
42
  } catch (err) {
55
- throw fail(err?.message || "network error");
56
- }
57
-
58
- if (!res.ok) {
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
- if (typeof content !== "string" || content.trim() === "") {
76
- throw fail("response is missing a 'content' field");
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: version.trim(), content };
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, baseUrl, stdout }) {
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} fetched from ${origin}`
142
- : `Fetched skill Template version ${version} from ${origin}`,
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: ${origin}${DASHBOARD_URL}).`);
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 — fetch the BagTheJob application-agent skill to your clipboard.
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 Fetch the skill and copy it for first-time install
182
- npx @bagthejobai/apply-agent update Fetch the latest skill and copy it to re-paste
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
- This tool never writes files, never stores credentials, and never registers a
191
- task. It only puts the skill on your clipboard for you to review and paste.`;
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, baseUrlFlag: undefined, stdout: false, help: false, version: false };
196
- for (let i = 0; i < argv.length; i++) {
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
- fetchImpl = fetch,
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.1.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 = await fetchSkill(baseUrl, fetchImpl);
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.0",
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/richkuo/job-search.git",
22
- "directory": "apply-agent-cli"
22
+ "url": "https://github.com/BagTheJob-Ai/apply-agent.git"
23
23
  },
24
24
  "keywords": [
25
25
  "bagthejob",