terminalhire 0.1.0

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 ADDED
@@ -0,0 +1,294 @@
1
+ # terminalhire — local-first job matching for developers
2
+
3
+ Pull curated job matches from a broad public pool. Matching runs entirely on your device. Your profile never leaves your machine.
4
+
5
+ **Domain:** [terminalhire.com](https://terminalhire.com)
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ # one-off — no global install needed
11
+ npx terminalhire help
12
+
13
+ # global install
14
+ npm i -g terminalhire
15
+
16
+ # wire up the Claude Code statusLine nudge (optional, recommended)
17
+ node $(npm root -g)/terminalhire/install.js
18
+ ```
19
+
20
+ The installer prints a full v3.1 disclosure, asks for explicit "yes", then offers GitHub sign-in (recommended), and finally writes one key to `~/.claude/settings.json`. Install is NOT consent to share your profile — that is a separate, per-role decision.
21
+
22
+ ## Uninstall
23
+
24
+ ```sh
25
+ node install.js --uninstall
26
+ terminalhire logout # clear GitHub token (if connected)
27
+ terminalhire profile --delete # also wipe local profile
28
+ rm -rf ~/.jpi # wipe everything
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ```sh
34
+ terminalhire login # sign in with GitHub — enriches profile instantly (recommended)
35
+ terminalhire logout # clear stored GitHub token from ~/.jpi/github-token.enc
36
+
37
+ terminalhire jobs # fetch index, match locally, browse ranked roles
38
+ terminalhire jobs --limit 20 # show top 20 results (default: 10)
39
+ terminalhire jobs --remote-only # filter to remote roles only
40
+ terminalhire jobs --all # show all matches above zero score
41
+
42
+ terminalhire profile --show # inspect your encrypted local profile (incl. GitHub fields)
43
+ terminalhire profile --edit # set displayName, contactEmail, prefs
44
+ terminalhire profile --delete # wipe profile and encryption key from disk
45
+ ```
46
+
47
+ ## GitHub sign-in (recommended)
48
+
49
+ GitHub is the **primary enrichment signal** for Terminalhire. Signing in solves the cold-start problem — you get accurate matches on the very first run instead of waiting for tag accumulation.
50
+
51
+ ```sh
52
+ terminalhire login
53
+ ```
54
+
55
+ This runs the **GitHub OAuth Device Flow**:
56
+
57
+ 1. A code like `XXXX-XXXX` and a URL (`https://github.com/login/device`) are displayed.
58
+ 2. You open the URL, enter the code, and click "Authorize".
59
+ 3. Terminalhire polls until you authorize, then stores the token encrypted at `~/.jpi/github-token.enc`.
60
+ 4. Your public GitHub profile is fetched, processed, and merged into your local profile.
61
+
62
+ ### What GitHub enriches
63
+
64
+ | Signal | Source |
65
+ |---|---|
66
+ | Skill tags | Repo languages + repo topics, filtered through `vocabulary.normalize()` |
67
+ | Seniority estimate | Account age × (repos + followers) per [documented thresholds](#seniority-thresholds) |
68
+ | Display name | `user.name` if not already set in profile |
69
+ | Contact email | `user.email` if public and not already set in profile |
70
+
71
+ ### Public scope guarantee
72
+
73
+ Scope requested: **`read:user`** — public profile + public repos only.
74
+
75
+ **Private-repo scopes are NEVER requested.** This means:
76
+ - No employer-IP risk from private repos.
77
+ - No code, secrets, or private file access.
78
+ - Only what is already publicly visible on your GitHub profile.
79
+
80
+ ### Data residency
81
+
82
+ - Token: encrypted at `~/.jpi/github-token.enc` (AES-256-GCM, same scheme as local profile).
83
+ - GitHub data stays **on your machine** — it enriches your local profile.
84
+ - GitHub fields cross the wire **only** in a consented `LeadPayload` when you type "yes".
85
+ - GitHub data is **never sent silently**.
86
+
87
+ ### Seniority thresholds
88
+
89
+ ```
90
+ junior : age < 2 yr OR (repos < 5 AND followers < 10)
91
+ mid : age 2–5 yr AND repos >= 5
92
+ senior : age 5–9 yr AND (repos >= 20 OR followers >= 100)
93
+ staff : age >= 9 yr AND (repos >= 40 OR followers >= 500)
94
+ ```
95
+
96
+ When signals conflict → conservative (lower) estimate. Unresolvable → undefined.
97
+
98
+ ### Mock mode (dev / CI)
99
+
100
+ No GitHub OAuth App registered yet? Set `TERMINALHIRE_GITHUB_MOCK=1`:
101
+
102
+ ```sh
103
+ TERMINALHIRE_GITHUB_MOCK=1 terminalhire login
104
+ ```
105
+
106
+ Uses the fixture at `fixtures/github-sample.json` instead of a live OAuth flow. No client ID needed. Safe for local development and CI.
107
+
108
+ ### Registering a GitHub OAuth App
109
+
110
+ 1. Go to github.com → Settings → Developer settings → OAuth Apps → **New OAuth App**
111
+ 2. Set "Authorization callback URL" to `http://localhost` (not used by device flow)
112
+ 3. Enable **Device Authorization** in the OAuth App settings
113
+ 4. Copy "Client ID" → set as `GITHUB_CLIENT_ID` in your environment
114
+ 5. For public (CLI-only) OAuth Apps, the client secret is optional. If you set one, add it as `GITHUB_CLIENT_SECRET`.
115
+
116
+ ```sh
117
+ export GITHUB_CLIENT_ID=Iv1.your_app_client_id
118
+ # optional:
119
+ export GITHUB_CLIENT_SECRET=your_client_secret
120
+ ```
121
+
122
+ ## Status bar nudge
123
+
124
+ After `terminalhire jobs` runs and finds matches, the Claude Code status bar shows a once-per-session discovery nudge:
125
+
126
+ ```
127
+ ✦ N roles match your current work — run: terminalhire jobs
128
+ ```
129
+
130
+ The nudge is printed at most once per Claude Code session. It reads only `~/.jpi/index-cache.json` (a matchCount written by the last `terminalhire jobs` run). It makes zero network calls.
131
+
132
+ ## Architecture (v3.1 hybrid)
133
+
134
+ ```
135
+ Server Client (your machine)
136
+ ────────────────────────────── ─────────────────────────────────────────
137
+ Broad public pool: ~/.jpi/profile.enc (AES-256-GCM encrypted)
138
+ Greenhouse + Ashby (ATS) ~/.jpi/github-token.enc (AES-256-GCM)
139
+ Himalayas + WWR + HN GET Fingerprint built from profile
140
+ Coastal buyer-lead roles ←─── /api/index (anonymous, no dev data)
141
+ Local match() from @jpi/core
142
+ Ranked list printed to terminal
143
+
144
+ POST /api/lead ←── ONLY on explicit "yes"
145
+ Named-entity consent per role
146
+ (GitHub fields included only if present AND consented)
147
+
148
+ GitHub (optional enrichment):
149
+ github.com/login/device/code ←── Device flow (read:user scope only)
150
+ api.github.com/users/<login> Token + data stay on machine
151
+ (public repos, topics, langs)
152
+ ```
153
+
154
+ **Zero dev-side egress during matching.** The index download is anonymous. GitHub enrichment stays local. The only outbound payload with developer data is a consented `LeadPayload`, for buyer-lead roles only.
155
+
156
+ ## Privacy
157
+
158
+ | What happens locally | What crosses the wire |
159
+ |---|---|
160
+ | Profile accumulated from personal project sessions | GET /api/index — anonymous, no dev data |
161
+ | Closed-vocab skill tags + seniority (encrypted at rest) | POST /api/lead — ONLY after explicit per-role "yes" consent |
162
+ | GitHub token + public profile data (encrypted at rest) | GitHub fields in lead — ONLY after consent AND GitHub is connected |
163
+ | Employer-repo sessions: language tags only | Nothing else |
164
+ | git email domain + remote host (employer detection only) | |
165
+
166
+ **Employer-repo exclusion (default on):** if `git config user.email` is a corporate domain or `git remote` points to a non-personal host, only language-level tags (`typescript`, `python`, …) accumulate. Fine-grained framework/infra tags are excluded. The flag `hasEmployerSessions` is recorded locally but never emitted.
167
+
168
+ ## Local profile encryption
169
+
170
+ - Algorithm: **AES-256-GCM** via Node built-in `crypto` (no external deps).
171
+ - Key: stored at `~/.jpi/key` with `0600` permissions. If `keytar` is installed, the OS keychain is preferred and the key file is not written.
172
+ - Profile file: `~/.jpi/profile.enc` — JSON blob `{ iv, tag, ciphertext }` (all hex-encoded).
173
+ - GitHub token file: `~/.jpi/github-token.enc` — same format, same key.
174
+
175
+ ## Lead payload shape
176
+
177
+ The exact object sent to `/api/lead` when you consent (nothing else is sent):
178
+
179
+ ```json
180
+ {
181
+ "opportunityId": "coastal:senior-fullstack-001",
182
+ "buyerId": "coastal",
183
+ "buyerLegalName": "Coastal Recruiting LLC",
184
+ "approvedFields": {
185
+ "skillTags": ["typescript", "react", "postgresql"],
186
+ "seniorityBand": "senior",
187
+ "displayName": "Your Name",
188
+ "contactEmail": "you@example.com",
189
+ "note": "Optional note typed at consent time",
190
+ "github": {
191
+ "login": "yourlogin",
192
+ "profileUrl": "https://github.com/yourlogin",
193
+ "topLanguages": ["typescript", "python", "go"],
194
+ "publicRepos": 42
195
+ }
196
+ },
197
+ "consentText": "You are about to share the following information with Coastal Recruiting LLC\n...",
198
+ "createdAt": "2026-06-14T00:00:00.000Z"
199
+ }
200
+ ```
201
+
202
+ - `displayName`, `contactEmail`, and `note` are included only if set.
203
+ - `github` is included **only** when: (a) you have run `terminalhire login` AND (b) you typed "yes" at the consent prompt.
204
+ - `skillTags` is always the full list from your profile.
205
+
206
+ ## Consent prompt text
207
+
208
+ The exact text shown before any lead is sent (GitHub profile connected):
209
+
210
+ ```
211
+ You are about to share the following information with Coastal Recruiting LLC
212
+ for opportunity: <title> at <company> (<jobId>)
213
+
214
+ Fields that will be sent:
215
+ • skillTags: ["typescript","react","postgresql"]
216
+ • seniorityBand: "senior"
217
+ • displayName: "Your Name" ← only if set in profile
218
+ • contactEmail: "you@example.com" ← only if set in profile
219
+ • github.login: "yourlogin"
220
+ • github.profileUrl: "https://github.com/yourlogin"
221
+ • github.topLanguages: ["typescript","python","go"]
222
+ • github.publicRepos: 42
223
+
224
+ GitHub fields above are public data only (scope: read:user). No private repos.
225
+
226
+ Nothing else leaves your machine. This action is specific to this role.
227
+ Coastal Recruiting LLC will use this to evaluate you for the role.
228
+ You can delete your profile at any time with: terminalhire profile --delete
229
+
230
+ Share your profile with Coastal Recruiting LLC for this role? [y/N]
231
+ ```
232
+
233
+ Without GitHub: the `github.*` fields and the note about public scope are omitted.
234
+
235
+ Typing anything other than `y` or `yes` aborts with no network call.
236
+
237
+ ## Apply modes
238
+
239
+ | `applyMode` | What happens when you select a role |
240
+ |---|---|
241
+ | `direct` | Opens the employer's public URL in your terminal. No data sent. |
242
+ | `buyer-lead` | Triggers the named-entity consent flow above. Nothing sent without "yes". |
243
+
244
+ The two modes are never silently conflated.
245
+
246
+ ## Zero-egress test (M-5)
247
+
248
+ ```sh
249
+ node test/zero-egress.test.js
250
+ ```
251
+
252
+ ## Environment variables
253
+
254
+ | Variable | Default | Description |
255
+ |---|---|---|
256
+ | `TERMINALHIRE_API_URL` | `https://terminalhire.com` | Base URL for `/api/index` and `/api/lead` (also accepts legacy `JPI_API_URL`) |
257
+ | `GITHUB_CLIENT_ID` | _(required for real OAuth)_ | GitHub OAuth App client ID (device flow) |
258
+ | `GITHUB_CLIENT_SECRET` | _(optional)_ | GitHub OAuth App client secret (public apps omit this) |
259
+ | `GITHUB_DEVICE_CLIENT_ID` | _(falls back to `GITHUB_CLIENT_ID`)_ | Device-flow specific client ID if different |
260
+ | `TERMINALHIRE_GITHUB_MOCK` | `0` | Set to `1` to skip real OAuth and use the fixture (dev/CI); also accepts legacy `JPI_GITHUB_MOCK` |
261
+
262
+ ## File layout
263
+
264
+ ```
265
+ apps/cli/
266
+ bin/
267
+ jpi.js — statusLine nudge (once-per-session, zero egress)
268
+ jpi-dispatch.js — 'terminalhire' bin entrypoint, routes subcommands
269
+ jpi-jobs.js — 'terminalhire jobs': fetch index, local match, consent flow
270
+ jpi-login.js — 'terminalhire login' / 'terminalhire logout': GitHub device flow + enrichment
271
+ jpi-profile.js — 'terminalhire profile': show/edit/delete encrypted profile
272
+ refresh.js — TOMBSTONE (v1 removed; exits non-zero)
273
+ src/
274
+ signal.ts — local fingerprint extractor (reads dep files + extensions)
275
+ profile.ts — encrypted profile: read/write/accumulate/delete
276
+ github-auth.ts — GitHub device flow: token I/O + encrypted storage
277
+ test/
278
+ zero-egress.test.js — M-5 zero-egress assertion suite
279
+ install.js — v3.1 installer with GitHub onboarding
280
+ package.json
281
+ ```
282
+
283
+ ## How data is used: local default vs consented lead
284
+
285
+ | Data | Default (local-enrichment) | Opt-in (consented lead) |
286
+ |---|---|---|
287
+ | Skill tags | Stored encrypted locally, used for local matching only | Included in `approvedFields.skillTags` on "yes" |
288
+ | Seniority | Stored encrypted locally | Included in `approvedFields.seniorityBand` on "yes" |
289
+ | Display name | Stored encrypted locally (pre-filled from GitHub if public) | Included if set and "yes" |
290
+ | Contact email | Stored encrypted locally (pre-filled from GitHub public email) | Included if set and "yes" |
291
+ | GitHub login / profileUrl | Stored in `profile.github` on `terminalhire login` | Included in `approvedFields.github` ONLY on "yes" |
292
+ | GitHub topLanguages | Used locally for tag inference (closed vocab) | Included in `approvedFields.github` ONLY on "yes" |
293
+ | GitHub token | Encrypted at `~/.jpi/github-token.enc` | Never sent |
294
+ | Private repos | Never accessed (scope: read:user) | N/A |