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/install.js ADDED
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install.js — v3 installer for jpi
4
+ *
5
+ * What it does:
6
+ * 1. Prints a full v3 disclosure (local-first, pull model, named buyer, lead payload)
7
+ * 2. Requires explicit "yes" before touching any system file
8
+ * 3. Backs up ~/.claude/settings.json
9
+ * 4. Merges a statusLine entry pointing to bin/jpi.js (nudge only, no egress)
10
+ * 5. Registers 'jpi' bin in PATH if npm global bin is writable
11
+ * 6. Supports --uninstall
12
+ * 7. Idempotent: safe to run multiple times
13
+ *
14
+ * Usage:
15
+ * node install.js
16
+ * node install.js --uninstall
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { join, resolve, dirname } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { createInterface } from 'node:readline';
24
+ import { spawnSync } from 'node:child_process';
25
+
26
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
27
+ // Resolve the nudge bin robustly: prefer the bundled dist output (published package),
28
+ // fall back to the legacy bin/ path for in-workspace / development installs.
29
+ const _distBin = resolve(join(__dirname, 'dist', 'bin', 'jpi.js'));
30
+ const _legacyBin = resolve(join(__dirname, 'bin', 'jpi.js'));
31
+ const BIN_PATH = existsSync(_distBin) ? _distBin : _legacyBin;
32
+ const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
33
+ const SETTINGS_DIR = dirname(SETTINGS_PATH);
34
+ const UNINSTALL = process.argv.includes('--uninstall');
35
+
36
+ // ── Helpers ───────────────────────────────────────────────────────────────────
37
+
38
+ function readSettings() {
39
+ if (!existsSync(SETTINGS_PATH)) return {};
40
+ try {
41
+ return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
42
+ } catch {
43
+ console.error('Error: ~/.claude/settings.json exists but is not valid JSON.');
44
+ console.error('Please fix it manually before running this installer.');
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ function backupSettings() {
50
+ if (!existsSync(SETTINGS_PATH)) return;
51
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
52
+ const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
53
+ copyFileSync(SETTINGS_PATH, backupPath);
54
+ console.log(` Backed up settings to: ${backupPath}`);
55
+ }
56
+
57
+ function writeSettings(settings) {
58
+ mkdirSync(SETTINGS_DIR, { recursive: true });
59
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8');
60
+ }
61
+
62
+ function ask(question) {
63
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
64
+ return new Promise(resolve => {
65
+ rl.question(question, answer => {
66
+ rl.close();
67
+ resolve(answer.trim().toLowerCase());
68
+ });
69
+ });
70
+ }
71
+
72
+ function buildStatusLineEntry(binPath) {
73
+ return `node ${binPath}`;
74
+ }
75
+
76
+ // ── Install ───────────────────────────────────────────────────────────────────
77
+
78
+ async function install() {
79
+ console.log('');
80
+ console.log('┌─────────────────────────────────────────────────────────────────┐');
81
+ console.log('│ terminalhire v3.1 — local-first job matching │');
82
+ console.log('│ Pull your matches. Your profile stays on-device. │');
83
+ console.log('└─────────────────────────────────────────────────────────────────┘');
84
+ console.log('');
85
+ console.log('DISCLOSURE — read before installing');
86
+ console.log('');
87
+ console.log('HOW IT WORKS (v3 pull model):');
88
+ console.log(' 1. `terminalhire jobs` downloads an anonymous job index from the server');
89
+ console.log(' (GET /api/index — no dev data in the request).');
90
+ console.log(' 2. Matching runs LOCALLY against an encrypted profile on your device.');
91
+ console.log(' 3. The status bar shows a once-per-session nudge if matches exist.');
92
+ console.log(' It reads nothing from disk and makes zero network calls.');
93
+ console.log('');
94
+ console.log('YOUR LOCAL PROFILE (~/.terminalhire/profile.enc):');
95
+ console.log(' • Encrypted at rest with AES-256-GCM (Node built-in crypto).');
96
+ console.log(' • Key stored at ~/.terminalhire/key (0600) or OS keychain if keytar is installed.');
97
+ console.log(' • Accumulates closed-vocab skill tags from your personal project sessions.');
98
+ console.log(' • Employer-repo sessions contribute LANGUAGE TAGS ONLY (e.g. "typescript").');
99
+ console.log(' Fine-grained framework/infra tags are excluded in employer context.');
100
+ console.log(' • NEVER leaves your machine except in a consented lead payload (see below).');
101
+ console.log('');
102
+ console.log('WHAT LEAVES YOUR MACHINE:');
103
+ console.log(' • GET /api/index — anonymous. No dev data. No cookies.');
104
+ console.log(' • POST /api/lead — ONLY when you explicitly type "yes" to a named-entity');
105
+ console.log(' consent prompt for a SPECIFIC role. The prompt names the buyer as');
106
+ console.log(' "Coastal Recruiting LLC" and shows you the EXACT fields being sent.');
107
+ console.log(' Install does NOT equal consent. Each lead is a separate decision.');
108
+ console.log('');
109
+ console.log('GITHUB (optional — public data only):');
110
+ console.log(' • Scope: read:user + public repos. NEVER private-repo scopes.');
111
+ console.log(' • Token encrypted at ~/.terminalhire/github-token.enc (same AES-256-GCM).');
112
+ console.log(' • GitHub data enriches your LOCAL profile — no data leaves the machine');
113
+ console.log(' unless you consent to include GitHub fields in a specific terminalhire lead.');
114
+ console.log('');
115
+ console.log('LEAD PAYLOAD (what is sent on consent — nothing else):');
116
+ console.log(' {');
117
+ console.log(' opportunityId, // the specific job id you consented for');
118
+ console.log(' buyerId, // "coastal"');
119
+ console.log(' buyerLegalName, // "Coastal Recruiting LLC"');
120
+ console.log(' approvedFields: {');
121
+ console.log(' skillTags, // your closed-vocab skill tags');
122
+ console.log(' seniorityBand, // optional');
123
+ console.log(' displayName, // optional, only if you set it in your profile');
124
+ console.log(' contactEmail, // optional, only if you set it in your profile');
125
+ console.log(' note, // optional, typed at consent time');
126
+ console.log(' github: { // optional, ONLY if GitHub connected AND you say "yes"');
127
+ console.log(' login, // your GitHub username');
128
+ console.log(' profileUrl, // https://github.com/<login>');
129
+ console.log(' topLanguages, // top public repo languages');
130
+ console.log(' publicRepos, // public repo count');
131
+ console.log(' }');
132
+ console.log(' },');
133
+ console.log(' consentText, // verbatim text you approved');
134
+ console.log(' createdAt // ISO timestamp');
135
+ console.log(' }');
136
+ console.log('');
137
+ console.log('APPLY-DIRECT vs BUYER-LEAD:');
138
+ console.log(' Most roles in the index are "apply-direct" — jpi opens the employer URL.');
139
+ console.log(' NO data is sent for apply-direct roles. Only Coastal-repped roles');
140
+ console.log(' offer the named-entity consent flow.');
141
+ console.log('');
142
+ console.log('HOW TO DISABLE / DELETE:');
143
+ console.log(' • Uninstall statusLine: node install.js --uninstall');
144
+ console.log(' • Delete local profile: terminalhire profile --delete');
145
+ console.log(' • Clear GitHub token: terminalhire logout');
146
+ console.log(' • Wipe everything: rm -rf ~/.terminalhire');
147
+ console.log('');
148
+
149
+ const installAnswer = await ask('Install terminalhire v3.1? Type "yes" to continue: ');
150
+ if (installAnswer !== 'yes') {
151
+ console.log('\nAborted — nothing was changed.');
152
+ process.exit(0);
153
+ }
154
+
155
+ console.log('');
156
+ const settings = readSettings();
157
+
158
+ if (settings.statusLine === buildStatusLineEntry(BIN_PATH)) {
159
+ console.log('Already installed.');
160
+ } else {
161
+ if (settings.statusLine) {
162
+ console.log(` Note: replacing existing statusLine: ${settings.statusLine}`);
163
+ }
164
+ backupSettings();
165
+ settings.statusLine = buildStatusLineEntry(BIN_PATH);
166
+ writeSettings(settings);
167
+ console.log(' Written statusLine to ~/.claude/settings.json');
168
+ }
169
+
170
+ // ── GitHub onboarding step ─────────────────────────────────────────────────
171
+ console.log('');
172
+ console.log('┌─────────────────────────────────────────────────────────────────┐');
173
+ console.log('│ RECOMMENDED: Sign in with GitHub for instant, accurate matches │');
174
+ console.log('│ │');
175
+ console.log('│ terminalhire uses your PUBLIC GitHub profile (scope: read:user) to:│');
176
+ console.log('│ • Infer skill tags from public repo languages + topics │');
177
+ console.log('│ • Estimate seniority from account age + contribution volume │');
178
+ console.log('│ • Pre-fill identity fields (name, public email if set) │');
179
+ console.log('│ │');
180
+ console.log('│ Your token is encrypted locally. No data leaves your machine │');
181
+ console.log('│ until you explicitly consent to share it in a specific lead. │');
182
+ console.log('│ Scope: read:user — public repos only. NEVER private repos. │');
183
+ console.log('└─────────────────────────────────────────────────────────────────┘');
184
+ console.log('');
185
+
186
+ const githubAnswer = await ask(
187
+ 'Sign in with GitHub now? [Y/n/skip] (Enter = yes, "skip" or "n" = stay local): '
188
+ );
189
+ const doGitHub = githubAnswer === '' || githubAnswer === 'y' || githubAnswer === 'yes';
190
+
191
+ if (doGitHub) {
192
+ console.log('');
193
+ console.log(' Starting GitHub device flow...');
194
+ const _loginDistPath = resolve(join(__dirname, 'dist', 'bin', 'jpi-login.js'));
195
+ const _loginLegacyPath = resolve(join(__dirname, 'bin', 'jpi-login.js'));
196
+ const loginScript = existsSync(_loginDistPath) ? _loginDistPath : _loginLegacyPath;
197
+ const child = spawnSync(process.execPath, [loginScript, 'login'], {
198
+ stdio: ['inherit', 'inherit', 'inherit'],
199
+ env: process.env,
200
+ });
201
+ if (child.status !== 0) {
202
+ console.log('');
203
+ console.log(' GitHub sign-in did not complete. You can run `terminalhire login` any time.');
204
+ }
205
+ } else {
206
+ console.log('');
207
+ console.log(' Staying local-only. You can sign in any time with: terminalhire login');
208
+ console.log(' Your profile will still accumulate tags from personal project sessions.');
209
+ }
210
+
211
+ console.log('');
212
+ console.log('Done. Restart Claude Code to activate the status bar nudge.');
213
+ console.log('');
214
+ console.log(' Status bar format (once per session, only when matches exist):');
215
+ console.log(' ✦ N roles match your current work — run: terminalhire jobs');
216
+ console.log('');
217
+ console.log(' Commands:');
218
+ console.log(' terminalhire login — sign in with GitHub (enriches profile instantly)');
219
+ console.log(' terminalhire logout — clear GitHub token');
220
+ console.log(' terminalhire jobs — fetch index, match locally, browse roles');
221
+ console.log(' terminalhire profile --show — inspect your encrypted profile');
222
+ console.log(' terminalhire profile --edit — set displayName, contactEmail, prefs');
223
+ console.log(' terminalhire profile --delete — wipe profile and key');
224
+ console.log('');
225
+ }
226
+
227
+ // ── Uninstall ─────────────────────────────────────────────────────────────────
228
+
229
+ async function uninstall() {
230
+ console.log('');
231
+ console.log('terminalhire uninstall');
232
+ console.log('');
233
+
234
+ const settings = readSettings();
235
+
236
+ if (!settings.statusLine) {
237
+ console.log('No statusLine entry found — nothing to remove.');
238
+ process.exit(0);
239
+ }
240
+
241
+ console.log(` Current statusLine: ${settings.statusLine}`);
242
+ const answer = await ask('Remove this entry? Type "yes" to continue: ');
243
+ if (answer !== 'yes') {
244
+ console.log('\nAborted — nothing was changed.');
245
+ process.exit(0);
246
+ }
247
+
248
+ console.log('');
249
+ backupSettings();
250
+ delete settings.statusLine;
251
+ writeSettings(settings);
252
+
253
+ console.log(' Removed statusLine from ~/.claude/settings.json');
254
+ console.log('');
255
+ console.log(' Your profile is untouched. To also delete it:');
256
+ console.log(' terminalhire profile --delete');
257
+ console.log(' rm -rf ~/.terminalhire');
258
+ console.log('');
259
+ console.log('Done.');
260
+ console.log('');
261
+ }
262
+
263
+ // ── Entry ─────────────────────────────────────────────────────────────────────
264
+
265
+ if (UNINSTALL) {
266
+ uninstall().catch(err => {
267
+ console.error('Uninstall error:', err.message);
268
+ process.exit(1);
269
+ });
270
+ } else {
271
+ install().catch(err => {
272
+ console.error('Install error:', err.message);
273
+ process.exit(1);
274
+ });
275
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "terminalhire",
3
+ "version": "0.1.0",
4
+ "description": "Local-first job matching for developers — Claude Code statusLine integration",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "bin": {
10
+ "terminalhire": "./dist/bin/jpi-dispatch.js",
11
+ "th": "./dist/bin/jpi-dispatch.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "fixtures",
16
+ "install.js",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "prepublishOnly": "npm run build",
22
+ "install-hook": "node install.js"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "keywords": [
28
+ "terminalhire",
29
+ "jobs",
30
+ "developer",
31
+ "claude",
32
+ "claude-code",
33
+ "job-matching",
34
+ "local-first"
35
+ ],
36
+ "author": "staqs",
37
+ "license": "MIT",
38
+ "devDependencies": {
39
+ "tsup": "^8.5.1",
40
+ "@types/node": "^20.12.0",
41
+ "typescript": "^5.4.5"
42
+ }
43
+ }