terminalhire 0.42.5 → 0.42.6

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.
@@ -11885,6 +11885,23 @@ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
11885
11885
  terminalhire claim ${ref}
11886
11886
  ${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
11887
11887
  }
11888
+ function wrapIndented(text, indent, width) {
11889
+ const pad = " ".repeat(indent);
11890
+ const words2 = String(text).split(/\s+/).filter(Boolean);
11891
+ if (words2.length === 0) return [];
11892
+ const lines = [];
11893
+ let line = "";
11894
+ for (const w of words2) {
11895
+ if (line === "") line = w;
11896
+ else if (line.length + 1 + w.length <= width) line += ` ${w}`;
11897
+ else {
11898
+ lines.push(pad + line);
11899
+ line = w;
11900
+ }
11901
+ }
11902
+ lines.push(pad + line);
11903
+ return lines;
11904
+ }
11888
11905
  function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
11889
11906
  const b = job.bounty ?? {};
11890
11907
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
@@ -11902,6 +11919,9 @@ ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
11902
11919
  );
11903
11920
  if (reason) console.log(` ${reason}`);
11904
11921
  if (continuityNote) console.log(` ${continuityNote}`);
11922
+ if (b.publicSummary) {
11923
+ for (const line of wrapIndented(sanitizeText(b.publicSummary), 3, 76)) console.log(line);
11924
+ }
11905
11925
  if (b.bountySource === "founder" && b.specProvenance) {
11906
11926
  console.log(
11907
11927
  ` Spec: ${b.specProvenance === "agent_drafted_human_confirmed" ? "agent drafted \xB7 founder confirmed" : "founder authored"}`
@@ -12140,7 +12160,8 @@ export {
12140
12160
  isPinnedFounderBounty,
12141
12161
  printBounty,
12142
12162
  rankBounties,
12143
- run
12163
+ run,
12164
+ wrapIndented
12144
12165
  };
12145
12166
  /*! Bundled license information:
12146
12167
 
@@ -15548,7 +15548,8 @@ __export(jpi_bounties_exports, {
15548
15548
  isPinnedFounderBounty: () => isPinnedFounderBounty,
15549
15549
  printBounty: () => printBounty,
15550
15550
  rankBounties: () => rankBounties,
15551
- run: () => run5
15551
+ run: () => run5,
15552
+ wrapIndented: () => wrapIndented
15552
15553
  });
15553
15554
  import { readFileSync as readFileSync18 } from "fs";
15554
15555
  import { join as join22 } from "path";
@@ -15607,6 +15608,23 @@ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
15607
15608
  terminalhire claim ${ref}
15608
15609
  ${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
15609
15610
  }
15611
+ function wrapIndented(text, indent, width) {
15612
+ const pad = " ".repeat(indent);
15613
+ const words2 = String(text).split(/\s+/).filter(Boolean);
15614
+ if (words2.length === 0) return [];
15615
+ const lines = [];
15616
+ let line = "";
15617
+ for (const w of words2) {
15618
+ if (line === "") line = w;
15619
+ else if (line.length + 1 + w.length <= width) line += ` ${w}`;
15620
+ else {
15621
+ lines.push(pad + line);
15622
+ line = w;
15623
+ }
15624
+ }
15625
+ lines.push(pad + line);
15626
+ return lines;
15627
+ }
15610
15628
  function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
15611
15629
  const b = job.bounty ?? {};
15612
15630
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
@@ -15624,6 +15642,9 @@ ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
15624
15642
  );
15625
15643
  if (reason) console.log(` ${reason}`);
15626
15644
  if (continuityNote) console.log(` ${continuityNote}`);
15645
+ if (b.publicSummary) {
15646
+ for (const line of wrapIndented(sanitizeText(b.publicSummary), 3, 76)) console.log(line);
15647
+ }
15627
15648
  if (b.bountySource === "founder" && b.specProvenance) {
15628
15649
  console.log(
15629
15650
  ` Spec: ${b.specProvenance === "agent_drafted_human_confirmed" ? "agent drafted \xB7 founder confirmed" : "founder authored"}`
@@ -35118,6 +35139,149 @@ var init_jpi_claim = __esm({
35118
35139
  }
35119
35140
  });
35120
35141
 
35142
+ // ../../packages/core/dist/secretScan.js
35143
+ function escapeRegExp(value) {
35144
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35145
+ }
35146
+ function findSecrets(value, options = {}) {
35147
+ const { currentHome = "", fixedOnly = false } = options;
35148
+ const found = [];
35149
+ const scan = (pattern, label, severity, span) => {
35150
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
35151
+ let match2;
35152
+ while ((match2 = re.exec(value)) !== null) {
35153
+ if (match2[0].length === 0) {
35154
+ re.lastIndex += 1;
35155
+ continue;
35156
+ }
35157
+ const hit = span(match2);
35158
+ if (hit)
35159
+ found.push({ label, severity, ...hit });
35160
+ }
35161
+ };
35162
+ const whole = (m, replacement) => ({
35163
+ start: m.index,
35164
+ end: m.index + m[0].length,
35165
+ replacement
35166
+ });
35167
+ for (const rule of FIXED_SECRET_RULES) {
35168
+ scan(rule.re, rule.label, "credential", (m) => whole(m, REDACTED));
35169
+ }
35170
+ if (fixedOnly)
35171
+ return found;
35172
+ scan(ASSIGNED_SECRET_RE, "an assigned secret", "credential", (m) => {
35173
+ const secret = m[4] ?? "";
35174
+ if (SECRET_REFERENCE_RE.test(secret))
35175
+ return null;
35176
+ const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
35177
+ return { start, end: start + secret.length, replacement: REDACTED };
35178
+ });
35179
+ if (currentHome) {
35180
+ scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", "path", (m) => whole(m, "~"));
35181
+ }
35182
+ scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", "path", (m) => whole(m, "~"));
35183
+ return found;
35184
+ }
35185
+ var REDACTED, FIXED_SECRET_RULES, ASSIGNED_SECRET_RE, SECRET_REFERENCE_RE, CROSS_PLATFORM_HOME_RE;
35186
+ var init_secretScan = __esm({
35187
+ "../../packages/core/dist/secretScan.js"() {
35188
+ "use strict";
35189
+ REDACTED = "[redacted]";
35190
+ FIXED_SECRET_RULES = [
35191
+ { label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
35192
+ /**
35193
+ * FINE-GRAINED PATS, which the rule above cannot match and which are the format GitHub
35194
+ * now issues by default. `github_pat_…` fails `gh[pousr]_` on its third character, and
35195
+ * `_` is a word character so there is no boundary for the rule to catch it mid-string
35196
+ * either — a fine-grained token scored ZERO findings and went out on the anonymous
35197
+ * index. Codex found it after two other reviewers and I had all read this list.
35198
+ *
35199
+ * The lesson is about the tests, not the regex: every case written for this scanner
35200
+ * used `ghp_`, so a hundred more of them would still have missed it. A rule list is
35201
+ * only as good as the shapes someone thought to try.
35202
+ */
35203
+ { label: "a GitHub token", re: /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g },
35204
+ { label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
35205
+ /**
35206
+ * THE REST OF THE FORMATS A DEVELOPER ACTUALLY PASTES. Found by sweeping candidate
35207
+ * credentials through `findSecrets` and printing which scored zero, rather than by
35208
+ * reading the list and thinking hard — reading it is what missed `github_pat_`.
35209
+ *
35210
+ * Every one of these is a self-identifying PREFIX, which is the property that makes a
35211
+ * rule safe to add here: it can only match a shape that is already a credential, so it
35212
+ * costs nothing when it is wrong. That is also why the AWS SECRET access key is
35213
+ * deliberately absent — it is forty characters of base64 with no prefix, and a rule for
35214
+ * it would refuse any posting that quoted a hash, a checksum or a build id.
35215
+ */
35216
+ { label: "an Anthropic API key", re: /\bsk-ant-[A-Za-z0-9-]{20,}\b/g },
35217
+ /**
35218
+ * NO HYPHEN IN THE BODY, and that restriction is the whole rule. The first cut allowed
35219
+ * `[A-Za-z0-9_-]{20,}` and refused `sk-payments-worker-staging`, `sk-checkout-service-eu-west-1`
35220
+ * and the branch name `sk-fix-the-retry-loop-again` — kebab-case is how services and
35221
+ * branches are named, and `sk-` is an ordinary prefix for one. Four out of four
35222
+ * realistic strings I tried were refused as OpenAI keys.
35223
+ *
35224
+ * A real key carries a long unbroken run of base64 characters; a kebab name never does,
35225
+ * because it has a hyphen every few characters. Requiring twenty consecutive non-hyphen
35226
+ * characters separates them cleanly and costs nothing on the token side.
35227
+ *
35228
+ * This is the rule that would have taught founders to switch the scanner off, and I
35229
+ * only found it because I went looking for false positives as hard as I had gone
35230
+ * looking for misses. The seven prose cases I wrote first all passed.
35231
+ */
35232
+ { label: "an OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_]{20,}\b/g },
35233
+ {
35234
+ label: "a Slack token",
35235
+ // `xoxe` (refresh) and `xapp` (app-level) are documented families the first cut did
35236
+ // not know, so both produced zero findings. Same lesson as `github_pat_`: a prefix
35237
+ // list is a guess about a vendor's naming until someone checks the vendor's docs.
35238
+ re: /\b(?:xox[baprse]-|xapp-)[A-Za-z0-9-]{10,}\b/g
35239
+ },
35240
+ { label: "a Google API key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
35241
+ { label: "an npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g },
35242
+ /**
35243
+ * A JWT is not always a secret — plenty are short-lived and harmless. It is here anyway
35244
+ * because the ones people paste into bug reports are session and access tokens, and the
35245
+ * three-segment `eyJ…` shape is specific enough that a false match is nearly impossible.
35246
+ */
35247
+ { label: "a JSON web token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
35248
+ /**
35249
+ * A password inline in a connection string. Anchored on the SCHEME so it cannot fire on
35250
+ * ordinary prose containing an `@`, and it takes the whole credential pair rather than
35251
+ * just the password — publishing the username alongside the host is most of the harm.
35252
+ */
35253
+ {
35254
+ label: "a database password",
35255
+ re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:/@]+:[^\s:/@]{6,}@/g
35256
+ },
35257
+ { label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
35258
+ {
35259
+ label: "a private key",
35260
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
35261
+ }
35262
+ ];
35263
+ ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
35264
+ SECRET_REFERENCE_RE = new RegExp([
35265
+ "^process\\.env\\.[A-Za-z_$][\\w$]*$",
35266
+ "^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
35267
+ "^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
35268
+ "^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
35269
+ "^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
35270
+ "^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
35271
+ "^\\$\\{?[A-Za-z_]\\w*\\}?$",
35272
+ // $VAR, ${VAR}
35273
+ "^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
35274
+ // crypto.randomUUID()
35275
+ // A call WITH arguments, but only when no argument is long enough to be a
35276
+ // credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
35277
+ // a secret wearing a function's clothes.
35278
+ "^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
35279
+ "^(?:null|undefined|true|false)$"
35280
+ ].join("|"), "i");
35281
+ CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
35282
+ }
35283
+ });
35284
+
35121
35285
  // src/posting-drafts.ts
35122
35286
  import {
35123
35287
  chmodSync as chmodSync2,
@@ -35248,9 +35412,6 @@ function toSubmittedPosting(draft) {
35248
35412
  ...draft.repo ? { repo: draft.repo } : {}
35249
35413
  };
35250
35414
  }
35251
- function escapeRegExp(value) {
35252
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35253
- }
35254
35415
  function assemblePieces(draft) {
35255
35416
  const parts = [];
35256
35417
  if (draft.title) {
@@ -35286,35 +35447,7 @@ function assemblePieces(draft) {
35286
35447
  return { scanned, pieces };
35287
35448
  }
35288
35449
  function detections(value, currentHome, fixedOnly = false) {
35289
- const found = [];
35290
- const scan = (pattern, label, span) => {
35291
- const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
35292
- let match2;
35293
- while ((match2 = re.exec(value)) !== null) {
35294
- if (match2[0].length === 0) {
35295
- re.lastIndex += 1;
35296
- continue;
35297
- }
35298
- const hit = span(match2);
35299
- if (hit) found.push({ label, ...hit });
35300
- }
35301
- };
35302
- const whole = (m, replacement) => ({
35303
- start: m.index,
35304
- end: m.index + m[0].length,
35305
- replacement
35306
- });
35307
- for (const rule of FIXED_SECRET_RULES) scan(rule.re, rule.label, (m) => whole(m, REDACTED));
35308
- if (fixedOnly) return found;
35309
- scan(ASSIGNED_SECRET_RE, "an assigned secret", (m) => {
35310
- const secret = m[4] ?? "";
35311
- if (SECRET_REFERENCE_RE.test(secret)) return null;
35312
- const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
35313
- return { start, end: start + secret.length, replacement: REDACTED };
35314
- });
35315
- if (currentHome) scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", (m) => whole(m, "~"));
35316
- scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", (m) => whole(m, "~"));
35317
- return found;
35450
+ return findSecrets(value, { currentHome, fixedOnly });
35318
35451
  }
35319
35452
  function boundaryDetections(scanned, pieces, currentHome, already) {
35320
35453
  let tight = "";
@@ -35454,45 +35587,14 @@ function preparePostingSubmission(draft, currentHome = homedir23()) {
35454
35587
  notices: draft.captureFailures.map((failure) => `could not capture ${failure}`)
35455
35588
  };
35456
35589
  }
35457
- var REDACTED, EMPTY_AFTER_REDACTION_RE, FIXED_SECRET_RULES, ASSIGNED_SECRET_RE, SECRET_REFERENCE_RE, SECRET_PATH_RE, CROSS_PLATFORM_HOME_RE;
35590
+ var EMPTY_AFTER_REDACTION_RE, SECRET_PATH_RE;
35458
35591
  var init_posting_drafts = __esm({
35459
35592
  "src/posting-drafts.ts"() {
35460
35593
  "use strict";
35461
35594
  init_state_dir();
35462
- REDACTED = "[redacted]";
35595
+ init_secretScan();
35463
35596
  EMPTY_AFTER_REDACTION_RE = /(?:\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*|\[redacted\]|["'`~]|\s)+/g;
35464
- FIXED_SECRET_RULES = [
35465
- { label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
35466
- { label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
35467
- { label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
35468
- {
35469
- label: "a private key",
35470
- re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
35471
- }
35472
- ];
35473
- ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
35474
- SECRET_REFERENCE_RE = new RegExp(
35475
- [
35476
- "^process\\.env\\.[A-Za-z_$][\\w$]*$",
35477
- "^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
35478
- "^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
35479
- "^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
35480
- "^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
35481
- "^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
35482
- "^\\$\\{?[A-Za-z_]\\w*\\}?$",
35483
- // $VAR, ${VAR}
35484
- "^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
35485
- // crypto.randomUUID()
35486
- // A call WITH arguments, but only when no argument is long enough to be a
35487
- // credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
35488
- // a secret wearing a function's clothes.
35489
- "^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
35490
- "^(?:null|undefined|true|false)$"
35491
- ].join("|"),
35492
- "i"
35493
- );
35494
35597
  SECRET_PATH_RE = /(?:^|[/\\])(?:\.env(?:\.[^/\\]+)?|id_(?:rsa|ed25519)|credentials(?:\.json)?|secrets?)(?:$|[/\\])/i;
35495
- CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
35496
35598
  }
35497
35599
  });
35498
35600
 
@@ -92,6 +92,143 @@ function ensureStateDirForSecret(dir) {
92
92
  applyStateDirSecretPolicy(dir, ensureStateDir(dir));
93
93
  }
94
94
 
95
+ // ../../packages/core/dist/secretScan.js
96
+ var REDACTED = "[redacted]";
97
+ var FIXED_SECRET_RULES = [
98
+ { label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
99
+ /**
100
+ * FINE-GRAINED PATS, which the rule above cannot match and which are the format GitHub
101
+ * now issues by default. `github_pat_…` fails `gh[pousr]_` on its third character, and
102
+ * `_` is a word character so there is no boundary for the rule to catch it mid-string
103
+ * either — a fine-grained token scored ZERO findings and went out on the anonymous
104
+ * index. Codex found it after two other reviewers and I had all read this list.
105
+ *
106
+ * The lesson is about the tests, not the regex: every case written for this scanner
107
+ * used `ghp_`, so a hundred more of them would still have missed it. A rule list is
108
+ * only as good as the shapes someone thought to try.
109
+ */
110
+ { label: "a GitHub token", re: /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g },
111
+ { label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
112
+ /**
113
+ * THE REST OF THE FORMATS A DEVELOPER ACTUALLY PASTES. Found by sweeping candidate
114
+ * credentials through `findSecrets` and printing which scored zero, rather than by
115
+ * reading the list and thinking hard — reading it is what missed `github_pat_`.
116
+ *
117
+ * Every one of these is a self-identifying PREFIX, which is the property that makes a
118
+ * rule safe to add here: it can only match a shape that is already a credential, so it
119
+ * costs nothing when it is wrong. That is also why the AWS SECRET access key is
120
+ * deliberately absent — it is forty characters of base64 with no prefix, and a rule for
121
+ * it would refuse any posting that quoted a hash, a checksum or a build id.
122
+ */
123
+ { label: "an Anthropic API key", re: /\bsk-ant-[A-Za-z0-9-]{20,}\b/g },
124
+ /**
125
+ * NO HYPHEN IN THE BODY, and that restriction is the whole rule. The first cut allowed
126
+ * `[A-Za-z0-9_-]{20,}` and refused `sk-payments-worker-staging`, `sk-checkout-service-eu-west-1`
127
+ * and the branch name `sk-fix-the-retry-loop-again` — kebab-case is how services and
128
+ * branches are named, and `sk-` is an ordinary prefix for one. Four out of four
129
+ * realistic strings I tried were refused as OpenAI keys.
130
+ *
131
+ * A real key carries a long unbroken run of base64 characters; a kebab name never does,
132
+ * because it has a hyphen every few characters. Requiring twenty consecutive non-hyphen
133
+ * characters separates them cleanly and costs nothing on the token side.
134
+ *
135
+ * This is the rule that would have taught founders to switch the scanner off, and I
136
+ * only found it because I went looking for false positives as hard as I had gone
137
+ * looking for misses. The seven prose cases I wrote first all passed.
138
+ */
139
+ { label: "an OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_]{20,}\b/g },
140
+ {
141
+ label: "a Slack token",
142
+ // `xoxe` (refresh) and `xapp` (app-level) are documented families the first cut did
143
+ // not know, so both produced zero findings. Same lesson as `github_pat_`: a prefix
144
+ // list is a guess about a vendor's naming until someone checks the vendor's docs.
145
+ re: /\b(?:xox[baprse]-|xapp-)[A-Za-z0-9-]{10,}\b/g
146
+ },
147
+ { label: "a Google API key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
148
+ { label: "an npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g },
149
+ /**
150
+ * A JWT is not always a secret — plenty are short-lived and harmless. It is here anyway
151
+ * because the ones people paste into bug reports are session and access tokens, and the
152
+ * three-segment `eyJ…` shape is specific enough that a false match is nearly impossible.
153
+ */
154
+ { label: "a JSON web token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
155
+ /**
156
+ * A password inline in a connection string. Anchored on the SCHEME so it cannot fire on
157
+ * ordinary prose containing an `@`, and it takes the whole credential pair rather than
158
+ * just the password — publishing the username alongside the host is most of the harm.
159
+ */
160
+ {
161
+ label: "a database password",
162
+ re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:/@]+:[^\s:/@]{6,}@/g
163
+ },
164
+ { label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
165
+ {
166
+ label: "a private key",
167
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
168
+ }
169
+ ];
170
+ var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
171
+ var SECRET_REFERENCE_RE = new RegExp([
172
+ "^process\\.env\\.[A-Za-z_$][\\w$]*$",
173
+ "^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
174
+ "^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
175
+ "^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
176
+ "^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
177
+ "^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
178
+ "^\\$\\{?[A-Za-z_]\\w*\\}?$",
179
+ // $VAR, ${VAR}
180
+ "^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
181
+ // crypto.randomUUID()
182
+ // A call WITH arguments, but only when no argument is long enough to be a
183
+ // credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
184
+ // a secret wearing a function's clothes.
185
+ "^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
186
+ "^(?:null|undefined|true|false)$"
187
+ ].join("|"), "i");
188
+ var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
189
+ function escapeRegExp(value) {
190
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
191
+ }
192
+ function findSecrets(value, options = {}) {
193
+ const { currentHome = "", fixedOnly = false } = options;
194
+ const found = [];
195
+ const scan = (pattern, label, severity, span) => {
196
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
197
+ let match;
198
+ while ((match = re.exec(value)) !== null) {
199
+ if (match[0].length === 0) {
200
+ re.lastIndex += 1;
201
+ continue;
202
+ }
203
+ const hit = span(match);
204
+ if (hit)
205
+ found.push({ label, severity, ...hit });
206
+ }
207
+ };
208
+ const whole = (m, replacement) => ({
209
+ start: m.index,
210
+ end: m.index + m[0].length,
211
+ replacement
212
+ });
213
+ for (const rule of FIXED_SECRET_RULES) {
214
+ scan(rule.re, rule.label, "credential", (m) => whole(m, REDACTED));
215
+ }
216
+ if (fixedOnly)
217
+ return found;
218
+ scan(ASSIGNED_SECRET_RE, "an assigned secret", "credential", (m) => {
219
+ const secret = m[4] ?? "";
220
+ if (SECRET_REFERENCE_RE.test(secret))
221
+ return null;
222
+ const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
223
+ return { start, end: start + secret.length, replacement: REDACTED };
224
+ });
225
+ if (currentHome) {
226
+ scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", "path", (m) => whole(m, "~"));
227
+ }
228
+ scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", "path", (m) => whole(m, "~"));
229
+ return found;
230
+ }
231
+
95
232
  // src/posting-drafts.ts
96
233
  function stateDir() {
97
234
  return process.env["TERMINALHIRE_DIR"] || join(homedir(), ".terminalhire");
@@ -211,43 +348,8 @@ function toSubmittedPosting(draft) {
211
348
  ...draft.repo ? { repo: draft.repo } : {}
212
349
  };
213
350
  }
214
- var REDACTED = "[redacted]";
215
351
  var EMPTY_AFTER_REDACTION_RE = /(?:\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*|\[redacted\]|["'`~]|\s)+/g;
216
- var FIXED_SECRET_RULES = [
217
- { label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
218
- { label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
219
- { label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
220
- {
221
- label: "a private key",
222
- re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
223
- }
224
- ];
225
- var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
226
- var SECRET_REFERENCE_RE = new RegExp(
227
- [
228
- "^process\\.env\\.[A-Za-z_$][\\w$]*$",
229
- "^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
230
- "^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
231
- "^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
232
- "^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
233
- "^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
234
- "^\\$\\{?[A-Za-z_]\\w*\\}?$",
235
- // $VAR, ${VAR}
236
- "^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
237
- // crypto.randomUUID()
238
- // A call WITH arguments, but only when no argument is long enough to be a
239
- // credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
240
- // a secret wearing a function's clothes.
241
- "^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
242
- "^(?:null|undefined|true|false)$"
243
- ].join("|"),
244
- "i"
245
- );
246
352
  var SECRET_PATH_RE = /(?:^|[/\\])(?:\.env(?:\.[^/\\]+)?|id_(?:rsa|ed25519)|credentials(?:\.json)?|secrets?)(?:$|[/\\])/i;
247
- var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
248
- function escapeRegExp(value) {
249
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
250
- }
251
353
  function assemblePieces(draft) {
252
354
  const parts = [];
253
355
  if (draft.title) {
@@ -283,35 +385,7 @@ function assemblePieces(draft) {
283
385
  return { scanned, pieces };
284
386
  }
285
387
  function detections(value, currentHome, fixedOnly = false) {
286
- const found = [];
287
- const scan = (pattern, label, span) => {
288
- const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
289
- let match;
290
- while ((match = re.exec(value)) !== null) {
291
- if (match[0].length === 0) {
292
- re.lastIndex += 1;
293
- continue;
294
- }
295
- const hit = span(match);
296
- if (hit) found.push({ label, ...hit });
297
- }
298
- };
299
- const whole = (m, replacement) => ({
300
- start: m.index,
301
- end: m.index + m[0].length,
302
- replacement
303
- });
304
- for (const rule of FIXED_SECRET_RULES) scan(rule.re, rule.label, (m) => whole(m, REDACTED));
305
- if (fixedOnly) return found;
306
- scan(ASSIGNED_SECRET_RE, "an assigned secret", (m) => {
307
- const secret = m[4] ?? "";
308
- if (SECRET_REFERENCE_RE.test(secret)) return null;
309
- const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
310
- return { start, end: start + secret.length, replacement: REDACTED };
311
- });
312
- if (currentHome) scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", (m) => whole(m, "~"));
313
- scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", (m) => whole(m, "~"));
314
- return found;
388
+ return findSecrets(value, { currentHome, fixedOnly });
315
389
  }
316
390
  function boundaryDetections(scanned, pieces, currentHome, already) {
317
391
  let tight = "";
@@ -84,6 +84,143 @@ function ensureStateDirForSecret(dir) {
84
84
  applyStateDirSecretPolicy(dir, ensureStateDir(dir));
85
85
  }
86
86
 
87
+ // ../../packages/core/dist/secretScan.js
88
+ var REDACTED = "[redacted]";
89
+ var FIXED_SECRET_RULES = [
90
+ { label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
91
+ /**
92
+ * FINE-GRAINED PATS, which the rule above cannot match and which are the format GitHub
93
+ * now issues by default. `github_pat_…` fails `gh[pousr]_` on its third character, and
94
+ * `_` is a word character so there is no boundary for the rule to catch it mid-string
95
+ * either — a fine-grained token scored ZERO findings and went out on the anonymous
96
+ * index. Codex found it after two other reviewers and I had all read this list.
97
+ *
98
+ * The lesson is about the tests, not the regex: every case written for this scanner
99
+ * used `ghp_`, so a hundred more of them would still have missed it. A rule list is
100
+ * only as good as the shapes someone thought to try.
101
+ */
102
+ { label: "a GitHub token", re: /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g },
103
+ { label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
104
+ /**
105
+ * THE REST OF THE FORMATS A DEVELOPER ACTUALLY PASTES. Found by sweeping candidate
106
+ * credentials through `findSecrets` and printing which scored zero, rather than by
107
+ * reading the list and thinking hard — reading it is what missed `github_pat_`.
108
+ *
109
+ * Every one of these is a self-identifying PREFIX, which is the property that makes a
110
+ * rule safe to add here: it can only match a shape that is already a credential, so it
111
+ * costs nothing when it is wrong. That is also why the AWS SECRET access key is
112
+ * deliberately absent — it is forty characters of base64 with no prefix, and a rule for
113
+ * it would refuse any posting that quoted a hash, a checksum or a build id.
114
+ */
115
+ { label: "an Anthropic API key", re: /\bsk-ant-[A-Za-z0-9-]{20,}\b/g },
116
+ /**
117
+ * NO HYPHEN IN THE BODY, and that restriction is the whole rule. The first cut allowed
118
+ * `[A-Za-z0-9_-]{20,}` and refused `sk-payments-worker-staging`, `sk-checkout-service-eu-west-1`
119
+ * and the branch name `sk-fix-the-retry-loop-again` — kebab-case is how services and
120
+ * branches are named, and `sk-` is an ordinary prefix for one. Four out of four
121
+ * realistic strings I tried were refused as OpenAI keys.
122
+ *
123
+ * A real key carries a long unbroken run of base64 characters; a kebab name never does,
124
+ * because it has a hyphen every few characters. Requiring twenty consecutive non-hyphen
125
+ * characters separates them cleanly and costs nothing on the token side.
126
+ *
127
+ * This is the rule that would have taught founders to switch the scanner off, and I
128
+ * only found it because I went looking for false positives as hard as I had gone
129
+ * looking for misses. The seven prose cases I wrote first all passed.
130
+ */
131
+ { label: "an OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_]{20,}\b/g },
132
+ {
133
+ label: "a Slack token",
134
+ // `xoxe` (refresh) and `xapp` (app-level) are documented families the first cut did
135
+ // not know, so both produced zero findings. Same lesson as `github_pat_`: a prefix
136
+ // list is a guess about a vendor's naming until someone checks the vendor's docs.
137
+ re: /\b(?:xox[baprse]-|xapp-)[A-Za-z0-9-]{10,}\b/g
138
+ },
139
+ { label: "a Google API key", re: /\bAIza[A-Za-z0-9_-]{35}\b/g },
140
+ { label: "an npm token", re: /\bnpm_[A-Za-z0-9]{36}\b/g },
141
+ /**
142
+ * A JWT is not always a secret — plenty are short-lived and harmless. It is here anyway
143
+ * because the ones people paste into bug reports are session and access tokens, and the
144
+ * three-segment `eyJ…` shape is specific enough that a false match is nearly impossible.
145
+ */
146
+ { label: "a JSON web token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
147
+ /**
148
+ * A password inline in a connection string. Anchored on the SCHEME so it cannot fire on
149
+ * ordinary prose containing an `@`, and it takes the whole credential pair rather than
150
+ * just the password — publishing the username alongside the host is most of the harm.
151
+ */
152
+ {
153
+ label: "a database password",
154
+ re: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s:/@]+:[^\s:/@]{6,}@/g
155
+ },
156
+ { label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
157
+ {
158
+ label: "a private key",
159
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
160
+ }
161
+ ];
162
+ var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
163
+ var SECRET_REFERENCE_RE = new RegExp([
164
+ "^process\\.env\\.[A-Za-z_$][\\w$]*$",
165
+ "^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
166
+ "^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
167
+ "^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
168
+ "^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
169
+ "^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
170
+ "^\\$\\{?[A-Za-z_]\\w*\\}?$",
171
+ // $VAR, ${VAR}
172
+ "^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
173
+ // crypto.randomUUID()
174
+ // A call WITH arguments, but only when no argument is long enough to be a
175
+ // credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
176
+ // a secret wearing a function's clothes.
177
+ "^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
178
+ "^(?:null|undefined|true|false)$"
179
+ ].join("|"), "i");
180
+ var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
181
+ function escapeRegExp(value) {
182
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
183
+ }
184
+ function findSecrets(value, options = {}) {
185
+ const { currentHome = "", fixedOnly = false } = options;
186
+ const found = [];
187
+ const scan = (pattern, label, severity, span) => {
188
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
189
+ let match;
190
+ while ((match = re.exec(value)) !== null) {
191
+ if (match[0].length === 0) {
192
+ re.lastIndex += 1;
193
+ continue;
194
+ }
195
+ const hit = span(match);
196
+ if (hit)
197
+ found.push({ label, severity, ...hit });
198
+ }
199
+ };
200
+ const whole = (m, replacement) => ({
201
+ start: m.index,
202
+ end: m.index + m[0].length,
203
+ replacement
204
+ });
205
+ for (const rule of FIXED_SECRET_RULES) {
206
+ scan(rule.re, rule.label, "credential", (m) => whole(m, REDACTED));
207
+ }
208
+ if (fixedOnly)
209
+ return found;
210
+ scan(ASSIGNED_SECRET_RE, "an assigned secret", "credential", (m) => {
211
+ const secret = m[4] ?? "";
212
+ if (SECRET_REFERENCE_RE.test(secret))
213
+ return null;
214
+ const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
215
+ return { start, end: start + secret.length, replacement: REDACTED };
216
+ });
217
+ if (currentHome) {
218
+ scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", "path", (m) => whole(m, "~"));
219
+ }
220
+ scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", "path", (m) => whole(m, "~"));
221
+ return found;
222
+ }
223
+
87
224
  // src/posting-drafts.ts
88
225
  var POSTING_DRAFT_FIELD_POLICY = {
89
226
  id: "local",
@@ -220,43 +357,8 @@ function toSubmittedPosting(draft) {
220
357
  ...draft.repo ? { repo: draft.repo } : {}
221
358
  };
222
359
  }
223
- var REDACTED = "[redacted]";
224
360
  var EMPTY_AFTER_REDACTION_RE = /(?:\b(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*|\[redacted\]|["'`~]|\s)+/g;
225
- var FIXED_SECRET_RULES = [
226
- { label: "a GitHub token", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
227
- { label: "an AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/g },
228
- { label: "a Stripe secret key", re: /\bsk_(?:live|test)_[A-Za-z0-9]{12,}\b/g },
229
- {
230
- label: "a private key",
231
- re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g
232
- }
233
- ];
234
- var ASSIGNED_SECRET_RE = /\b(password|passwd|secret|token|api[_-]?key)(\s*[:=]\s*)(["'`]?)([^\s"'`]{12,})\3/gi;
235
- var SECRET_REFERENCE_RE = new RegExp(
236
- [
237
- "^process\\.env\\.[A-Za-z_$][\\w$]*$",
238
- "^process\\.env\\[[\"'`][^\"'`]*[\"'`]\\]$",
239
- "^import\\.meta\\.env\\.[A-Za-z_$][\\w$]*$",
240
- "^Deno\\.env\\.get\\([\"'`][^\"'`]*[\"'`]\\)$",
241
- "^os\\.environ(?:\\.get\\(|\\[)[\"'`]?[A-Za-z_]\\w*[\"'`]?[\\])]$",
242
- "^ENV\\[[\"'`]?[A-Za-z_]\\w*[\"'`]?\\]$",
243
- "^\\$\\{?[A-Za-z_]\\w*\\}?$",
244
- // $VAR, ${VAR}
245
- "^[A-Za-z_$][\\w$.]*\\(\\s*\\)$",
246
- // crypto.randomUUID()
247
- // A call WITH arguments, but only when no argument is long enough to be a
248
- // credential — `getSecret(actualHardcodedSecret123456)` is not a reference, it is
249
- // a secret wearing a function's clothes.
250
- "^[A-Za-z_$][\\w$.]*\\((?![^)]{12,})[\\w$.,\\s]*\\)$",
251
- "^(?:null|undefined|true|false)$"
252
- ].join("|"),
253
- "i"
254
- );
255
361
  var SECRET_PATH_RE = /(?:^|[/\\])(?:\.env(?:\.[^/\\]+)?|id_(?:rsa|ed25519)|credentials(?:\.json)?|secrets?)(?:$|[/\\])/i;
256
- var CROSS_PLATFORM_HOME_RE = /(?:\/(?:Users|home)\/[^/\s]+|[A-Za-z]:\\Users\\[^\\\s]+)/g;
257
- function escapeRegExp(value) {
258
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
259
- }
260
362
  function assemblePieces(draft) {
261
363
  const parts = [];
262
364
  if (draft.title) {
@@ -292,35 +394,7 @@ function assemblePieces(draft) {
292
394
  return { scanned, pieces };
293
395
  }
294
396
  function detections(value, currentHome, fixedOnly = false) {
295
- const found = [];
296
- const scan = (pattern, label, span) => {
297
- const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
298
- let match;
299
- while ((match = re.exec(value)) !== null) {
300
- if (match[0].length === 0) {
301
- re.lastIndex += 1;
302
- continue;
303
- }
304
- const hit = span(match);
305
- if (hit) found.push({ label, ...hit });
306
- }
307
- };
308
- const whole = (m, replacement) => ({
309
- start: m.index,
310
- end: m.index + m[0].length,
311
- replacement
312
- });
313
- for (const rule of FIXED_SECRET_RULES) scan(rule.re, rule.label, (m) => whole(m, REDACTED));
314
- if (fixedOnly) return found;
315
- scan(ASSIGNED_SECRET_RE, "an assigned secret", (m) => {
316
- const secret = m[4] ?? "";
317
- if (SECRET_REFERENCE_RE.test(secret)) return null;
318
- const start = m.index + (m[1] ?? "").length + (m[2] ?? "").length + (m[3] ?? "").length;
319
- return { start, end: start + secret.length, replacement: REDACTED };
320
- });
321
- if (currentHome) scan(new RegExp(escapeRegExp(currentHome)), "a home-directory path", (m) => whole(m, "~"));
322
- scan(CROSS_PLATFORM_HOME_RE, "a home-directory path", (m) => whole(m, "~"));
323
- return found;
397
+ return findSecrets(value, { currentHome, fixedOnly });
324
398
  }
325
399
  function boundaryDetections(scanned, pieces, currentHome, already) {
326
400
  let tight = "";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.42.5",
4
- "description": "Local-first job matching for developers \u2014 ambient job matches in the Claude Code spinner. Your profile never leaves your machine.",
3
+ "version": "0.42.6",
4
+ "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Your profile never leaves your machine.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/staqsIO/terminalhire-plugin.git"