terminalhire 0.2.2 → 0.2.4
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/dist/bin/jpi-dispatch.js +254 -53
- package/dist/bin/jpi-init.js +5 -5
- package/dist/bin/jpi-jobs.js +220 -47
- package/dist/bin/jpi-learn.js +118 -0
- package/dist/bin/jpi-login.js +220 -47
- package/dist/bin/jpi-profile.js +118 -0
- package/dist/bin/jpi-refresh.js +249 -48
- package/dist/bin/jpi-save.js +118 -0
- package/dist/bin/jpi-sync.js +118 -0
- package/dist/bin/spinner.js +29 -1
- package/dist/src/profile.js +110 -0
- package/dist/src/signal.js +110 -0
- package/install.js +81 -260
- package/package.json +3 -3
package/install.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* install.js —
|
|
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
|
|
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
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
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
|
|
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.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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('│
|
|
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 (
|
|
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
|
|
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('
|
|
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('
|
|
267
|
-
console.log(' •
|
|
268
|
-
console.log('
|
|
269
|
-
console.log(' •
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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('
|
|
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,35 +160,10 @@ async function install() {
|
|
|
309
160
|
console.log('');
|
|
310
161
|
const backupPath = backupSettings();
|
|
311
162
|
|
|
312
|
-
if (installStrategy === 'direct') {
|
|
313
|
-
// Claude Code requires statusLine as an object { type, command } — NOT a bare
|
|
314
|
-
// string (a string fails schema validation and makes Claude skip ALL settings).
|
|
315
|
-
settings.statusLine = { type: 'command', command: buildDirectEntry(BIN_PATH) };
|
|
316
|
-
writeSettings(settings);
|
|
317
|
-
console.log(' Set statusLine in ~/.claude/settings.json');
|
|
318
|
-
console.log(` → node ${BIN_PATH}`);
|
|
319
|
-
} else {
|
|
320
|
-
// Write wrapper
|
|
321
|
-
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
322
|
-
const wrapperContent = buildWrapper(currentStatusLine);
|
|
323
|
-
writeFileSync(WRAPPER_PATH, wrapperContent, 'utf8');
|
|
324
|
-
chmodSync(WRAPPER_PATH, 0o755);
|
|
325
|
-
console.log(` Created wrapper: ${WRAPPER_PATH}`);
|
|
326
|
-
|
|
327
|
-
// Object form { type, command } — a bare string breaks Claude Code's settings schema.
|
|
328
|
-
settings.statusLine = { type: 'command', command: `bash ${WRAPPER_PATH}` };
|
|
329
|
-
writeSettings(settings);
|
|
330
|
-
console.log(' Updated statusLine in ~/.claude/settings.json');
|
|
331
|
-
console.log(` → bash ${WRAPPER_PATH}`);
|
|
332
|
-
console.log(' (chains existing command + terminalhire nudge)');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (backupPath) {
|
|
336
|
-
console.log(` Backup: ${backupPath}`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
163
|
// Enable the spinner job surface as part of this single consented install
|
|
340
|
-
// (Rule
|
|
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;
|
|
341
167
|
try {
|
|
342
168
|
patchConfig({ spinner: { enabled: true, mode: 'replace', max: 6 } });
|
|
343
169
|
const spinnerMod = await loadSpinnerModule();
|
|
@@ -347,33 +173,55 @@ async function install() {
|
|
|
347
173
|
const cache = JSON.parse(readFileSync(join(TERMINALHIRE_DIR, 'index-cache.json'), 'utf8'));
|
|
348
174
|
if (Array.isArray(cache.topMatches)) topMatches = cache.topMatches;
|
|
349
175
|
} catch { /* no cache yet — monitor will populate it */ }
|
|
176
|
+
|
|
350
177
|
const verbs = spinnerMod.buildSpinnerPool(topMatches, 6);
|
|
351
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;
|
|
352
191
|
console.log(
|
|
353
192
|
' Spinner job surface: ENABLED' +
|
|
354
193
|
(verbs.length
|
|
355
194
|
? ` (${verbs.length} match${verbs.length === 1 ? '' : 'es'} live now)`
|
|
356
195
|
: ' (matches appear after the first background refresh)')
|
|
357
196
|
);
|
|
197
|
+
if (tipsCount > 0) {
|
|
198
|
+
console.log(` Tip links: ${tipsCount} ⌘-clickable listing${tipsCount === 1 ? '' : 's'}`);
|
|
199
|
+
}
|
|
358
200
|
console.log(' Turn off any time: terminalhire spinner --off');
|
|
359
201
|
}
|
|
360
202
|
} catch { /* spinner is best-effort; never block the install */ }
|
|
361
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
|
+
|
|
362
213
|
console.log('');
|
|
363
|
-
console.log('Done. Restart Claude Code to activate the
|
|
364
|
-
console.log('');
|
|
365
|
-
console.log(' Status bar format (default: once per session, only when matches exist):');
|
|
366
|
-
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.');
|
|
367
215
|
console.log('');
|
|
368
|
-
console.log('
|
|
369
|
-
console.log('
|
|
370
|
-
console.log('
|
|
371
|
-
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.');
|
|
372
219
|
console.log('');
|
|
373
220
|
console.log(' Other commands:');
|
|
374
221
|
console.log(' terminalhire login — sign in with GitHub (enriches profile instantly)');
|
|
375
222
|
console.log(' terminalhire logout — clear GitHub token');
|
|
376
223
|
console.log(' terminalhire jobs — fetch index, match locally, browse roles');
|
|
224
|
+
console.log(' terminalhire spinner --off — disable the ambient spinner surface');
|
|
377
225
|
console.log(' terminalhire profile --show — inspect your encrypted profile');
|
|
378
226
|
console.log(' terminalhire profile --edit — set displayName, contactEmail, prefs');
|
|
379
227
|
console.log(' terminalhire profile --delete — wipe profile and key');
|
|
@@ -386,68 +234,41 @@ async function uninstall() {
|
|
|
386
234
|
console.log('');
|
|
387
235
|
console.log('terminalhire uninstall');
|
|
388
236
|
console.log('');
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (!settings.statusLine) {
|
|
393
|
-
console.log('No statusLine entry found — nothing to remove.');
|
|
394
|
-
process.exit(0);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
console.log(` Current statusLine: ${settings.statusLine}`);
|
|
398
|
-
|
|
399
|
-
// Check if there is a backup to restore
|
|
400
|
-
const latestBackup = findLatestBackup();
|
|
401
|
-
if (latestBackup) {
|
|
402
|
-
console.log(` Backup found: ${latestBackup}`);
|
|
403
|
-
console.log(' Uninstalling will restore this backup (restoring original statusLine).');
|
|
404
|
-
} else {
|
|
405
|
-
console.log(' No backup found — statusLine entry will be deleted (set to none).');
|
|
406
|
-
}
|
|
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.');
|
|
407
240
|
console.log('');
|
|
408
241
|
|
|
409
|
-
const answer = await ask('
|
|
242
|
+
const answer = await ask('Disable the terminalhire spinner job surface? Type "yes" to continue: ');
|
|
410
243
|
if (answer !== 'yes') {
|
|
411
244
|
console.log('\nAborted — nothing was changed.');
|
|
412
245
|
process.exit(0);
|
|
413
246
|
}
|
|
414
247
|
|
|
415
248
|
console.log('');
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
copyFileSync(latestBackup, SETTINGS_PATH);
|
|
420
|
-
console.log(` Restored settings from backup: ${latestBackup}`);
|
|
421
|
-
} else {
|
|
422
|
-
// Just remove the statusLine key
|
|
423
|
-
backupSettings();
|
|
424
|
-
delete settings.statusLine;
|
|
425
|
-
writeSettings(settings);
|
|
426
|
-
console.log(' Removed statusLine from ~/.claude/settings.json');
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Remove wrapper if it exists and is ours
|
|
430
|
-
if (existsSync(WRAPPER_PATH)) {
|
|
431
|
-
try {
|
|
432
|
-
const wrapperContent = readFileSync(WRAPPER_PATH, 'utf8');
|
|
433
|
-
if (wrapperContent.includes(WRAPPER_SENTINEL)) {
|
|
434
|
-
// We don't delete it automatically — user may have customized it
|
|
435
|
-
console.log(` Wrapper file left in place: ${WRAPPER_PATH}`);
|
|
436
|
-
console.log(' (Remove manually if desired: rm ~/.terminalhire/statusline-wrapper.sh)');
|
|
437
|
-
}
|
|
438
|
-
} catch { /* ignore */ }
|
|
249
|
+
const backupPath = backupSettings();
|
|
250
|
+
if (backupPath) {
|
|
251
|
+
console.log(` Backup: ${backupPath}`);
|
|
439
252
|
}
|
|
440
253
|
|
|
441
|
-
// Remove the spinner job verbs we injected (preserving any the user set
|
|
254
|
+
// Remove the spinner job verbs + tips we injected (preserving any the user set
|
|
255
|
+
// themselves) and disable the surface in our config.
|
|
442
256
|
try {
|
|
443
257
|
patchConfig({ spinner: { enabled: false } });
|
|
444
258
|
const spinnerMod = await loadSpinnerModule();
|
|
445
259
|
if (spinnerMod) {
|
|
446
|
-
const
|
|
260
|
+
const vres = spinnerMod.clearSpinnerVerbs();
|
|
447
261
|
console.log(
|
|
448
262
|
' Removed spinner job verbs' +
|
|
449
|
-
(
|
|
263
|
+
(vres && vres.keptUserVerbs ? ` (kept ${vres.keptUserVerbs} of your own)` : '') + '.'
|
|
450
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 */ }
|
|
451
272
|
}
|
|
452
273
|
} catch { /* best-effort */ }
|
|
453
274
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminalhire",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Local-first job matching for developers — Claude Code
|
|
3
|
+
"version": "0.2.4",
|
|
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"
|