seekter 0.11.0 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +226 -9
  2. package/package.json +1 -1
  3. package/readme.mjs +48 -0
package/README.md CHANGED
@@ -1,19 +1,236 @@
1
- # seekter
1
+ <img src="https://raw.githubusercontent.com/selfishprimate/seekter/main/images/seekter-cover.jpg" alt="Seekter — job search, filtered by your rules" width="100%">
2
2
 
3
- Sets up [Seekter](https://seekter.dev), an open-source job-search kit for Claude Code. It searches job sources, filters postings against your own rules, fills application forms in your own Chrome, and keeps the tracker as markdown files on your machine.
3
+ # Seekter
4
+
5
+ [![version](https://img.shields.io/github/v/release/selfishprimate/seekter?label=version)](https://github.com/selfishprimate/seekter/releases/latest)
6
+
7
+ > **This package installs Seekter.** `npx seekter init` puts the newest release in `./seekter` (or the folder you name); then open Claude Code there and run `/seekter-init`. The package has no dependencies. Everything below is the project's README.
8
+
9
+ A job-search agent for Claude Code. It searches job sources every day, filters postings against **your** rules (location, visa, sectors, seniority, language), fills application forms in your own Chrome, and keeps every application and skip as a markdown file you can read, grep and diff.
10
+
11
+ It was built over a month of daily use by one job seeker and then emptied of personal data, so it's opinionated where the lessons were expensive: dedup before every form, never guess an answer, never invent an anecdote, never touch a CAPTCHA or a password. **Nothing in the tracked files assumes a field**: the titles, queries, boards and filters all come from your profile, and `/seekter-init` builds them from your answers. The measurements in `reference/` were taken in one discipline and say so where it matters.
12
+
13
+ Why it exists, and the principles it is built on, are in [MANIFESTO.md](https://github.com/selfishprimate/seekter/blob/main/MANIFESTO.md): employers already automate how applications are read, and a candidate may use tools for their side too, as long as what they send is true and they stay within the rules.
14
+
15
+ > **Before you run it**
16
+ > - Every application goes out in your name, and you are responsible for what it says.
17
+ > - Seekter never fills LinkedIn Easy Apply. Reading LinkedIn is off by default; switching it on is against LinkedIn's terms and puts your account at risk.
18
+ > - Seekter is free, but running it needs a paid Claude plan or an Anthropic API account.
19
+ >
20
+ > Read [DISCLAIMER.md](https://github.com/selfishprimate/seekter/blob/main/DISCLAIMER.md) for the details.
21
+
22
+ ## Quick start
4
23
 
5
24
  ```bash
6
- npx seekter init
25
+ npx seekter init # puts the newest release in ./seekter
7
26
  cd seekter
8
- claude
27
+ claude # open Claude Code in the folder
28
+ ```
29
+
30
+ Or clone it, if you want to send fixes back: `git clone https://github.com/selfishprimate/seekter && cd seekter`.
31
+
32
+ Then, inside Claude Code:
33
+
34
+ ```
35
+ /seekter-init # ~40 short questions, one at a time. Writes profile/ (git-ignored).
36
+ /seekter-run # today's search and applications
37
+ ```
38
+
39
+ Requirements: [Claude Code](https://docs.claude.com/en/docs/claude-code) (a paid Claude plan or an Anthropic API account; Seekter itself is free, running it is not), the Claude in Chrome extension and [Node.js](https://nodejs.org) 20 or newer. No packages, no `npm install`.
40
+
41
+ ## Commands
42
+
43
+ | Command | What it does |
44
+ |---|---|
45
+ | `/seekter-init` | Interviews you and writes your profile: contact details, resumes, target roles, where you can work, salary bands, standard form answers, sectors you won't touch, a fact bank for free-text answers, your writing voice, and the search queries. Resumable. Can import an existing tracker from a Notion/Sheets CSV. |
46
+ | `/seekter-run` | The daily run. Four sources in a fixed order (links you bring, freehire API, LinkedIn, other boards), filtering, dedup, form filling, tracker update, and a report with a per-source table. Applies without asking when a posting fits; stops only for things only you can decide. Every posting gets a 1-5 fit score before its form, which orders the queue and never removes anything. |
47
+ | `/seekter-log` | Records what happened next: rejections, interviews, offers, applications you made by hand, or a sweep of your inbox. |
48
+ | `/seekter-interview` | Prepares you for the next round: a screening, a hiring-manager call, a portfolio review, a test or an async video. Reads the posting, what your application said and the company's own site, answers each of the role's asks with how you work first and one of your own stories second, and lists what only you can decide (the salary to name, relocation timing). Never invents a story or a number, and never answers the invitation. |
49
+ | `/seekter-report` | Funnel, replies and interviews broken down by source, apply path, form system, location track, title, fit score and resume type (`stats --by`), top skip reasons, open hand-offs, and at most two suggested changes. |
50
+ | `/seekter-git` | Ships the kit. Branches, commits and pushes the shareable files (skills, references, scripts) after a run has taught Seekter something, and scans the diff for your personal details first so they never leave your machine. Your profile, applications and runs are never committed. Once you have merged, it can also cut the release. |
51
+
52
+ ## Daily use
53
+
54
+ | When | Command | What it needs |
55
+ |---|---|---|
56
+ | Once | `/seekter-init` | Your resume file(s). Takes 20–30 minutes; you can stop and resume. |
57
+ | Every working day | `/seekter-run` | Chrome open with the Claude in Chrome extension and your webmail logged in. Any links you collected go in `profile/links.txt` or the chat. |
58
+ | When a company replies, or weekly | `/seekter-log` | For an inbox sweep: your webmail open and logged in, in the same Chrome. |
59
+ | Before an interview | `/seekter-interview` | The company's name, and the stage and date if the invitation is not in your inbox. Your stories in `profile/interviews/README.md` make it much better. |
60
+ | Weekly | `/seekter-report` | Nothing. It reads the tracker only. |
61
+ | After a run changes a skill or a reference | `/seekter-git` | A git remote you can push to. Optional: `gh` for the pull request. |
62
+
63
+ Run `/seekter-log` **before** `/seekter-report`. The report reads `applications/` and `runs/`, not your email, so replies that haven't been logged don't show up in it.
64
+
65
+ `/seekter-log` can be used two ways:
66
+
67
+ - **Tell it what happened:** "Acme rejected me", "I have a call with Globex on Thursday", "I applied to Initech myself". It moves the right file and writes a log line.
68
+ - **Ask for an inbox sweep:** "check my inbox for replies". It opens your webmail in Chrome, reads each message body (subjects are unreliable: many rejections are titled just "Your application with X"), and files every reply as rejected, interviewing or offer. A reply from a company with no tracker entry is added as a new record. It never answers an email; anything that asks you to act (a scheduling link, a take-home) is listed for you.
69
+
70
+ The inbox sweep uses the browser because the Microsoft 365 connector doesn't accept personal Outlook.com or Hotmail accounts. Any webmail you can read in Chrome works.
71
+
72
+ The **Needs you** table at the top of `applications/README.md` lists everything waiting on you: a CAPTCHA, an account wall, a question only you can answer, with the next step for each. Clear it before asking for more applications.
73
+
74
+ ## Your files
75
+
76
+ ```
77
+ profile/
78
+ profile.md ← everything about you; the only place personal values live
79
+ settings.json ← every switch and number: queries, boards, LinkedIn mode, limits
80
+ documents/ ← files you bring: a resume PDF if you keep no resume.json, a portfolio PDF, cover letters
81
+ resume/ ← the resume PDFs forms receive, one per resume type, named after you
82
+ json/resume.json ← the master resume they are printed from: facts, a pool of bullets, the resume types
83
+ drafts/ ← review copies that still print unapproved text
84
+ ```
85
+
86
+ **A resume you bring goes in `profile/documents/`.** `/seekter-init` asks for the file path and copies it there; to add or replace one later, drop the PDF in that folder and update the table in `profile/profile.md` §2 (which resume is the default, which one is for which role type). **With a master resume** (`profile/resume/json/resume.json`), Seekter prints a PDF per resume type into `profile/resume/` instead, and those are what forms receive.
87
+
88
+ To change a rule (a new blacklisted company, a salary band, a city you'd now accept), tell Seekter in chat; it edits `profile/profile.md` and quotes your words there. You can also edit the file by hand.
89
+
90
+ **`profile/settings.json` holds the switches**, each documented in `templates/settings.json`, and you can edit it by hand at any time. `/seekter-init` writes it from the template before the first question, so a setup you stop halfway still leaves a valid file: a key you never answered runs on the template's default. `node scripts/seekter.mjs settings --check` shows which values are still defaults and whether anything required is missing. Guardrails are not settings: nothing in this file can make Seekter solve a CAPTCHA, create an account or fill LinkedIn Easy Apply. A setup from before v0.3 has `profile/search.json`; the first run moves it to `settings.json` with its values unchanged.
91
+
92
+ ## The tracker
93
+
94
+ Every posting Seekter touches is recorded once, filed by the month it was first handled:
95
+
96
+ ```
97
+ applications/
98
+ README.md ← generated overview: needs you, in progress, last 30 days, one row per month
99
+ 2026-09/
100
+ README.md ← generated: that month's applications with status
101
+ 2026-09-22--ruby-labs--senior-backend-engineer.md
102
+ 2026-09-21--wise--staff-data-scientist.md
103
+ ...
104
+ skipped.md ← one table row per posting passed over, with the reason
105
+ 2026-10/
106
+ ```
107
+
108
+ - **Applications and hand-offs are files.** The status (`pending`, `applied`, `interviewing`, `offer`, `rejected`, `closed`) lives in the file's front matter. Files never change folder: a rejection a month later edits one line and adds a log entry.
109
+ - **Skips are rows, not files.** Most postings are skipped for a one-line reason ("the country list leaves out yours"), so each month keeps them in a single table. Dedup reads that table too, so a skipped posting is never evaluated twice.
110
+ - **You read the generated pages, not the folders.** `applications/README.md` is rebuilt after every `add` and `move`.
111
+
112
+ ```markdown
113
+ ---
114
+ company: Ruby Labs
115
+ role: Senior Backend Engineer
116
+ status: applied
117
+ url: https://jobs.ashbyhq.com/ruby-labs/1e548ada-…
118
+ source: freehire
119
+ ats: ashby
120
+ location_fit: A
121
+ applied: 2026-09-22
122
+ job_key: uuid:1e548ada-…
123
+ ---
124
+
125
+ # Senior Backend Engineer · Ruby Labs
126
+
127
+ ## Why it fits
128
+ ## Notes
129
+ ## Answers submitted ← free-text answers as sent, so no sentence goes to two companies
130
+ ## Log
131
+ - 2026-09-22: applied
132
+ ```
133
+
134
+ `job_key` is a normalised identity (LinkedIn ID, ATS UUID, Greenhouse ID…), so the same job reached through LinkedIn, an aggregator and the company site is still caught as a duplicate.
135
+
136
+ ```bash
137
+ node scripts/seekter.mjs check <url> --company "Acme" # exit 1 if already tracked, 2 if the company is on hold
138
+ node scripts/seekter.mjs move <url-or-file> rejected --note "form mail, 2 days" # edits the status in place
139
+ node scripts/seekter.mjs list --status pending
140
+ node scripts/seekter.mjs stats --since 2026-09-01
141
+ node scripts/seekter.mjs normalize --dry-run # tidy enum values, fill ats from the url
142
+ node scripts/seekter.mjs migrate # one-off: old applications/<status>/ folders -> month folders
143
+ ```
144
+
145
+ `normalize` lowercases `source`, `ats` and `apply_type`, derives the application system (`ats`) from the posting URL, and moves an ATS name that was stored as `source` (a common mix-up in hand-kept trackers) into `ats`. It also collapses a log line written twice in a row. The CSV importer and `add` already do the rest; run it after editing files by hand.
146
+
147
+ ### Importing an existing tracker
148
+
149
+ Export your Notion database, Airtable or Google Sheet as CSV, then:
150
+
151
+ ```bash
152
+ node scripts/import_csv.mjs export.csv --dry-run # shows what would be created
153
+ node scripts/import_csv.mjs export.csv
154
+ ```
155
+
156
+ Column names are matched loosely (Position/Role/Title, Company, Status, Job URL, Applied on, Source, Notes…). Rows that point to the same posting are merged. Rows without a URL are imported but can only be matched by company name later, so fill in URLs where you have them.
157
+
158
+ ### Your resume: one set of facts, a resume for each kind of role
159
+
160
+ A Product Designer posting and a Forward Deployed Engineer posting look for different evidence in the first lines of every job, so they should not get the same resume. But four resume files drift apart: fix a date in one and the others keep the old one. Seekter keeps **one master resume** (`profile/resume/json/resume.json`, whose PDFs it prints into `profile/resume/`, named `<file_prefix>-<type>.pdf` so a recruiter sees your name on the file): every job, date and number once, and under each job a pool of bullets tagged by theme (`product`, `design-system`, `engineering`, `research`, `ui`, `ai`…). Each **resume type** in the same file picks from that pool: its own headline and summary, the bullets that fit, in the order that fits, which projects and skill groups to show, and how much to say about older jobs. A type can choose and order the facts but cannot add one.
161
+
162
+ ```bash
163
+ node scripts/resume.mjs list profile/resume/json/resume.json # the resume types and their PDFs
164
+ node scripts/resume.mjs validate profile/resume/json/resume.json # facts, numbers, dates, voice, length
165
+ node scripts/resume.mjs pdf profile/resume/json/resume.json --all # print every type into profile/resume/
166
+ node scripts/resume.mjs pdf profile/resume/json/resume.json --variant product-designer --drafts # review copy in profile/resume/drafts/
167
+ ```
168
+
169
+ Two pages is the ceiling, checked on A4 and US Letter (`validate --pages`). Length is not what screens a resume out, but past two pages most screeners call it too long, and the first half of page one carries the decision: headline, employers, dates and a portfolio link. So a type can say less about older work instead of cutting it: `bullets_per_job` and `older` tiers, `earlier: {before}` to fold older jobs into one closing line, `short: true` to print each bullet's one-line `short` version, `skill_items`, and `layout: "compact"` (same type and colour, each job on one line). The same pass checks what a parser gets from the file: a PDF with Type 3 fonts (how Chrome embeds a variable font, and parsers lose the spaces in its bold text), no text layer, or over 2.5 MB is a problem, and so is a PDF already in `profile/resume/` with one of those. The resume prints in Inter, which ships with the kit as static fonts (SIL Open Font License, `templates/resume/fonts/LICENSE`), so printing needs no network. Each application's tracker record keeps the type that was sent (`add --resume <type>`), so outcomes can be read per type later.
170
+
171
+ `validate` holds the resume to the profile: every number has to be in the profile's facts, dates and titles have to match its work history, the profile's banned phrases are flagged, and so are overlapping jobs and a resume likely longer than two pages. New text (a bullet, a headline, a summary) carries `"draft": true` until you approve it, and a PDF that would print a draft is refused unless `--drafts` asks for a review copy. The look is `templates/resume/resume.css`. `templates/resume/resume.example.json` is a fictional master to start from. If your browser is not in a standard place, set `SEEKTER_CHROME` to its binary.
172
+
173
+ The same rules hold for what a form receives. Before a cover letter or a free-text answer goes into a field, `node scripts/claims.mjs <file> --posting <posting.txt>` flags any number that is in neither your profile's facts nor the posting, your banned phrases and long dashes, and any sentence of eight words or more already sent to another company.
174
+
175
+ ## What's in the repo
176
+
177
+ ```
178
+ .claude/skills/ seekter-init · seekter-run · seekter-log · seekter-interview · seekter-report · seekter-git
179
+ reference/ sources/ (one file per job source) · ats/ (one file per application form system); each has a _core.md read first
180
+ templates/ profile.md · settings.json · resume/ (the resume stylesheet, an example master resume and an example resume in the site's format)
181
+ scripts/ seekter.mjs · freehire_sweep.mjs · employer_sweep.mjs · import_csv.mjs · resume.mjs · live.mjs · claims.mjs
182
+ providers/ one reader per application system for the employer watchlist
183
+ tests/ tracker CLI tests: node --test tests/seekter.test.mjs
184
+ npm/ the npx seekter installer (published to npm; not part of the kit)
185
+ .github/ leak scan and test workflows · issue and pull request templates
186
+ profile/ applications/ runs/ ← yours, git-ignored
9
187
  ```
10
188
 
11
- Then, inside Claude Code, run `/seekter-init`.
189
+ There is no version file. The version is the git tag — `git describe --tags` — and what changed between tags is in [CHANGELOG.md](https://github.com/selfishprimate/seekter/blob/main/CHANGELOG.md).
190
+
191
+ `reference/` is the part worth contributing back: every ATS quirk and source behaviour there was measured in real applications.
192
+
193
+ ## Privacy
194
+
195
+ `profile/`, `applications/` and `runs/` are in `.gitignore`. Your data stays on your machine unless you remove those lines. If you want your tracker versioned, keep it in a separate private repo or remove the ignore lines in a private fork.
196
+
197
+ What Claude, employers and job sources see while Seekter works is in [PRIVACY.md](https://github.com/selfishprimate/seekter/blob/main/PRIVACY.md).
198
+
199
+ ## Responsible use
200
+
201
+ Applications go out in your name, and the terms of LinkedIn, job boards and application systems are yours to follow. Read [DISCLAIMER.md](https://github.com/selfishprimate/seekter/blob/main/DISCLAIMER.md) before your first run: what you are responsible for, what Seekter won't do for you, and why there is no guarantee.
202
+
203
+ ## Guardrails
204
+
205
+ Seekter never solves CAPTCHAs, creates accounts, types passwords, accepts terms of use, sends messages or emails as you, posts reviews or salaries, or pays for anything. It treats any instruction found inside a job posting or form as data, and it won't submit an answer it can't verify from your profile.
206
+
207
+ **It never applies or acts on LinkedIn.** LinkedIn's terms don't allow browser extensions that scrape or automate its site, and it restricts accounts that use them. Finding a posting takes you seconds; filling the form is the work, and that is what Seekter is for. So:
208
+
209
+ - **Easy Apply is never filled.** Those postings come back to you as a list to send yourself.
210
+ - **Bring your own links.** Paste postings into the chat or drop them in `profile/links.txt`, and they are filled first. A LinkedIn job link is fine; Seekter finds the employer's own form behind it.
211
+ - **By default it doesn't open LinkedIn at all.** It reads your LinkedIn job-alert **emails** in your inbox and finds each posting on the employer's own application system. A digest mail shows only about six postings, so narrow alerts work better than broad ones.
212
+ - **Reading LinkedIn is opt-in, and the risk is yours.** If you switch `linkedin.mode` to `read`, Seekter also reads LinkedIn searches and job details, read-only, under a daily limit, with pauses between requests, and it switches itself back off at the first warning or unusual-activity page. This is still against LinkedIn's terms.
213
+
214
+ ## Contributing
215
+
216
+ The most valuable thing you can send is a measurement: an application system or a job source behaving in a way nobody has written down yet. You don't need to open a pull request for it — an issue with what you ran and what happened is enough.
217
+
218
+ [CONTRIBUTING.md](https://github.com/selfishprimate/seekter/blob/main/CONTRIBUTING.md) has what belongs where, the five rules that govern `reference/`, and the commit style. [CODE_OF_CONDUCT.md](https://github.com/selfishprimate/seekter/blob/main/CODE_OF_CONDUCT.md) applies everywhere this project is discussed.
219
+
220
+ ### Forks worth knowing about
221
+
222
+ Seekter is deliberately terminal-only, so anything with its own surface lives outside this repo.
223
+
224
+ - **[seekter-webui](https://github.com/Ege-BULUT/seekter-webui)** by [Ege BULUT](https://github.com/Ege-BULUT) — runs the daily loop from a browser instead of the terminal: a standard-library server that drives the same tracker CLI, with the sweep, the filtering and the reports behind an interface. Not maintained here, and not covered by this repo's guardrails or tests.
225
+
226
+ ## Security
227
+
228
+ Seekter drives your logged-in browser and holds your contact details and resumes on disk, so the interesting questions are about untrusted input rather than about a server. Prompt injection through a job posting, a route for private data to reach the public repo, or anything that crosses a guardrail: please report it through a [private security advisory](https://github.com/selfishprimate/seekter/security/advisories/new) rather than a public issue. [SECURITY.md](https://github.com/selfishprimate/seekter/blob/main/SECURITY.md) has the details.
12
229
 
13
- `init` downloads the newest release from [GitHub](https://github.com/selfishprimate/seekter/releases/latest) and unpacks it into `./seekter`, or into the folder you name (`npx seekter init my-search`). The folder must be new or empty. Nothing else is installed: the kit has no dependencies, and this package has none either.
230
+ ## License
14
231
 
15
- You need Claude Code, the Claude in Chrome extension and Node.js 20 or newer. Seekter is free; running it is not, because it runs on your own paid Claude plan or API account.
232
+ MIT. See [LICENSE](https://github.com/selfishprimate/seekter/blob/main/LICENSE).
16
233
 
17
- Applications go out in your name. Read [DISCLAIMER.md](https://github.com/selfishprimate/seekter/blob/main/DISCLAIMER.md) before the first run.
234
+ <img src="https://raw.githubusercontent.com/selfishprimate/seekter/main/images/seekter-thank-you.jpg" alt="Thank you for using Seekter" width="100%">
18
235
 
19
- MIT licensed.
236
+ <p align="center"><em>Dedicated to the memory of <strong>Tarçın</strong>, the most loved and most loving cat of all time.</em></p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seekter",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Sets up Seekter, the open-source job-search kit for Claude Code: npx seekter init",
5
5
  "type": "module",
6
6
  "bin": {
package/readme.mjs ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // Writes npm/README.md from the repository's README.md, so the npm page shows the same text as
3
+ // GitHub. Run it before every `npm publish` from this folder: npm shows the README packed into the
4
+ // latest version, never the repository's, and a page can only change by publishing.
5
+ //
6
+ // node npm/readme.mjs
7
+ //
8
+ // The npm page resolves nothing relative to the repository, so links to files become GitHub links,
9
+ // images become raw.githubusercontent.com addresses, and GitHub's own alert syntax ("> [!NOTE]"),
10
+ // which npm prints as text, is dropped. Not shipped: package.json lists only bin/ and README.md.
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+
15
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
16
+ const BLOB = "https://github.com/selfishprimate/seekter/blob/main/";
17
+ const RAW = "https://raw.githubusercontent.com/selfishprimate/seekter/main/";
18
+
19
+ const INTRO = `> **This package installs Seekter.** \`npx seekter init\` puts the newest release in \`./seekter\` (or the folder you name); then open Claude Code there and run \`/seekter-init\`. The package has no dependencies. Everything below is the project's README.
20
+
21
+ `;
22
+
23
+ const relative = (p) => !/^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i.test(p);
24
+
25
+ // The note goes under the title block (cover image, heading, version badge), so the page still
26
+ // opens on the picture; with no badge line it goes first.
27
+ export function npmReadme(text) {
28
+ const out = String(text)
29
+ // Images: markdown and HTML.
30
+ .replace(/(!\[[^\]]*\]\()([^)\s]+)(\))/g, (m, a, p, b) => (relative(p) ? a + RAW + p.replace(/^\.\//, "") + b : m))
31
+ .replace(/(<img\b[^>]*\bsrc=")([^"]+)(")/g, (m, a, p, b) => (relative(p) ? a + RAW + p.replace(/^\.\//, "") + b : m))
32
+ // Links to files in the repository.
33
+ .replace(/(\]\()([^)\s]+)(\))/g, (m, a, p, b) => (relative(p) ? a + BLOB + p.replace(/^\.\//, "") + b : m))
34
+ // GitHub alerts: npm would print "[!IMPORTANT]" literally; the quote around it stays.
35
+ .replace(/^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n/gm, "");
36
+ const badge = out.match(/^\[!\[version\].*\n\n?/m);
37
+ if (!badge) return INTRO + out;
38
+ const at = badge.index + badge[0].length;
39
+ return out.slice(0, at) + INTRO + out.slice(at);
40
+ }
41
+
42
+ function main() {
43
+ const src = fs.readFileSync(path.join(HERE, "..", "README.md"), "utf8");
44
+ fs.writeFileSync(path.join(HERE, "README.md"), npmReadme(src));
45
+ console.log("npm/README.md written from README.md");
46
+ }
47
+
48
+ if (process.argv[1] && pathToFileURL(fs.realpathSync(process.argv[1])).href === import.meta.url) main();