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.
- package/README.md +226 -9
- package/package.json +1 -1
- package/readme.mjs +48 -0
package/README.md
CHANGED
|
@@ -1,19 +1,236 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
3
|
+
# Seekter
|
|
4
|
+
|
|
5
|
+
[](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
|
-
|
|
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
|
-
|
|
230
|
+
## License
|
|
14
231
|
|
|
15
|
-
|
|
232
|
+
MIT. See [LICENSE](https://github.com/selfishprimate/seekter/blob/main/LICENSE).
|
|
16
233
|
|
|
17
|
-
|
|
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
|
-
|
|
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
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();
|