terminalhire 0.2.0 → 0.2.3

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.
@@ -157,10 +157,12 @@ function clearSpinnerVerbs() {
157
157
  }
158
158
  return { cleared: true, keptUserVerbs };
159
159
  }
160
- function buildTips(topMatches, baseUrl, max = 3) {
160
+ function buildTips(topMatches, baseUrl, max = 8) {
161
161
  const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
162
162
  const out = [];
163
- const seen = /* @__PURE__ */ new Set();
163
+ const seenRole = /* @__PURE__ */ new Set();
164
+ const perCompany = /* @__PURE__ */ new Map();
165
+ const COMPANY_CAP = 2;
164
166
  for (const m of Array.isArray(topMatches) ? topMatches : []) {
165
167
  if (!m || !m.title || !m.company || !m.id) continue;
166
168
  const idx = String(m.id).indexOf(":");
@@ -168,14 +170,20 @@ function buildTips(topMatches, baseUrl, max = 3) {
168
170
  const source = String(m.id).slice(0, idx);
169
171
  const ext = String(m.id).slice(idx + 1);
170
172
  if (!source || !ext) continue;
171
- const key = `${source}/${ext}`;
172
- if (seen.has(key)) continue;
173
- seen.add(key);
174
- let title = String(m.title).trim().replace(/\s+/g, " ");
173
+ const companyRaw = String(m.company).trim().replace(/\s+/g, " ");
174
+ const titleRaw = String(m.title).trim().replace(/\s+/g, " ");
175
+ const roleKey = `${titleRaw.toLowerCase()}@${companyRaw.toLowerCase()}`;
176
+ const coKey = companyRaw.toLowerCase();
177
+ if (seenRole.has(roleKey)) continue;
178
+ if ((perCompany.get(coKey) || 0) >= COMPANY_CAP) continue;
179
+ seenRole.add(roleKey);
180
+ perCompany.set(coKey, (perCompany.get(coKey) || 0) + 1);
181
+ let title = titleRaw;
175
182
  if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
176
- const company = titleCase(String(m.company).trim().replace(/\s+/g, " "));
183
+ const company = titleCase(companyRaw);
177
184
  const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
178
- const url = `${base}/j/${source}/${encodeURIComponent(ext)}`;
185
+ const token = Buffer.from(String(m.id)).toString("base64url");
186
+ const url = `${base}/j/${token}`;
179
187
  out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
180
188
  if (out.length >= max) break;
181
189
  }
package/install.js CHANGED
@@ -1,26 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * install.js — v3.1 installer for terminalhire
3
+ * install.js — installer for terminalhire (ambient spinner job surface)
4
4
  *
5
5
  * ToS / safety guardrails (binding — see CONSTITUTION.md Rule 7 and Rule 12):
6
- * - ONLY uses the official Claude Code `statusLine` settings.json hook.
6
+ * - ONLY uses the official Claude Code spinner settings (spinnerVerbs +
7
+ * spinnerTipsOverride) via the spinner module. NEVER writes settings.statusLine.
7
8
  * - NEVER silently modifies ~/.claude/settings.json.
8
9
  * - Always backs up settings.json (timestamped) before any write.
9
- * - Prints a clear disclosure of exactly what it changed.
10
- * - Provides one-command uninstall that restores the backup.
11
- * - CHAIN, never clobber: if a statusLine already exists that is NOT ours,
12
- * creates a wrapper script that runs BOTH commands, then points settings.json
13
- * at the wrapper. Idempotent — detects existing wrapper and does not double-wrap.
10
+ * - Prints a clear disclosure of exactly what it changes.
11
+ * - Requires an explicit typed "yes" before touching any system file.
12
+ * - Provides one-command uninstall that clears our spinner verbs + tips.
14
13
  * - postinstall (not this file) is print-only and never calls this file.
15
14
  *
16
15
  * What it does:
17
- * 1. Prints full v3 disclosure
16
+ * 1. Prints full disclosure (ambient spinner job surface only)
18
17
  * 2. Requires explicit "yes" before touching any system file
19
18
  * 3. Backs up ~/.claude/settings.json (timestamped)
20
- * 4. If no existing statusLine sets terminalhire's nudge directly
21
- * If existing statusLine (not ours) builds a wrapper that chains both
22
- * 5. Supports --uninstall (restores the backup / removes wrapper)
23
- * 7. Idempotent: safe to run multiple times
19
+ * 4. Enables the ambient spinner job surface (spinnerVerbs + spinnerTipsOverride)
20
+ * 5. Supports --uninstall (clears our spinner verbs + tips, disables spinner)
21
+ * 6. Idempotent: safe to run multiple times
24
22
  *
25
23
  * Usage:
26
24
  * node install.js
@@ -28,29 +26,22 @@
28
26
  */
29
27
 
30
28
  import {
31
- readFileSync, writeFileSync, copyFileSync, existsSync,
32
- mkdirSync, chmodSync, readdirSync,
29
+ readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync,
33
30
  } from 'node:fs';
34
31
  import { homedir } from 'node:os';
35
32
  import { join, resolve, dirname } from 'node:path';
36
33
  import { fileURLToPath, pathToFileURL } from 'node:url';
37
34
  import { createInterface } from 'node:readline';
38
- import { spawnSync } from 'node:child_process';
39
35
 
40
36
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
41
37
 
42
- // Resolve the nudge bin robustly: prefer the bundled dist output (published package),
43
- // fall back to the legacy bin/ path for in-workspace / development installs.
44
- const _distBin = resolve(join(__dirname, 'dist', 'bin', 'jpi.js'));
45
- const _legacyBin = resolve(join(__dirname, 'bin', 'jpi.js'));
46
- const BIN_PATH = existsSync(_distBin) ? _distBin : _legacyBin;
47
-
48
38
  const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
49
39
  const SETTINGS_DIR = dirname(SETTINGS_PATH);
50
40
  const TERMINALHIRE_DIR = join(homedir(), '.terminalhire');
51
- const WRAPPER_PATH = join(TERMINALHIRE_DIR, 'statusline-wrapper.sh');
52
41
  const CONFIG_FILE = join(TERMINALHIRE_DIR, 'config.json');
53
42
 
43
+ const UNINSTALL = process.argv.includes('--uninstall');
44
+
54
45
  // Resolve the spinner module (dist preferred; bin fallback for the dev workspace).
55
46
  async function loadSpinnerModule() {
56
47
  const candidates = [
@@ -77,27 +68,8 @@ function patchConfig(patch) {
77
68
  writeFileSync(CONFIG_FILE, JSON.stringify({ ...cfg, ...patch }, null, 2) + '\n', 'utf8');
78
69
  }
79
70
 
80
- // The existing statusLine command on the user's machine that we must preserve
81
- const KNOWN_EXISTING_STATUSLINE = 'bash /Users/ericgang/.claude/statusline-command.sh';
82
-
83
- const UNINSTALL = process.argv.includes('--uninstall');
84
-
85
- // ── Sentinel comment embedded in the wrapper so we can detect our own wrappers
86
- const WRAPPER_SENTINEL = '# terminalhire-wrapper-v1';
87
-
88
71
  // ── Helpers ───────────────────────────────────────────────────────────────────
89
72
 
90
- function readSettings() {
91
- if (!existsSync(SETTINGS_PATH)) return {};
92
- try {
93
- return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
94
- } catch {
95
- console.error('Error: ~/.claude/settings.json exists but is not valid JSON.');
96
- console.error('Please fix it manually before running this installer.');
97
- process.exit(1);
98
- }
99
- }
100
-
101
73
  function backupSettings() {
102
74
  if (!existsSync(SETTINGS_PATH)) return null;
103
75
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
@@ -107,11 +79,6 @@ function backupSettings() {
107
79
  return backupPath;
108
80
  }
109
81
 
110
- function writeSettings(settings) {
111
- mkdirSync(SETTINGS_DIR, { recursive: true });
112
- writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8');
113
- }
114
-
115
82
  function ask(question) {
116
83
  const rl = createInterface({ input: process.stdin, output: process.stdout });
117
84
  return new Promise(res => {
@@ -122,124 +89,32 @@ function ask(question) {
122
89
  });
123
90
  }
124
91
 
125
- /** Build the direct (no-chain) statusLine entry for terminalhire. */
126
- function buildDirectEntry(binPath) {
127
- return `node ${binPath}`;
128
- }
129
-
130
- /**
131
- * Returns true if the given statusLine value is terminalhire's own entry:
132
- * either the direct node entry or our wrapper script.
133
- */
134
- function isOurEntry(statusLine) {
135
- if (!statusLine) return false;
136
- // Direct entry
137
- if (statusLine === buildDirectEntry(BIN_PATH)) return true;
138
- // Points at our wrapper
139
- if (statusLine === `bash ${WRAPPER_PATH}`) return true;
140
- // Wrapper exists and contains our sentinel
141
- if (existsSync(WRAPPER_PATH)) {
142
- try {
143
- const wrapperContent = readFileSync(WRAPPER_PATH, 'utf8');
144
- return wrapperContent.includes(WRAPPER_SENTINEL);
145
- } catch { /* fall through */ }
146
- }
147
- return false;
148
- }
149
-
150
- /**
151
- * Normalize a statusLine value that may be a string or an object
152
- * (e.g. {type:"command", command:"..."} from Claude Code's settings).
153
- * Returns the shell command string, or null if the value is unusable.
154
- */
155
- function extractCommand(statusLine) {
156
- if (!statusLine) return null;
157
- if (typeof statusLine === 'string') return statusLine;
158
- // Object form: {type: "command", command: "..."}
159
- if (typeof statusLine === 'object' && typeof statusLine.command === 'string') {
160
- return statusLine.command;
161
- }
162
- return null;
163
- }
164
-
165
- /**
166
- * Build the chaining wrapper shell script.
167
- * Both the existing command and the terminalhire nudge receive the SAME stdin JSON.
168
- * Output: existing command's output first, then terminalhire's output.
169
- * existingCmd MUST be a plain string (call extractCommand first).
170
- */
171
- function buildWrapper(existingCmd) {
172
- return `#!/usr/bin/env bash
173
- ${WRAPPER_SENTINEL}
174
- # Chains the original statusLine command with terminalhire's nudge.
175
- # Both receive the same stdin JSON. Original output prints first, then terminalhire.
176
- # Generated by: node install.js
177
- # Existing command: ${existingCmd}
178
- # terminalhire nudge: node ${BIN_PATH}
179
-
180
- # Read stdin once into a variable
181
- INPUT=$(cat)
182
-
183
- # Run the existing statusLine command
184
- EXISTING_OUT=$(printf '%s' "$INPUT" | ${existingCmd} 2>/dev/null || true)
185
-
186
- # Run terminalhire's nudge
187
- TH_OUT=$(printf '%s' "$INPUT" | node ${BIN_PATH} 2>/dev/null || true)
188
-
189
- # Print existing output (if any), then terminalhire output (if any)
190
- if [ -n "$EXISTING_OUT" ]; then
191
- printf '%s\\n' "$EXISTING_OUT"
192
- fi
193
- if [ -n "$TH_OUT" ]; then
194
- printf '%s\\n' "$TH_OUT"
195
- fi
196
- `;
197
- }
198
-
199
- /**
200
- * Find the most recent terminalhire backup of settings.json.
201
- * Returns the path or null.
202
- */
203
- function findLatestBackup() {
204
- try {
205
- const files = readdirSync(SETTINGS_DIR)
206
- .filter(f => f.startsWith('settings.json.terminalhire-backup-'))
207
- .sort()
208
- .reverse();
209
- if (files.length === 0) return null;
210
- return join(SETTINGS_DIR, files[0]);
211
- } catch {
212
- return null;
213
- }
214
- }
215
-
216
92
  // ── Install ───────────────────────────────────────────────────────────────────
217
93
 
218
94
  async function install() {
219
95
  console.log('');
220
96
  console.log('┌─────────────────────────────────────────────────────────────────┐');
221
- console.log('│ terminalhire v0.1.1 install statusLine hook │');
222
- console.log('│ Pull your matches. Your profile stays on-device. │');
97
+ console.log('│ terminalhire — enable the ambient spinner job surface │');
98
+ console.log('│ Pull your matches. Your profile stays on-device. │');
223
99
  console.log('└─────────────────────────────────────────────────────────────────┘');
224
100
  console.log('');
225
101
  console.log('DISCLOSURE — read before installing');
226
102
  console.log('');
227
- console.log('HOW IT WORKS (v3 pull model):');
103
+ console.log('HOW IT WORKS (pull model):');
228
104
  console.log(' 1. `terminalhire jobs` downloads an anonymous job index from the server');
229
105
  console.log(' (GET /api/index — no dev data in the request).');
230
106
  console.log(' 2. Matching runs LOCALLY against an encrypted profile on your device.');
231
- console.log(' 3. The status bar shows a nudge when matches exist.');
107
+ console.log(' 3. The ambient spinner surfaces your top matches while Claude works.');
232
108
  console.log(' It reads only local files and makes zero network calls.');
233
- console.log(' 4. Nudge frequency: configurable via `terminalhire config --nudge`.');
234
- console.log(' Default: once per session. Options: always | every:N.');
235
109
  console.log('');
236
- console.log(' 5. SPINNER JOB SURFACE (enabled by this install):');
110
+ console.log(' AMBIENT SPINNER JOB SURFACE (enabled by this install):');
237
111
  console.log(' While Claude is working, the spinner line shows your top LOCAL job');
238
112
  console.log(' matches, e.g. Senior Backend Engineer @ Stripe · 82% …');
239
113
  console.log(' The "tip" line below it shows a ⌘-clickable link to open the listing');
240
114
  console.log(' (a terminalhire.com/j/… redirect — clicks are logged anonymously, no');
241
115
  console.log(' profile data). Uses official `spinnerVerbs`/`spinnerTipsOverride` settings');
242
116
  console.log(' (no patching, Rule 7). Computed locally, zero egress — only public job text.');
117
+ console.log(' This install does NOT write any statusLine — the spinner is the only surface.');
243
118
  console.log(' Turn it off any time: terminalhire spinner --off');
244
119
  console.log('');
245
120
  console.log('YOUR LOCAL PROFILE (~/.terminalhire/profile.enc):');
@@ -263,44 +138,20 @@ async function install() {
263
138
  console.log(' • GitHub data enriches your LOCAL profile — no data leaves the machine');
264
139
  console.log(' unless you consent to include GitHub fields in a specific terminalhire lead.');
265
140
  console.log('');
266
- console.log('HOW TO DISABLE / DELETE:');
267
- console.log(' • Uninstall statusLine: node install.js --uninstall');
268
- console.log(' Delete local profile: terminalhire profile --delete');
269
- console.log(' • Clear GitHub token: terminalhire logout');
270
- console.log(' • Wipe everything: rm -rf ~/.terminalhire');
141
+ console.log('WHAT THIS INSTALL CHANGES:');
142
+ console.log(' • ~/.claude/settings.json enables the spinner job surface only');
143
+ console.log(' (spinnerVerbs + spinnerTipsOverride). It does NOT write statusLine.');
144
+ console.log(' • A timestamped backup is created before any change.');
271
145
  console.log('');
272
-
273
- const settings = readSettings();
274
- // Normalize: settings.statusLine may be a string or an object {type,command}
275
- const currentStatusLine = extractCommand(settings.statusLine);
276
-
277
- // Already installed?
278
- if (isOurEntry(currentStatusLine)) {
279
- console.log('Already installed (statusLine is already terminalhire or our wrapper).');
280
- console.log('');
281
- console.log('To change nudge frequency: terminalhire config --nudge <session|always|every:N>');
282
- console.log('To uninstall: node install.js --uninstall');
283
- console.log('');
284
- return;
285
- }
286
-
287
- // Determine install strategy
288
- let installStrategy;
289
- if (!currentStatusLine) {
290
- installStrategy = 'direct';
291
- console.log(' No existing statusLine found.');
292
- console.log(` Will set: statusLine = "node ${BIN_PATH}"`);
293
- } else {
294
- installStrategy = 'chain';
295
- console.log(` Existing statusLine detected: ${currentStatusLine}`);
296
- console.log(' Will create a wrapper at: ~/.terminalhire/statusline-wrapper.sh');
297
- console.log(' The wrapper runs BOTH commands (existing first, terminalhire second).');
298
- console.log(' Both receive the same stdin JSON from Claude Code.');
299
- console.log(` Will set: statusLine = "bash ${WRAPPER_PATH}"`);
300
- }
146
+ console.log('HOW TO DISABLE / DELETE:');
147
+ console.log(' • Disable spinner surface: node install.js --uninstall');
148
+ console.log(' • (or) terminalhire spinner --off');
149
+ console.log(' • Delete local profile: terminalhire profile --delete');
150
+ console.log(' • Clear GitHub token: terminalhire logout');
151
+ console.log(' • Wipe everything: rm -rf ~/.terminalhire');
301
152
  console.log('');
302
153
 
303
- const installAnswer = await ask('Install terminalhire statusLine hook? Type "yes" to continue: ');
154
+ const installAnswer = await ask('Enable the terminalhire ambient spinner job surface? Type "yes" to continue: ');
304
155
  if (installAnswer !== 'yes') {
305
156
  console.log('\nAborted — nothing was changed.');
306
157
  process.exit(0);
@@ -309,32 +160,10 @@ async function install() {
309
160
  console.log('');
310
161
  const backupPath = backupSettings();
311
162
 
312
- if (installStrategy === 'direct') {
313
- settings.statusLine = buildDirectEntry(BIN_PATH);
314
- writeSettings(settings);
315
- console.log(' Set statusLine in ~/.claude/settings.json');
316
- console.log(` → node ${BIN_PATH}`);
317
- } else {
318
- // Write wrapper
319
- mkdirSync(TERMINALHIRE_DIR, { recursive: true });
320
- const wrapperContent = buildWrapper(currentStatusLine);
321
- writeFileSync(WRAPPER_PATH, wrapperContent, 'utf8');
322
- chmodSync(WRAPPER_PATH, 0o755);
323
- console.log(` Created wrapper: ${WRAPPER_PATH}`);
324
-
325
- settings.statusLine = `bash ${WRAPPER_PATH}`;
326
- writeSettings(settings);
327
- console.log(' Updated statusLine in ~/.claude/settings.json');
328
- console.log(` → bash ${WRAPPER_PATH}`);
329
- console.log(' (chains existing command + terminalhire nudge)');
330
- }
331
-
332
- if (backupPath) {
333
- console.log(` Backup: ${backupPath}`);
334
- }
335
-
336
163
  // Enable the spinner job surface as part of this single consented install
337
- // (Rule 10, amended: install IS the opt-in). Best-effort never blocks install.
164
+ // (Rule 12: disclosure + timestamped backup + typed "yes" above). The spinner
165
+ // module is the ONLY writer of ~/.claude/settings.json here — verbs + tips only.
166
+ let enabled = false;
338
167
  try {
339
168
  patchConfig({ spinner: { enabled: true, mode: 'replace', max: 6 } });
340
169
  const spinnerMod = await loadSpinnerModule();
@@ -344,33 +173,55 @@ async function install() {
344
173
  const cache = JSON.parse(readFileSync(join(TERMINALHIRE_DIR, 'index-cache.json'), 'utf8'));
345
174
  if (Array.isArray(cache.topMatches)) topMatches = cache.topMatches;
346
175
  } catch { /* no cache yet — monitor will populate it */ }
176
+
347
177
  const verbs = spinnerMod.buildSpinnerPool(topMatches, 6);
348
178
  if (verbs.length > 0) spinnerMod.applySpinnerVerbs(verbs, 'replace');
179
+
180
+ // Tips line: ⌘-clickable listing links for the same top matches.
181
+ let tipsCount = 0;
182
+ try {
183
+ const tips = spinnerMod.buildTips(topMatches, 'https://terminalhire.com', 8);
184
+ if (Array.isArray(tips) && tips.length > 0) {
185
+ spinnerMod.applySpinnerTips(tips);
186
+ tipsCount = tips.length;
187
+ }
188
+ } catch { /* tips are best-effort */ }
189
+
190
+ enabled = true;
349
191
  console.log(
350
192
  ' Spinner job surface: ENABLED' +
351
193
  (verbs.length
352
194
  ? ` (${verbs.length} match${verbs.length === 1 ? '' : 'es'} live now)`
353
195
  : ' (matches appear after the first background refresh)')
354
196
  );
197
+ if (tipsCount > 0) {
198
+ console.log(` Tip links: ${tipsCount} ⌘-clickable listing${tipsCount === 1 ? '' : 's'}`);
199
+ }
355
200
  console.log(' Turn off any time: terminalhire spinner --off');
356
201
  }
357
202
  } catch { /* spinner is best-effort; never block the install */ }
358
203
 
204
+ if (backupPath) {
205
+ console.log(` Backup: ${backupPath}`);
206
+ }
207
+
208
+ if (!enabled) {
209
+ console.log(' Could not load the spinner module — nothing was enabled.');
210
+ console.log(' Run `terminalhire spinner --on` after building, or reinstall the package.');
211
+ }
212
+
359
213
  console.log('');
360
- console.log('Done. Restart Claude Code to activate the status bar nudge.');
361
- console.log('');
362
- console.log(' Status bar format (default: once per session, only when matches exist):');
363
- console.log(' ✦ N roles match your current work — run: terminalhire jobs');
214
+ console.log('Done. Restart Claude Code to activate the ambient spinner job surface.');
364
215
  console.log('');
365
- console.log(' Nudge frequency:');
366
- console.log(' terminalhire config --nudge session (default once per session)');
367
- console.log(' terminalhire config --nudge always (every statusLine render)');
368
- console.log(' terminalhire config --nudge every:N (every Nth render)');
216
+ console.log(' While Claude works, the spinner shows your top LOCAL matches, e.g.');
217
+ console.log(' Senior Backend Engineer @ Stripe · 82% ');
218
+ console.log(' with a ⌘-clickable tip link to open the listing.');
369
219
  console.log('');
370
220
  console.log(' Other commands:');
371
221
  console.log(' terminalhire login — sign in with GitHub (enriches profile instantly)');
372
222
  console.log(' terminalhire logout — clear GitHub token');
373
223
  console.log(' terminalhire jobs — fetch index, match locally, browse roles');
224
+ console.log(' terminalhire spinner --off — disable the ambient spinner surface');
374
225
  console.log(' terminalhire profile --show — inspect your encrypted profile');
375
226
  console.log(' terminalhire profile --edit — set displayName, contactEmail, prefs');
376
227
  console.log(' terminalhire profile --delete — wipe profile and key');
@@ -383,68 +234,41 @@ async function uninstall() {
383
234
  console.log('');
384
235
  console.log('terminalhire uninstall');
385
236
  console.log('');
386
-
387
- const settings = readSettings();
388
-
389
- if (!settings.statusLine) {
390
- console.log('No statusLine entry found — nothing to remove.');
391
- process.exit(0);
392
- }
393
-
394
- console.log(` Current statusLine: ${settings.statusLine}`);
395
-
396
- // Check if there is a backup to restore
397
- const latestBackup = findLatestBackup();
398
- if (latestBackup) {
399
- console.log(` Backup found: ${latestBackup}`);
400
- console.log(' Uninstalling will restore this backup (restoring original statusLine).');
401
- } else {
402
- console.log(' No backup found — statusLine entry will be deleted (set to none).');
403
- }
237
+ console.log(' This disables the ambient spinner job surface and clears the spinner');
238
+ console.log(' verbs + tips terminalhire added (any verbs/tips you set yourself are kept).');
239
+ console.log(' It does NOT touch settings.statusLine.');
404
240
  console.log('');
405
241
 
406
- const answer = await ask('Uninstall terminalhire hook? Type "yes" to continue: ');
242
+ const answer = await ask('Disable the terminalhire spinner job surface? Type "yes" to continue: ');
407
243
  if (answer !== 'yes') {
408
244
  console.log('\nAborted — nothing was changed.');
409
245
  process.exit(0);
410
246
  }
411
247
 
412
248
  console.log('');
413
-
414
- if (latestBackup) {
415
- // Restore the backup
416
- copyFileSync(latestBackup, SETTINGS_PATH);
417
- console.log(` Restored settings from backup: ${latestBackup}`);
418
- } else {
419
- // Just remove the statusLine key
420
- backupSettings();
421
- delete settings.statusLine;
422
- writeSettings(settings);
423
- console.log(' Removed statusLine from ~/.claude/settings.json');
424
- }
425
-
426
- // Remove wrapper if it exists and is ours
427
- if (existsSync(WRAPPER_PATH)) {
428
- try {
429
- const wrapperContent = readFileSync(WRAPPER_PATH, 'utf8');
430
- if (wrapperContent.includes(WRAPPER_SENTINEL)) {
431
- // We don't delete it automatically — user may have customized it
432
- console.log(` Wrapper file left in place: ${WRAPPER_PATH}`);
433
- console.log(' (Remove manually if desired: rm ~/.terminalhire/statusline-wrapper.sh)');
434
- }
435
- } catch { /* ignore */ }
249
+ const backupPath = backupSettings();
250
+ if (backupPath) {
251
+ console.log(` Backup: ${backupPath}`);
436
252
  }
437
253
 
438
- // Remove the spinner job verbs we injected (preserving any the user set themselves).
254
+ // Remove the spinner job verbs + tips we injected (preserving any the user set
255
+ // themselves) and disable the surface in our config.
439
256
  try {
440
257
  patchConfig({ spinner: { enabled: false } });
441
258
  const spinnerMod = await loadSpinnerModule();
442
259
  if (spinnerMod) {
443
- const res = spinnerMod.clearSpinnerVerbs();
260
+ const vres = spinnerMod.clearSpinnerVerbs();
444
261
  console.log(
445
262
  ' Removed spinner job verbs' +
446
- (res && res.keptUserVerbs ? ` (kept ${res.keptUserVerbs} of your own)` : '') + '.'
263
+ (vres && vres.keptUserVerbs ? ` (kept ${vres.keptUserVerbs} of your own)` : '') + '.'
447
264
  );
265
+ try {
266
+ const tres = spinnerMod.clearSpinnerTips();
267
+ console.log(
268
+ ' Removed spinner tip links' +
269
+ (tres && tres.keptUserTips ? ` (kept ${tres.keptUserTips} of your own)` : '') + '.'
270
+ );
271
+ } catch { /* tips clear is best-effort */ }
448
272
  }
449
273
  } catch { /* best-effort */ }
450
274
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.2.0",
4
- "description": "Local-first job matching for developers — Claude Code statusLine nudge and ambient spinner job surface",
3
+ "version": "0.2.3",
4
+ "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/staqsIO/terminalhire.git"
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "build": "tsup",
30
- "bundle:plugin": "npm run build && rm -rf ../../plugins/terminalhire/dist && cp -R dist ../../plugins/terminalhire/dist && cp package.json ../../plugins/terminalhire/dist/package.json",
30
+ "bundle:plugin": "npm run build && rm -rf ../../plugins/terminalhire/dist && cp -R dist ../../plugins/terminalhire/dist && cp package.json ../../plugins/terminalhire/dist/package.json && cp install.js ../../plugins/terminalhire/dist/install.js && cp postinstall.js ../../plugins/terminalhire/dist/postinstall.js",
31
31
  "prepublishOnly": "npm run build",
32
32
  "install-hook": "node install.js",
33
33
  "postinstall": "node ./postinstall.js"