terminalhire 0.42.6 → 0.42.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # terminalhire — your code is your résumé
2
2
 
3
- Local-first job + bounty matching, and a verifiable **Proof of Work** credential earned from PRs that maintainers who aren't you merged into established repos — checkable on GitHub, impossible to self-report. Your profile never leaves your machine.
3
+ Job + bounty matching in the terminal, and a **Proof of Work** credential earned from PRs that maintainers who aren't you merged into established repos — checkable on GitHub, impossible to self-report. Claim a scoped bounty and get paid when the poster accepts it.
4
4
 
5
5
  **Domain:** [terminalhire.com](https://terminalhire.com)
6
6
 
@@ -57,10 +57,10 @@ terminalhire trajectory # trajectory from your local Claude Code corpus
57
57
  terminalhire trajectory --export # write a derived score + Markdown to ~/.terminalhire/
58
58
  terminalhire trajectory --inward # also show private rework/recovery (never exported)
59
59
 
60
- terminalhire devs # opted-in peers & founders — profile never leaves your machine
60
+ terminalhire devs # opted-in peers & founders — matched on your machine
61
61
  terminalhire intro <login> # request a double-opt-in intro (typed-yes consent)
62
62
  terminalhire inbox # unread chats, connections & pending intros in one place
63
- terminalhire mcp --print-config # local-first match + claim-record MCP config for AI hosts
63
+ terminalhire mcp --print-config # match + claim-record MCP config for AI hosts
64
64
 
65
65
  terminalhire profile --show # inspect your encrypted local profile (incl. GitHub fields)
66
66
  terminalhire profile --edit # set displayName, contactEmail, prefs
@@ -84,18 +84,19 @@ This runs the **GitHub OAuth Device Flow**:
84
84
 
85
85
  ### What GitHub enriches
86
86
 
87
- | Signal | Source |
88
- |---|---|
89
- | Skill tags | Repo languages + repo topics, filtered through `vocabulary.normalize()` |
87
+ | Signal | Source |
88
+ | ------------------ | ------------------------------------------------------------------------------------ |
89
+ | Skill tags | Repo languages + repo topics, filtered through `vocabulary.normalize()` |
90
90
  | Seniority estimate | Account age × (repos + followers) per [documented thresholds](#seniority-thresholds) |
91
- | Display name | `user.name` if not already set in profile |
92
- | Contact email | `user.email` if public and not already set in profile |
91
+ | Display name | `user.name` if not already set in profile |
92
+ | Contact email | `user.email` if public and not already set in profile |
93
93
 
94
94
  ### Public scope guarantee
95
95
 
96
96
  Scope requested: **`read:user`** — public profile + public repos only.
97
97
 
98
98
  **Private-repo scopes are NEVER requested.** This means:
99
+
99
100
  - No employer-IP risk from private repos.
100
101
  - No code, secrets, or private file access.
101
102
  - Only what is already publicly visible on your GitHub profile.
@@ -199,13 +200,13 @@ GitHub (optional enrichment):
199
200
 
200
201
  ## Privacy
201
202
 
202
- | What happens locally | What crosses the wire |
203
- |---|---|
204
- | Profile accumulated from personal project sessions | GET /api/index — anonymous, no dev data |
205
- | Closed-vocab skill tags + seniority (encrypted at rest) | POST /api/lead — ONLY after explicit per-role "yes" consent |
206
- | GitHub token + public profile data (encrypted at rest) | GitHub fields in lead — ONLY after consent AND GitHub is connected |
207
- | Employer-repo sessions: language tags only | Nothing else |
208
- | git email domain + remote host (employer detection only) | |
203
+ | What happens locally | What crosses the wire |
204
+ | -------------------------------------------------------- | ------------------------------------------------------------------ |
205
+ | Profile accumulated from personal project sessions | GET /api/index — anonymous, no dev data |
206
+ | Closed-vocab skill tags + seniority (encrypted at rest) | POST /api/lead — ONLY after explicit per-role "yes" consent |
207
+ | GitHub token + public profile data (encrypted at rest) | GitHub fields in lead — ONLY after consent AND GitHub is connected |
208
+ | Employer-repo sessions: language tags only | Nothing else |
209
+ | git email domain + remote host (employer detection only) | |
209
210
 
210
211
  **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.
211
212
 
@@ -280,9 +281,9 @@ Typing anything other than `y` or `yes` aborts with no network call.
280
281
 
281
282
  ## Apply modes
282
283
 
283
- | `applyMode` | What happens when you select a role |
284
- |---|---|
285
- | `direct` | Opens the employer's public URL in your terminal. No data sent. |
284
+ | `applyMode` | What happens when you select a role |
285
+ | ------------ | ------------------------------------------------------------------------- |
286
+ | `direct` | Opens the employer's public URL in your terminal. No data sent. |
286
287
  | `buyer-lead` | Triggers the named-entity consent flow above. Nothing sent without "yes". |
287
288
 
288
289
  The two modes are never silently conflated.
@@ -295,13 +296,13 @@ node test/zero-egress.test.js
295
296
 
296
297
  ## Environment variables
297
298
 
298
- | Variable | Default | Description |
299
- |---|---|---|
300
- | `TERMINALHIRE_API_URL` | `https://terminalhire.com` | Base URL for `/api/index` and `/api/lead` (also accepts legacy `JPI_API_URL`) |
301
- | `GITHUB_CLIENT_ID` | _(required for real OAuth)_ | GitHub OAuth App client ID (device flow) |
302
- | `GITHUB_CLIENT_SECRET` | _(optional)_ | GitHub OAuth App client secret (public apps omit this) |
303
- | `GITHUB_DEVICE_CLIENT_ID` | _(falls back to `GITHUB_CLIENT_ID`)_ | Device-flow specific client ID if different |
304
- | `TERMINALHIRE_GITHUB_MOCK` | `0` | Set to `1` to skip real OAuth and use the fixture (dev/CI); also accepts legacy `JPI_GITHUB_MOCK` |
299
+ | Variable | Default | Description |
300
+ | -------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------- |
301
+ | `TERMINALHIRE_API_URL` | `https://terminalhire.com` | Base URL for `/api/index` and `/api/lead` (also accepts legacy `JPI_API_URL`) |
302
+ | `GITHUB_CLIENT_ID` | _(required for real OAuth)_ | GitHub OAuth App client ID (device flow) |
303
+ | `GITHUB_CLIENT_SECRET` | _(optional)_ | GitHub OAuth App client secret (public apps omit this) |
304
+ | `GITHUB_DEVICE_CLIENT_ID` | _(falls back to `GITHUB_CLIENT_ID`)_ | Device-flow specific client ID if different |
305
+ | `TERMINALHIRE_GITHUB_MOCK` | `0` | Set to `1` to skip real OAuth and use the fixture (dev/CI); also accepts legacy `JPI_GITHUB_MOCK` |
305
306
 
306
307
  ## File layout
307
308
 
@@ -326,13 +327,13 @@ apps/cli/
326
327
 
327
328
  ## How data is used: local default vs consented lead
328
329
 
329
- | Data | Default (local-enrichment) | Opt-in (consented lead) |
330
- |---|---|---|
331
- | Skill tags | Stored encrypted locally, used for local matching only | Included in `approvedFields.skillTags` on "yes" |
332
- | Seniority | Stored encrypted locally | Included in `approvedFields.seniorityBand` on "yes" |
333
- | Display name | Stored encrypted locally (pre-filled from GitHub if public) | Included if set and "yes" |
334
- | Contact email | Stored encrypted locally (pre-filled from GitHub public email) | Included if set and "yes" |
335
- | GitHub login / profileUrl | Stored in `profile.github` on `terminalhire login` | Included in `approvedFields.github` ONLY on "yes" |
336
- | GitHub topLanguages | Used locally for tag inference (closed vocab) | Included in `approvedFields.github` ONLY on "yes" |
337
- | GitHub token | Encrypted at `~/.terminalhire/github-token.enc` | Never sent |
338
- | Private repos | Never accessed (scope: read:user) | N/A |
330
+ | Data | Default (local-enrichment) | Opt-in (consented lead) |
331
+ | ------------------------- | -------------------------------------------------------------- | --------------------------------------------------- |
332
+ | Skill tags | Stored encrypted locally, used for local matching only | Included in `approvedFields.skillTags` on "yes" |
333
+ | Seniority | Stored encrypted locally | Included in `approvedFields.seniorityBand` on "yes" |
334
+ | Display name | Stored encrypted locally (pre-filled from GitHub if public) | Included if set and "yes" |
335
+ | Contact email | Stored encrypted locally (pre-filled from GitHub public email) | Included if set and "yes" |
336
+ | GitHub login / profileUrl | Stored in `profile.github` on `terminalhire login` | Included in `approvedFields.github` ONLY on "yes" |
337
+ | GitHub topLanguages | Used locally for tag inference (closed vocab) | Included in `approvedFields.github` ONLY on "yes" |
338
+ | GitHub token | Encrypted at `~/.terminalhire/github-token.enc` | Never sent |
339
+ | Private repos | Never accessed (scope: read:user) | N/A |
@@ -77,7 +77,11 @@ function updateIndexCache(patch) {
77
77
  renameSync(tmp, INDEX_CACHE_FILE);
78
78
  return entry;
79
79
  }
80
+ function writeIndexCache(index) {
81
+ return updateIndexCache({ index, indexETag: "" });
82
+ }
80
83
  export {
81
84
  readCacheEntry,
82
- updateIndexCache
85
+ updateIndexCache,
86
+ writeIndexCache
83
87
  };
@@ -10540,7 +10540,8 @@ var init_state_dir = __esm({
10540
10540
  var cache_store_exports = {};
10541
10541
  __export(cache_store_exports, {
10542
10542
  readCacheEntry: () => readCacheEntry,
10543
- updateIndexCache: () => updateIndexCache
10543
+ updateIndexCache: () => updateIndexCache,
10544
+ writeIndexCache: () => writeIndexCache
10544
10545
  });
10545
10546
  import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
10546
10547
  import { join as join2 } from "path";
@@ -10566,6 +10567,9 @@ function updateIndexCache(patch) {
10566
10567
  renameSync(tmp, INDEX_CACHE_FILE);
10567
10568
  return entry;
10568
10569
  }
10570
+ function writeIndexCache(index) {
10571
+ return updateIndexCache({ index, indexETag: "" });
10572
+ }
10569
10573
  var TERMINALHIRE_DIR, INDEX_CACHE_FILE, SCHEMA_VERSION2, tmpCounter;
10570
10574
  var init_cache_store = __esm({
10571
10575
  "bin/cache-store.js"() {
@@ -10861,6 +10865,7 @@ __export(profile_exports, {
10861
10865
  accumulateTags: () => accumulateTags,
10862
10866
  addSavedJob: () => addSavedJob,
10863
10867
  deleteProfile: () => deleteProfile,
10868
+ isBlankProfile: () => isBlankProfile,
10864
10869
  listSavedJobs: () => listSavedJobs,
10865
10870
  profileToFingerprint: () => profileToFingerprint,
10866
10871
  readProfile: () => readProfile,
@@ -10901,6 +10906,17 @@ function migrateTagWeights(profile) {
10901
10906
  }
10902
10907
  }
10903
10908
  }
10909
+ function isBlankProfile(profile) {
10910
+ if (profile.skillTags.length > 0) return false;
10911
+ if (Object.keys(profile.tagWeights).length > 0) return false;
10912
+ for (const [key, value] of Object.entries(profile)) {
10913
+ if (DERIVED_KEYS.has(key)) continue;
10914
+ if (value === void 0 || value === null) continue;
10915
+ if (Array.isArray(value) && value.length === 0) continue;
10916
+ return false;
10917
+ }
10918
+ return true;
10919
+ }
10904
10920
  async function readProfile() {
10905
10921
  const parsed = await profileStore.read();
10906
10922
  migrateTagWeights(parsed);
@@ -10985,7 +11001,7 @@ function profileToFingerprint(profile) {
10985
11001
  }
10986
11002
  };
10987
11003
  }
10988
- var TERMINALHIRE_DIR3, PROFILE_FILE, profileStore, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
11004
+ var TERMINALHIRE_DIR3, PROFILE_FILE, profileStore, DERIVED_KEYS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
10989
11005
  var init_profile = __esm({
10990
11006
  "src/profile.ts"() {
10991
11007
  "use strict";
@@ -10997,6 +11013,13 @@ var init_profile = __esm({
10997
11013
  blank: blankProfile,
10998
11014
  keyPolicy: "keytar-first-file-fallback"
10999
11015
  });
11016
+ DERIVED_KEYS = /* @__PURE__ */ new Set([
11017
+ "version",
11018
+ "updatedAt",
11019
+ "skillTags",
11020
+ "tagWeights",
11021
+ "hasEmployerSessions"
11022
+ ]);
11000
11023
  LANGUAGE_TAGS = /* @__PURE__ */ new Set([
11001
11024
  "typescript",
11002
11025
  "javascript",
@@ -11836,9 +11859,6 @@ function readIndexCache() {
11836
11859
  return null;
11837
11860
  }
11838
11861
  }
11839
- function writeIndexCache(index) {
11840
- updateIndexCache({ index, indexETag: "" });
11841
- }
11842
11862
  async function fetchIndex() {
11843
11863
  const cached = readIndexCache();
11844
11864
  if (cached) return cached;
@@ -5591,6 +5591,7 @@ __export(profile_exports, {
5591
5591
  accumulateTags: () => accumulateTags,
5592
5592
  addSavedJob: () => addSavedJob,
5593
5593
  deleteProfile: () => deleteProfile,
5594
+ isBlankProfile: () => isBlankProfile,
5594
5595
  listSavedJobs: () => listSavedJobs,
5595
5596
  profileToFingerprint: () => profileToFingerprint,
5596
5597
  readProfile: () => readProfile,
@@ -5631,6 +5632,17 @@ function migrateTagWeights(profile) {
5631
5632
  }
5632
5633
  }
5633
5634
  }
5635
+ function isBlankProfile(profile) {
5636
+ if (profile.skillTags.length > 0) return false;
5637
+ if (Object.keys(profile.tagWeights).length > 0) return false;
5638
+ for (const [key, value] of Object.entries(profile)) {
5639
+ if (DERIVED_KEYS.has(key)) continue;
5640
+ if (value === void 0 || value === null) continue;
5641
+ if (Array.isArray(value) && value.length === 0) continue;
5642
+ return false;
5643
+ }
5644
+ return true;
5645
+ }
5634
5646
  async function readProfile() {
5635
5647
  const parsed = await profileStore.read();
5636
5648
  migrateTagWeights(parsed);
@@ -5715,7 +5727,7 @@ function profileToFingerprint(profile) {
5715
5727
  }
5716
5728
  };
5717
5729
  }
5718
- var TERMINALHIRE_DIR7, PROFILE_FILE, profileStore, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
5730
+ var TERMINALHIRE_DIR7, PROFILE_FILE, profileStore, DERIVED_KEYS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
5719
5731
  var init_profile = __esm({
5720
5732
  "src/profile.ts"() {
5721
5733
  "use strict";
@@ -5727,6 +5739,13 @@ var init_profile = __esm({
5727
5739
  blank: blankProfile,
5728
5740
  keyPolicy: "keytar-first-file-fallback"
5729
5741
  });
5742
+ DERIVED_KEYS = /* @__PURE__ */ new Set([
5743
+ "version",
5744
+ "updatedAt",
5745
+ "skillTags",
5746
+ "tagWeights",
5747
+ "hasEmployerSessions"
5748
+ ]);
5730
5749
  LANGUAGE_TAGS = /* @__PURE__ */ new Set([
5731
5750
  "typescript",
5732
5751
  "javascript",
@@ -5821,6 +5840,7 @@ function defaultIntroDeps() {
5821
5840
  });
5822
5841
  });
5823
5842
  },
5843
+ isTTY: () => Boolean(process.stdin.isTTY),
5824
5844
  fetchImpl: (...args) => globalThis.fetch(...args),
5825
5845
  openBrowser: (url) => {
5826
5846
  void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
@@ -5859,7 +5879,9 @@ async function runIntroRequest(args, overrides) {
5859
5879
  const deps = { ...defaultIntroDeps(), ...overrides };
5860
5880
  const targetLogin = args.targetLogin?.trim().replace(/^@/, "");
5861
5881
  if (!targetLogin) {
5862
- deps.errorLog('\n Usage: terminalhire intro <github-login> [--note "..."] [--contact <email>] [--name "..."]\n');
5882
+ deps.errorLog(
5883
+ '\n Usage: terminalhire intro <github-login> [--note "..."] [--contact <email>] [--name "..."]\n'
5884
+ );
5863
5885
  deps.exit(1);
5864
5886
  return;
5865
5887
  }
@@ -5886,7 +5908,16 @@ async function runIntroRequest(args, overrides) {
5886
5908
  note: args.note
5887
5909
  });
5888
5910
  renderConsentCard(payload, deps);
5889
- const answer = await deps.prompt(' Type "yes" to send this intro request (anything else cancels): ');
5911
+ if (!deps.isTTY()) {
5912
+ deps.errorLog(
5913
+ "\n stdin is not interactive \u2014 re-run `terminalhire intro` in a real terminal to confirm.\n"
5914
+ );
5915
+ deps.exit(1);
5916
+ return;
5917
+ }
5918
+ const answer = await deps.prompt(
5919
+ ' Type "yes" to send this intro request (anything else cancels): '
5920
+ );
5890
5921
  if (answer !== "yes") {
5891
5922
  deps.log("\n Cancelled \u2014 nothing was sent.\n");
5892
5923
  deps.exit(0);
@@ -5954,9 +5985,13 @@ async function runIntroRequest(args, overrides) {
5954
5985
  } catch {
5955
5986
  }
5956
5987
  if (deduped) {
5957
- deps.log(`
5958
- You already have a pending intro request to @${targetLogin} \u2014 nothing new was sent.`);
5959
- deps.log(" They can accept it any time from `terminalhire intro --list` or their dashboard.\n");
5988
+ deps.log(
5989
+ `
5990
+ You already have a pending intro request to @${targetLogin} \u2014 nothing new was sent.`
5991
+ );
5992
+ deps.log(
5993
+ " They can accept it any time from `terminalhire intro --list` or their dashboard.\n"
5994
+ );
5960
5995
  } else if (notified) {
5961
5996
  deps.log(`
5962
5997
  Intro request sent to @${targetLogin}. They will see only your public login`);
@@ -6001,9 +6036,11 @@ async function runIntroDecision(args, overrides) {
6001
6036
  try {
6002
6037
  intros = await fetchIntros(deps, cookie);
6003
6038
  } catch (err) {
6004
- deps.errorLog(`
6039
+ deps.errorLog(
6040
+ `
6005
6041
  Could not look up the request: ${err instanceof Error ? err.message : String(err)}
6006
- `);
6042
+ `
6043
+ );
6007
6044
  deps.exit(1);
6008
6045
  return;
6009
6046
  }
@@ -6018,8 +6055,10 @@ async function runIntroDecision(args, overrides) {
6018
6055
  return;
6019
6056
  }
6020
6057
  if (matches.length > 1) {
6021
- deps.errorLog(`
6022
- ${matches.length} pending requests from @${handle} \u2014 ${args.action === "accept" ? "accepting" : "declining"} one (the rest are redundant duplicates).`);
6058
+ deps.errorLog(
6059
+ `
6060
+ ${matches.length} pending requests from @${handle} \u2014 ${args.action === "accept" ? "accepting" : "declining"} one (the rest are redundant duplicates).`
6061
+ );
6023
6062
  }
6024
6063
  id = matches[0].id;
6025
6064
  }
@@ -6041,7 +6080,16 @@ async function runIntroDecision(args, overrides) {
6041
6080
  }
6042
6081
  deps.log(" Nothing else is shared. Declining shares nothing.");
6043
6082
  deps.log("");
6044
- const answer = await deps.prompt(' Type "yes" to accept and share your contact (anything else cancels): ');
6083
+ if (!deps.isTTY()) {
6084
+ deps.errorLog(
6085
+ "\n stdin is not interactive \u2014 re-run `terminalhire intro --accept` in a real terminal to confirm.\n"
6086
+ );
6087
+ deps.exit(1);
6088
+ return;
6089
+ }
6090
+ const answer = await deps.prompt(
6091
+ ' Type "yes" to accept and share your contact (anything else cancels): '
6092
+ );
6045
6093
  if (answer !== "yes") {
6046
6094
  deps.log("\n Cancelled \u2014 nothing was sent.\n");
6047
6095
  deps.exit(0);
@@ -6138,7 +6186,9 @@ async function runIntroList(overrides) {
6138
6186
  switch (result.status) {
6139
6187
  case "no-session":
6140
6188
  deps.log("\n No linked web session found on this machine.");
6141
- deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
6189
+ deps.log(
6190
+ " Run `terminalhire link` to connect this terminal to your account, then re-run.\n"
6191
+ );
6142
6192
  deps.exit(0);
6143
6193
  return;
6144
6194
  case "expired":
@@ -25186,6 +25186,7 @@ __export(profile_exports, {
25186
25186
  accumulateTags: () => accumulateTags,
25187
25187
  addSavedJob: () => addSavedJob,
25188
25188
  deleteProfile: () => deleteProfile,
25189
+ isBlankProfile: () => isBlankProfile,
25189
25190
  listSavedJobs: () => listSavedJobs,
25190
25191
  profileToFingerprint: () => profileToFingerprint,
25191
25192
  readProfile: () => readProfile,
@@ -25226,6 +25227,17 @@ function migrateTagWeights(profile) {
25226
25227
  }
25227
25228
  }
25228
25229
  }
25230
+ function isBlankProfile(profile) {
25231
+ if (profile.skillTags.length > 0) return false;
25232
+ if (Object.keys(profile.tagWeights).length > 0) return false;
25233
+ for (const [key, value] of Object.entries(profile)) {
25234
+ if (DERIVED_KEYS.has(key)) continue;
25235
+ if (value === void 0 || value === null) continue;
25236
+ if (Array.isArray(value) && value.length === 0) continue;
25237
+ return false;
25238
+ }
25239
+ return true;
25240
+ }
25229
25241
  async function readProfile() {
25230
25242
  const parsed = await profileStore.read();
25231
25243
  migrateTagWeights(parsed);
@@ -25310,7 +25322,7 @@ function profileToFingerprint(profile) {
25310
25322
  }
25311
25323
  };
25312
25324
  }
25313
- var TERMINALHIRE_DIR7, PROFILE_FILE, profileStore, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
25325
+ var TERMINALHIRE_DIR7, PROFILE_FILE, profileStore, DERIVED_KEYS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
25314
25326
  var init_profile = __esm({
25315
25327
  "src/profile.ts"() {
25316
25328
  "use strict";
@@ -25322,6 +25334,13 @@ var init_profile = __esm({
25322
25334
  blank: blankProfile,
25323
25335
  keyPolicy: "keytar-first-file-fallback"
25324
25336
  });
25337
+ DERIVED_KEYS = /* @__PURE__ */ new Set([
25338
+ "version",
25339
+ "updatedAt",
25340
+ "skillTags",
25341
+ "tagWeights",
25342
+ "hasEmployerSessions"
25343
+ ]);
25325
25344
  LANGUAGE_TAGS = /* @__PURE__ */ new Set([
25326
25345
  "typescript",
25327
25346
  "javascript",
@@ -25890,7 +25909,8 @@ var init_approved_claims_badge = __esm({
25890
25909
  var cache_store_exports = {};
25891
25910
  __export(cache_store_exports, {
25892
25911
  readCacheEntry: () => readCacheEntry,
25893
- updateIndexCache: () => updateIndexCache
25912
+ updateIndexCache: () => updateIndexCache,
25913
+ writeIndexCache: () => writeIndexCache
25894
25914
  });
25895
25915
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, renameSync as renameSync5 } from "fs";
25896
25916
  import { join as join16 } from "path";
@@ -25916,6 +25936,9 @@ function updateIndexCache(patch) {
25916
25936
  renameSync5(tmp, INDEX_CACHE_FILE);
25917
25937
  return entry;
25918
25938
  }
25939
+ function writeIndexCache(index) {
25940
+ return updateIndexCache({ index, indexETag: "" });
25941
+ }
25919
25942
  var TERMINALHIRE_DIR10, INDEX_CACHE_FILE, SCHEMA_VERSION2, tmpCounter;
25920
25943
  var init_cache_store = __esm({
25921
25944
  "bin/cache-store.js"() {
@@ -28849,6 +28872,7 @@ function renderRunView(run2, message) {
28849
28872
  }
28850
28873
  if (run2.previewUrl) lines.push(` preview: ${terminalSafeInline(run2.previewUrl)}`);
28851
28874
  if (run2.roundTrips != null) lines.push(` round trips: ${run2.roundTrips}`);
28875
+ if (run2.passMeaning) lines.push(` ${terminalSafeInline(run2.passMeaning)}`);
28852
28876
  if (run2.logTail) {
28853
28877
  lines.push(" \u2500\u2500 log tail \u2500\u2500");
28854
28878
  for (const l of terminalSafeLines(run2.logTail)) lines.push(` \u2502 ${l}`);
@@ -28914,8 +28938,7 @@ async function watchRunsLoop({
28914
28938
  for (let i = 1; i <= attempts; i++) {
28915
28939
  const r = await fetchOnce();
28916
28940
  if (r.kind === "refusal") {
28917
- log(`terminalhire claim: ${renderServerRefusal(r.status, r.body)}`);
28918
- return { outcome: "refused" };
28941
+ return { outcome: "refused", status: r.status, body: r.body };
28919
28942
  }
28920
28943
  if (r.kind === "network") {
28921
28944
  log(` (attempt ${i}/${attempts}: terminalhire unreachable \u2014 ${r.error}; retrying)`);
@@ -29139,7 +29162,12 @@ async function cmdRuns(id, flags = {}) {
29139
29162
  ` Watching CI for ${id} \u2014 up to ${RUNS_POLL_ATTEMPTS} checks, one every ${Math.round(RUNS_POLL_INTERVAL_MS / 1e3)}s.`
29140
29163
  );
29141
29164
  const out = await watchRunsLoop({ id, fetchOnce });
29142
- if (out.outcome === "refused") process.exit(1);
29165
+ if (out.outcome === "refused") {
29166
+ if (!retireRevokedReadCredential(out.status, out.body, "reading CI results")) {
29167
+ console.error(`terminalhire claim: ${renderServerRefusal(out.status, out.body)}`);
29168
+ }
29169
+ process.exit(1);
29170
+ }
29143
29171
  return;
29144
29172
  }
29145
29173
  const r = await fetchOnce();
@@ -114,7 +114,8 @@ var init_founder_paid_badge = __esm({
114
114
  var cache_store_exports = {};
115
115
  __export(cache_store_exports, {
116
116
  readCacheEntry: () => readCacheEntry,
117
- updateIndexCache: () => updateIndexCache
117
+ updateIndexCache: () => updateIndexCache,
118
+ writeIndexCache: () => writeIndexCache
118
119
  });
119
120
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync } from "fs";
120
121
  import { join as join2 } from "path";
@@ -140,6 +141,9 @@ function updateIndexCache(patch) {
140
141
  renameSync(tmp, INDEX_CACHE_FILE);
141
142
  return entry;
142
143
  }
144
+ function writeIndexCache(index) {
145
+ return updateIndexCache({ index, indexETag: "" });
146
+ }
143
147
  var TERMINALHIRE_DIR2, INDEX_CACHE_FILE, SCHEMA_VERSION, tmpCounter;
144
148
  var init_cache_store = __esm({
145
149
  "bin/cache-store.js"() {
@@ -2854,6 +2854,7 @@ __export(profile_exports, {
2854
2854
  accumulateTags: () => accumulateTags,
2855
2855
  addSavedJob: () => addSavedJob,
2856
2856
  deleteProfile: () => deleteProfile,
2857
+ isBlankProfile: () => isBlankProfile,
2857
2858
  listSavedJobs: () => listSavedJobs,
2858
2859
  profileToFingerprint: () => profileToFingerprint,
2859
2860
  readProfile: () => readProfile,
@@ -2894,6 +2895,17 @@ function migrateTagWeights(profile) {
2894
2895
  }
2895
2896
  }
2896
2897
  }
2898
+ function isBlankProfile(profile) {
2899
+ if (profile.skillTags.length > 0) return false;
2900
+ if (Object.keys(profile.tagWeights).length > 0) return false;
2901
+ for (const [key, value] of Object.entries(profile)) {
2902
+ if (DERIVED_KEYS.has(key)) continue;
2903
+ if (value === void 0 || value === null) continue;
2904
+ if (Array.isArray(value) && value.length === 0) continue;
2905
+ return false;
2906
+ }
2907
+ return true;
2908
+ }
2897
2909
  async function readProfile() {
2898
2910
  const parsed = await profileStore.read();
2899
2911
  migrateTagWeights(parsed);
@@ -2978,7 +2990,7 @@ function profileToFingerprint(profile) {
2978
2990
  }
2979
2991
  };
2980
2992
  }
2981
- var TERMINALHIRE_DIR4, PROFILE_FILE, profileStore, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
2993
+ var TERMINALHIRE_DIR4, PROFILE_FILE, profileStore, DERIVED_KEYS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
2982
2994
  var init_profile = __esm({
2983
2995
  "src/profile.ts"() {
2984
2996
  "use strict";
@@ -2990,6 +3002,13 @@ var init_profile = __esm({
2990
3002
  blank: blankProfile,
2991
3003
  keyPolicy: "keytar-first-file-fallback"
2992
3004
  });
3005
+ DERIVED_KEYS = /* @__PURE__ */ new Set([
3006
+ "version",
3007
+ "updatedAt",
3008
+ "skillTags",
3009
+ "tagWeights",
3010
+ "hasEmployerSessions"
3011
+ ]);
2993
3012
  LANGUAGE_TAGS = /* @__PURE__ */ new Set([
2994
3013
  "typescript",
2995
3014
  "javascript",
@@ -3854,6 +3873,9 @@ function updateIndexCache(patch) {
3854
3873
  renameSync(tmp, INDEX_CACHE_FILE);
3855
3874
  return entry;
3856
3875
  }
3876
+ function writeIndexCache(index) {
3877
+ return updateIndexCache({ index, indexETag: "" });
3878
+ }
3857
3879
 
3858
3880
  // src/config.ts
3859
3881
  init_state_dir();
@@ -4157,9 +4179,6 @@ function readIndexCache() {
4157
4179
  return null;
4158
4180
  }
4159
4181
  }
4160
- function writeIndexCache(index) {
4161
- updateIndexCache({ index, indexETag: "" });
4162
- }
4163
4182
  async function fetchIndex(fetchImpl, useCache = true) {
4164
4183
  if (useCache) {
4165
4184
  const cached = readIndexCache();
@@ -10809,6 +10809,7 @@ __export(profile_exports, {
10809
10809
  accumulateTags: () => accumulateTags,
10810
10810
  addSavedJob: () => addSavedJob,
10811
10811
  deleteProfile: () => deleteProfile,
10812
+ isBlankProfile: () => isBlankProfile,
10812
10813
  listSavedJobs: () => listSavedJobs,
10813
10814
  profileToFingerprint: () => profileToFingerprint,
10814
10815
  readProfile: () => readProfile,
@@ -10849,6 +10850,17 @@ function migrateTagWeights(profile) {
10849
10850
  }
10850
10851
  }
10851
10852
  }
10853
+ function isBlankProfile(profile) {
10854
+ if (profile.skillTags.length > 0) return false;
10855
+ if (Object.keys(profile.tagWeights).length > 0) return false;
10856
+ for (const [key, value] of Object.entries(profile)) {
10857
+ if (DERIVED_KEYS.has(key)) continue;
10858
+ if (value === void 0 || value === null) continue;
10859
+ if (Array.isArray(value) && value.length === 0) continue;
10860
+ return false;
10861
+ }
10862
+ return true;
10863
+ }
10852
10864
  async function readProfile() {
10853
10865
  const parsed = await profileStore.read();
10854
10866
  migrateTagWeights(parsed);
@@ -10933,7 +10945,7 @@ function profileToFingerprint(profile) {
10933
10945
  }
10934
10946
  };
10935
10947
  }
10936
- var TERMINALHIRE_DIR3, PROFILE_FILE, profileStore, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
10948
+ var TERMINALHIRE_DIR3, PROFILE_FILE, profileStore, DERIVED_KEYS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
10937
10949
  var init_profile = __esm({
10938
10950
  "src/profile.ts"() {
10939
10951
  "use strict";
@@ -10945,6 +10957,13 @@ var init_profile = __esm({
10945
10957
  blank: blankProfile,
10946
10958
  keyPolicy: "keytar-first-file-fallback"
10947
10959
  });
10960
+ DERIVED_KEYS = /* @__PURE__ */ new Set([
10961
+ "version",
10962
+ "updatedAt",
10963
+ "skillTags",
10964
+ "tagWeights",
10965
+ "hasEmployerSessions"
10966
+ ]);
10948
10967
  LANGUAGE_TAGS = /* @__PURE__ */ new Set([
10949
10968
  "typescript",
10950
10969
  "javascript",