terminalhire 0.1.1 → 0.2.2

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.
@@ -0,0 +1,674 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // ../../packages/core/src/types.ts
13
+ var init_types = __esm({
14
+ "../../packages/core/src/types.ts"() {
15
+ "use strict";
16
+ }
17
+ });
18
+
19
+ // ../../packages/core/src/vocabulary.ts
20
+ function normalize(tokens) {
21
+ const result = /* @__PURE__ */ new Set();
22
+ for (const raw of tokens) {
23
+ const lower = raw.toLowerCase().trim();
24
+ if (VOCAB_SET.has(lower)) {
25
+ result.add(lower);
26
+ continue;
27
+ }
28
+ const mapped = SYNONYMS[lower];
29
+ if (mapped && VOCAB_SET.has(mapped)) {
30
+ result.add(mapped);
31
+ }
32
+ }
33
+ return Array.from(result);
34
+ }
35
+ var VOCABULARY, SYNONYMS, VOCAB_SET;
36
+ var init_vocabulary = __esm({
37
+ "../../packages/core/src/vocabulary.ts"() {
38
+ "use strict";
39
+ VOCABULARY = [
40
+ // Languages
41
+ "typescript",
42
+ "javascript",
43
+ "python",
44
+ "go",
45
+ "rust",
46
+ "java",
47
+ "ruby",
48
+ "elixir",
49
+ "scala",
50
+ "kotlin",
51
+ "swift",
52
+ "cpp",
53
+ "csharp",
54
+ "php",
55
+ "haskell",
56
+ "clojure",
57
+ "r",
58
+ // Frontend frameworks / libs
59
+ "react",
60
+ "nextjs",
61
+ "vue",
62
+ "nuxt",
63
+ "svelte",
64
+ "angular",
65
+ "solidjs",
66
+ "tailwind",
67
+ "css",
68
+ "html",
69
+ "graphql",
70
+ "trpc",
71
+ // Backend frameworks
72
+ "nodejs",
73
+ "express",
74
+ "fastify",
75
+ "nestjs",
76
+ "django",
77
+ "fastapi",
78
+ "flask",
79
+ "rails",
80
+ "spring",
81
+ "actix",
82
+ "gin",
83
+ "phoenix",
84
+ "laravel",
85
+ "dotnet",
86
+ // Infrastructure & DevOps
87
+ "kubernetes",
88
+ "docker",
89
+ "terraform",
90
+ "aws",
91
+ "gcp",
92
+ "azure",
93
+ "ci-cd",
94
+ "github-actions",
95
+ "linux",
96
+ "nginx",
97
+ "pulumi",
98
+ "ansible",
99
+ "prometheus",
100
+ "grafana",
101
+ "datadog",
102
+ "opentelemetry",
103
+ // Data & ML
104
+ "postgresql",
105
+ "mysql",
106
+ "sqlite",
107
+ "mongodb",
108
+ "redis",
109
+ "elasticsearch",
110
+ "kafka",
111
+ "rabbitmq",
112
+ "data-engineering",
113
+ "spark",
114
+ "airflow",
115
+ "dbt",
116
+ "ml",
117
+ "llm",
118
+ "pytorch",
119
+ "tensorflow",
120
+ "pandas",
121
+ "numpy",
122
+ // Domains / capabilities
123
+ "oauth",
124
+ "authentication",
125
+ "security",
126
+ "payments",
127
+ "billing",
128
+ "frontend",
129
+ "backend",
130
+ "devops",
131
+ "mobile",
132
+ "ios",
133
+ "android",
134
+ "api-design",
135
+ "microservices",
136
+ "websockets",
137
+ "testing",
138
+ "accessibility",
139
+ "seo",
140
+ "performance",
141
+ "observability",
142
+ "search",
143
+ "realtime"
144
+ ];
145
+ SYNONYMS = {
146
+ // Kubernetes aliases
147
+ "k8s": "kubernetes",
148
+ "kube": "kubernetes",
149
+ // Auth / identity
150
+ "passport": "authentication",
151
+ "oauth2": "oauth",
152
+ "oidc": "oauth",
153
+ "jwt": "authentication",
154
+ "saml": "authentication",
155
+ "auth0": "authentication",
156
+ "clerk": "authentication",
157
+ "nextauth": "authentication",
158
+ // Payments
159
+ "@stripe/stripe-js": "payments",
160
+ "stripe": "payments",
161
+ "braintree": "payments",
162
+ "paddle": "payments",
163
+ "lemonsqueezy": "payments",
164
+ "recurly": "billing",
165
+ "chargebee": "billing",
166
+ // Framework / lib aliases
167
+ "next": "nextjs",
168
+ "next.js": "nextjs",
169
+ "nuxt.js": "nuxt",
170
+ "vue.js": "vue",
171
+ "angular.js": "angular",
172
+ "angularjs": "angular",
173
+ "express.js": "express",
174
+ "expressjs": "express",
175
+ "fastapi": "fastapi",
176
+ "nest": "nestjs",
177
+ "nest.js": "nestjs",
178
+ "sveltekit": "svelte",
179
+ // Language aliases
180
+ "ts": "typescript",
181
+ "js": "javascript",
182
+ "py": "python",
183
+ "golang": "go",
184
+ "c++": "cpp",
185
+ "c#": "csharp",
186
+ ".net": "dotnet",
187
+ "asp.net": "dotnet",
188
+ // DB aliases
189
+ "postgres": "postgresql",
190
+ "pg": "postgresql",
191
+ "mongo": "mongodb",
192
+ "elastic": "elasticsearch",
193
+ // Cloud aliases
194
+ "amazon web services": "aws",
195
+ "google cloud": "gcp",
196
+ "google cloud platform": "gcp",
197
+ "microsoft azure": "azure",
198
+ // CI/CD aliases
199
+ "github actions": "github-actions",
200
+ "circle ci": "ci-cd",
201
+ "circleci": "ci-cd",
202
+ "jenkins": "ci-cd",
203
+ "gitlab ci": "ci-cd",
204
+ "travis": "ci-cd",
205
+ // Mobile
206
+ "react native": "mobile",
207
+ "flutter": "mobile",
208
+ "expo": "mobile",
209
+ // AI / ML
210
+ "openai": "llm",
211
+ "anthropic": "llm",
212
+ "langchain": "llm",
213
+ "llamaindex": "llm",
214
+ "hugging face": "ml",
215
+ "huggingface": "ml",
216
+ "scikit-learn": "ml",
217
+ "sklearn": "ml",
218
+ // Data pipeline
219
+ "apache kafka": "kafka",
220
+ "apache spark": "spark",
221
+ "apache airflow": "airflow",
222
+ // Misc
223
+ "tailwindcss": "tailwind",
224
+ "tw": "tailwind",
225
+ "gql": "graphql",
226
+ "ws": "websockets",
227
+ "socket.io": "websockets",
228
+ "jest": "testing",
229
+ "vitest": "testing",
230
+ "playwright": "testing",
231
+ "cypress": "testing"
232
+ };
233
+ VOCAB_SET = new Set(VOCABULARY);
234
+ }
235
+ });
236
+
237
+ // ../../packages/core/src/matcher.ts
238
+ var init_matcher = __esm({
239
+ "../../packages/core/src/matcher.ts"() {
240
+ "use strict";
241
+ }
242
+ });
243
+
244
+ // ../../packages/core/src/feeds/greenhouse.ts
245
+ var init_greenhouse = __esm({
246
+ "../../packages/core/src/feeds/greenhouse.ts"() {
247
+ "use strict";
248
+ init_vocabulary();
249
+ }
250
+ });
251
+
252
+ // ../../packages/core/src/feeds/ashby.ts
253
+ var init_ashby = __esm({
254
+ "../../packages/core/src/feeds/ashby.ts"() {
255
+ "use strict";
256
+ init_vocabulary();
257
+ }
258
+ });
259
+
260
+ // ../../packages/core/src/feeds/himalayas.ts
261
+ var init_himalayas = __esm({
262
+ "../../packages/core/src/feeds/himalayas.ts"() {
263
+ "use strict";
264
+ init_vocabulary();
265
+ }
266
+ });
267
+
268
+ // ../../packages/core/src/feeds/wwr.ts
269
+ var init_wwr = __esm({
270
+ "../../packages/core/src/feeds/wwr.ts"() {
271
+ "use strict";
272
+ init_vocabulary();
273
+ }
274
+ });
275
+
276
+ // ../../packages/core/src/feeds/hn.ts
277
+ var init_hn = __esm({
278
+ "../../packages/core/src/feeds/hn.ts"() {
279
+ "use strict";
280
+ init_vocabulary();
281
+ }
282
+ });
283
+
284
+ // ../../packages/core/src/feeds/index.ts
285
+ var init_feeds = __esm({
286
+ "../../packages/core/src/feeds/index.ts"() {
287
+ "use strict";
288
+ init_greenhouse();
289
+ init_ashby();
290
+ init_himalayas();
291
+ init_wwr();
292
+ init_hn();
293
+ }
294
+ });
295
+
296
+ // ../../packages/core/src/coastal.ts
297
+ import { readFileSync } from "fs";
298
+ import { join } from "path";
299
+ import { fileURLToPath } from "url";
300
+ var init_coastal = __esm({
301
+ "../../packages/core/src/coastal.ts"() {
302
+ "use strict";
303
+ }
304
+ });
305
+
306
+ // ../../packages/core/src/indexer.ts
307
+ var init_indexer = __esm({
308
+ "../../packages/core/src/indexer.ts"() {
309
+ "use strict";
310
+ init_feeds();
311
+ init_coastal();
312
+ }
313
+ });
314
+
315
+ // ../../packages/core/src/github.ts
316
+ var init_github = __esm({
317
+ "../../packages/core/src/github.ts"() {
318
+ "use strict";
319
+ init_vocabulary();
320
+ }
321
+ });
322
+
323
+ // ../../packages/core/src/index.ts
324
+ var init_src = __esm({
325
+ "../../packages/core/src/index.ts"() {
326
+ "use strict";
327
+ init_types();
328
+ init_vocabulary();
329
+ init_matcher();
330
+ init_feeds();
331
+ init_indexer();
332
+ init_coastal();
333
+ init_github();
334
+ }
335
+ });
336
+
337
+ // src/profile.ts
338
+ var profile_exports = {};
339
+ __export(profile_exports, {
340
+ accumulateGitHubTags: () => accumulateGitHubTags,
341
+ accumulateSession: () => accumulateSession,
342
+ accumulateTags: () => accumulateTags,
343
+ addSavedJob: () => addSavedJob,
344
+ deleteProfile: () => deleteProfile,
345
+ listSavedJobs: () => listSavedJobs,
346
+ profileToFingerprint: () => profileToFingerprint,
347
+ readProfile: () => readProfile,
348
+ removeSavedJob: () => removeSavedJob,
349
+ writeProfile: () => writeProfile
350
+ });
351
+ import {
352
+ createCipheriv,
353
+ createDecipheriv,
354
+ randomBytes
355
+ } from "crypto";
356
+ import {
357
+ readFileSync as readFileSync2,
358
+ writeFileSync,
359
+ mkdirSync,
360
+ existsSync
361
+ } from "fs";
362
+ import { join as join2 } from "path";
363
+ import { homedir } from "os";
364
+ async function loadKey() {
365
+ try {
366
+ const kt = await import("keytar");
367
+ const stored = await kt.getPassword("terminalhire", "profile-key");
368
+ if (stored) {
369
+ return Buffer.from(stored, "hex");
370
+ }
371
+ const key2 = randomBytes(KEY_BYTES);
372
+ await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
373
+ return key2;
374
+ } catch {
375
+ }
376
+ mkdirSync(TERMINALHIRE_DIR, { recursive: true });
377
+ if (existsSync(KEY_FILE)) {
378
+ return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
379
+ }
380
+ const key = randomBytes(KEY_BYTES);
381
+ writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
382
+ return key;
383
+ }
384
+ function encrypt(plaintext, key) {
385
+ const iv = randomBytes(IV_BYTES);
386
+ const cipher = createCipheriv(ALGO, key, iv);
387
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
388
+ const tag = cipher.getAuthTag();
389
+ return {
390
+ iv: iv.toString("hex"),
391
+ tag: tag.toString("hex"),
392
+ ciphertext: ct.toString("hex")
393
+ };
394
+ }
395
+ function decrypt(blob, key) {
396
+ const decipher = createDecipheriv(
397
+ ALGO,
398
+ key,
399
+ Buffer.from(blob.iv, "hex")
400
+ );
401
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
402
+ const plain = Buffer.concat([
403
+ decipher.update(Buffer.from(blob.ciphertext, "hex")),
404
+ decipher.final()
405
+ ]);
406
+ return plain.toString("utf8");
407
+ }
408
+ function blankProfile() {
409
+ return {
410
+ version: 3,
411
+ skillTags: [],
412
+ tagWeights: {},
413
+ hasEmployerSessions: false,
414
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
415
+ };
416
+ }
417
+ function recencyDecay(lastSeen) {
418
+ const ageMs = Date.now() - new Date(lastSeen).getTime();
419
+ return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
420
+ }
421
+ function tagScore(w) {
422
+ return w.count * recencyDecay(w.lastSeen);
423
+ }
424
+ function deriveSkillTags(tagWeights) {
425
+ return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
426
+ }
427
+ function migrateTagWeights(profile) {
428
+ if (!profile.tagWeights) {
429
+ profile.tagWeights = {};
430
+ }
431
+ const now = (/* @__PURE__ */ new Date()).toISOString();
432
+ for (const tag of profile.skillTags) {
433
+ if (!profile.tagWeights[tag]) {
434
+ profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
435
+ }
436
+ }
437
+ }
438
+ async function readProfile() {
439
+ if (!existsSync(PROFILE_FILE)) return blankProfile();
440
+ try {
441
+ const key = await loadKey();
442
+ const raw = readFileSync2(PROFILE_FILE, "utf8");
443
+ const blob = JSON.parse(raw);
444
+ const plaintext = decrypt(blob, key);
445
+ const parsed = JSON.parse(plaintext);
446
+ migrateTagWeights(parsed);
447
+ return parsed;
448
+ } catch {
449
+ return blankProfile();
450
+ }
451
+ }
452
+ async function writeProfile(profile) {
453
+ mkdirSync(TERMINALHIRE_DIR, { recursive: true });
454
+ const key = await loadKey();
455
+ profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
456
+ profile.skillTags = deriveSkillTags(profile.tagWeights);
457
+ const blob = encrypt(JSON.stringify(profile), key);
458
+ writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
459
+ }
460
+ function accumulateSession(profile, tags, isEmployerContext, inferredSeniority) {
461
+ const now = (/* @__PURE__ */ new Date()).toISOString();
462
+ let filtered = normalize(tags);
463
+ if (isEmployerContext) {
464
+ filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
465
+ profile.hasEmployerSessions = true;
466
+ }
467
+ for (const tag of filtered) {
468
+ const existing = profile.tagWeights[tag];
469
+ if (existing) {
470
+ existing.count += 1;
471
+ existing.sessions += 1;
472
+ existing.lastSeen = now;
473
+ } else {
474
+ profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
475
+ }
476
+ }
477
+ if (inferredSeniority && !isEmployerContext) {
478
+ profile.seniority = inferredSeniority;
479
+ }
480
+ }
481
+ async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
482
+ const profile = await readProfile();
483
+ accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
484
+ await writeProfile(profile);
485
+ }
486
+ function accumulateGitHubTags(profile, tags) {
487
+ accumulateSession(
488
+ profile,
489
+ tags,
490
+ /* isEmployerContext */
491
+ false
492
+ );
493
+ }
494
+ async function listSavedJobs() {
495
+ const profile = await readProfile();
496
+ return profile.savedJobs ?? [];
497
+ }
498
+ async function addSavedJob(job) {
499
+ const profile = await readProfile();
500
+ const existing = profile.savedJobs ?? [];
501
+ const filtered = existing.filter((j) => j.id !== job.id);
502
+ profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
503
+ await writeProfile(profile);
504
+ }
505
+ async function removeSavedJob(id) {
506
+ const profile = await readProfile();
507
+ const existing = profile.savedJobs ?? [];
508
+ const filtered = existing.filter((j) => j.id !== id);
509
+ if (filtered.length === existing.length) return false;
510
+ profile.savedJobs = filtered;
511
+ await writeProfile(profile);
512
+ return true;
513
+ }
514
+ async function deleteProfile() {
515
+ const { rmSync } = await import("fs");
516
+ try {
517
+ rmSync(PROFILE_FILE);
518
+ } catch {
519
+ }
520
+ try {
521
+ rmSync(KEY_FILE);
522
+ } catch {
523
+ }
524
+ }
525
+ function profileToFingerprint(profile) {
526
+ const rankedTags = Object.entries(profile.tagWeights).map(([tag, w]) => ({ tag, score: tagScore(w) })).filter(({ score }) => score >= MIN_FINGERPRINT_SCORE).sort((a, b) => b.score - a.score).map(({ tag }) => tag);
527
+ const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
528
+ return {
529
+ skillTags,
530
+ seniorityBand: profile.seniority,
531
+ prefs: {
532
+ roleTypes: profile.roleTypes,
533
+ remoteOnly: profile.remoteOnly,
534
+ compFloorUsd: profile.compFloorUsd
535
+ }
536
+ };
537
+ }
538
+ var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
539
+ var init_profile = __esm({
540
+ "src/profile.ts"() {
541
+ "use strict";
542
+ init_src();
543
+ TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
544
+ PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
545
+ KEY_FILE = join2(TERMINALHIRE_DIR, "key");
546
+ ALGO = "aes-256-gcm";
547
+ KEY_BYTES = 32;
548
+ IV_BYTES = 12;
549
+ DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
550
+ LANGUAGE_TAGS = /* @__PURE__ */ new Set([
551
+ "typescript",
552
+ "javascript",
553
+ "python",
554
+ "go",
555
+ "rust",
556
+ "java",
557
+ "ruby",
558
+ "elixir",
559
+ "scala",
560
+ "kotlin",
561
+ "swift",
562
+ "cpp",
563
+ "csharp",
564
+ "php",
565
+ "haskell",
566
+ "clojure",
567
+ "r"
568
+ ]);
569
+ MIN_FINGERPRINT_SCORE = 0.05;
570
+ }
571
+ });
572
+
573
+ // bin/jpi-save.js
574
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
575
+ import { join as join3 } from "path";
576
+ import { homedir as homedir2 } from "os";
577
+ import { fileURLToPath as fileURLToPath2 } from "url";
578
+ var __dirname = fileURLToPath2(new URL(".", import.meta.url));
579
+ var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
580
+ var INDEX_CACHE_FILE = join3(TERMINALHIRE_DIR2, "index-cache.json");
581
+ function findJobInCache(jobId) {
582
+ try {
583
+ if (!existsSync2(INDEX_CACHE_FILE)) return null;
584
+ const raw = readFileSync3(INDEX_CACHE_FILE, "utf8");
585
+ const entry = JSON.parse(raw);
586
+ const jobs = entry?.index?.jobs ?? [];
587
+ return jobs.find((j) => j.id === jobId) ?? null;
588
+ } catch {
589
+ return null;
590
+ }
591
+ }
592
+ async function cmdSave(jobId) {
593
+ if (!jobId) {
594
+ console.error("Usage: terminalhire save <jobId>");
595
+ console.error(" jobId is shown in `terminalhire jobs` output (e.g. greenhouse:abc123)");
596
+ process.exit(1);
597
+ }
598
+ const { addSavedJob: addSavedJob2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
599
+ const job = findJobInCache(jobId);
600
+ if (!job) {
601
+ console.error(`terminalhire save: job '${jobId}' not found in local index cache.`);
602
+ console.error(" Run `terminalhire jobs` first to populate the cache.");
603
+ process.exit(1);
604
+ }
605
+ await addSavedJob2({
606
+ id: job.id,
607
+ title: job.title,
608
+ company: job.company,
609
+ url: job.url,
610
+ source: job.source,
611
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
612
+ // overwritten by addSavedJob, but required by type
613
+ });
614
+ console.log(`Saved: ${job.title} \u2014 ${job.company}`);
615
+ console.log(` id: ${job.id}`);
616
+ console.log(` url: ${job.url}`);
617
+ console.log(" Stored locally in encrypted profile. Run `terminalhire saved` to list.");
618
+ }
619
+ async function cmdSaved() {
620
+ const { listSavedJobs: listSavedJobs2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
621
+ const jobs = await listSavedJobs2();
622
+ if (jobs.length === 0) {
623
+ console.log("No saved jobs. Use `terminalhire save <jobId>` to save a role.");
624
+ return;
625
+ }
626
+ console.log(`
627
+ ${jobs.length} saved job${jobs.length === 1 ? "" : "s"}:
628
+ `);
629
+ for (const j of jobs) {
630
+ const date = new Date(j.savedAt).toLocaleDateString();
631
+ console.log(` ${j.id}`);
632
+ console.log(` ${j.title} \u2014 ${j.company}`);
633
+ console.log(` ${j.url}`);
634
+ console.log(` Saved: ${date}`);
635
+ console.log("");
636
+ }
637
+ console.log("To remove: terminalhire unsave <jobId>");
638
+ }
639
+ async function cmdUnsave(jobId) {
640
+ if (!jobId) {
641
+ console.error("Usage: terminalhire unsave <jobId>");
642
+ process.exit(1);
643
+ }
644
+ const { removeSavedJob: removeSavedJob2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
645
+ const removed = await removeSavedJob2(jobId);
646
+ if (removed) {
647
+ console.log(`Removed saved job: ${jobId}`);
648
+ } else {
649
+ console.error(`terminalhire unsave: job '${jobId}' was not in your saved list.`);
650
+ process.exit(1);
651
+ }
652
+ }
653
+ async function run() {
654
+ const verb = process.argv[2];
655
+ const jobId = process.argv[3];
656
+ try {
657
+ if (verb === "save") {
658
+ await cmdSave(jobId);
659
+ } else if (verb === "saved") {
660
+ await cmdSaved();
661
+ } else if (verb === "unsave") {
662
+ await cmdUnsave(jobId);
663
+ } else {
664
+ console.error(`terminalhire: unknown save verb '${verb}'. Expected: save | saved | unsave`);
665
+ process.exit(1);
666
+ }
667
+ } catch (err) {
668
+ console.error("terminalhire save error:", err.message ?? err);
669
+ process.exit(1);
670
+ }
671
+ }
672
+ export {
673
+ run
674
+ };