terminalhire 0.33.0 → 0.34.0

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.
Files changed (42) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/claim-push-bg.js +122 -56
  3. package/dist/bin/jpi-bounties.js +1014 -296
  4. package/dist/bin/jpi-chat-read.js +118 -50
  5. package/dist/bin/jpi-chat.js +271 -125
  6. package/dist/bin/jpi-claim.js +1280 -423
  7. package/dist/bin/jpi-contribute.js +646 -167
  8. package/dist/bin/jpi-devs.js +613 -236
  9. package/dist/bin/jpi-dispatch.js +2354 -762
  10. package/dist/bin/jpi-hub.js +1136 -337
  11. package/dist/bin/jpi-inbox.js +271 -125
  12. package/dist/bin/jpi-init.js +490 -168
  13. package/dist/bin/jpi-intro.js +252 -112
  14. package/dist/bin/jpi-jobs.js +631 -254
  15. package/dist/bin/jpi-learn.js +252 -112
  16. package/dist/bin/jpi-login.js +651 -258
  17. package/dist/bin/jpi-profile.js +252 -112
  18. package/dist/bin/jpi-project.js +461 -162
  19. package/dist/bin/jpi-protocol.js +451 -0
  20. package/dist/bin/jpi-refresh.js +809 -366
  21. package/dist/bin/jpi-repo.js +1858 -0
  22. package/dist/bin/jpi-save.js +256 -116
  23. package/dist/bin/jpi-spinner.js +68 -25
  24. package/dist/bin/jpi-sync.js +258 -118
  25. package/dist/bin/jpi-trajectory.js +261 -121
  26. package/dist/bin/peer-connect-prompt.js +2 -2
  27. package/dist/bin/spinner.js +67 -24
  28. package/dist/src/acceptance-score.js +16 -22
  29. package/dist/src/chat-client.js +85 -36
  30. package/dist/src/chat-keystore.js +85 -36
  31. package/dist/src/claims.js +100 -40
  32. package/dist/src/crypto-store.js +248 -0
  33. package/dist/src/github-auth.js +30 -14
  34. package/dist/src/intro.js +252 -112
  35. package/dist/src/profile.js +204 -90
  36. package/dist/src/protocol.js +380 -0
  37. package/dist/src/repo-experience.js +1067 -0
  38. package/dist/src/reputation/fetch.js +68 -25
  39. package/dist/src/signal.js +67 -24
  40. package/dist/src/trajectory.js +261 -121
  41. package/install.js +3 -3
  42. package/package.json +2 -2
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. Matching runs entirely on your device. Your profile never leaves your machine.
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.
4
4
 
5
5
  **Domain:** [terminalhire.com](https://terminalhire.com)
6
6
 
@@ -40,7 +40,7 @@ terminalhire spinner --off
40
40
  terminalhire login # sign in with GitHub — enriches profile instantly (recommended)
41
41
  terminalhire logout # clear stored GitHub token from ~/.terminalhire/github-token.enc
42
42
 
43
- terminalhire jobs # fetch index, match locally, browse ranked roles
43
+ terminalhire jobs # fetch index, browse ranked roles matched to your profile
44
44
  terminalhire jobs --limit 20 # show top 20 results (default: 10)
45
45
  terminalhire jobs --remote-only # filter to remote roles only
46
46
  terminalhire jobs --all # show all matches above zero score
@@ -57,7 +57,7 @@ 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, matched 100% locally
60
+ terminalhire devs # opted-in peers & founders profile never leaves 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
63
  terminalhire mcp --print-config # read-only MCP server config for VS Code/Cursor/Codex/Zed/JetBrains
@@ -157,7 +157,7 @@ The nudge is printed at most once per Claude Code session. It reads only `~/.ter
157
157
  While you work inside Claude Code, terminalhire surfaces your top local job matches directly in the spinner — the ambient status indicator that appears while Claude is thinking.
158
158
 
159
159
  - **Enabled at install.** No additional setup required.
160
- - **Computed locally.** Match scoring runs on your machine against your encrypted local profile. No network call is made for the spinner display.
160
+ - **Profile never leaves your machine.** Match scoring runs against your encrypted local profile. No network call is made for the spinner display.
161
161
  - **Zero egress.** The spinner reads only `~/.terminalhire/index-cache.json` (written by the last `terminalhire jobs` run). Nothing leaves your device.
162
162
  - **Reversible.** Disable at any time without uninstalling:
163
163
 
@@ -45,9 +45,9 @@ var init_keytar = __esm({
45
45
  }
46
46
  });
47
47
 
48
- // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node
48
+ // node-file:/private/tmp/claude-501/-Users-ericgang-job-placement-inline/9716ff9c-0531-4844-adf4-286763cf8ab8/scratchpad/wt-v034/node_modules/keytar/build/Release/keytar.node
49
49
  var require_keytar = __commonJS({
50
- "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/agent-a0bc554500876cd51/node_modules/keytar/build/Release/keytar.node"(exports, module) {
50
+ "node-file:/private/tmp/claude-501/-Users-ericgang-job-placement-inline/9716ff9c-0531-4844-adf4-286763cf8ab8/scratchpad/wt-v034/node_modules/keytar/build/Release/keytar.node"(exports, module) {
51
51
  "use strict";
52
52
  init_keytar();
53
53
  try {
@@ -103,15 +103,49 @@ __export(claims_exports, {
103
103
  acceptedPRRate: () => acceptedPRRate,
104
104
  findClaim: () => findClaim,
105
105
  listClaims: () => listClaims,
106
+ nextPolledState: () => nextPolledState,
106
107
  readClaims: () => readClaims,
107
108
  recordClaim: () => recordClaim,
108
109
  removeClaim: () => removeClaim,
109
110
  toPushedClaim: () => toPushedClaim,
110
111
  updateClaim: () => updateClaim
111
112
  });
112
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync2 } from "fs";
113
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, renameSync as renameSync2, existsSync as existsSync2, rmSync as rmSync2, statSync } from "fs";
114
+ import { randomBytes as randomBytes2 } from "crypto";
113
115
  import { join as join2 } from "path";
114
116
  import { homedir as homedir2 } from "os";
117
+ function sleepSync(ms) {
118
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
119
+ }
120
+ function withClaimsLock(fn) {
121
+ mkdirSync2(TERMINALHIRE_DIR2, { recursive: true, mode: 448 });
122
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
123
+ for (; ; ) {
124
+ try {
125
+ mkdirSync2(LOCK_DIR, { mode: 448 });
126
+ break;
127
+ } catch {
128
+ try {
129
+ if (Date.now() - statSync(LOCK_DIR).mtimeMs > LOCK_STALE_MS) {
130
+ rmSync2(LOCK_DIR, { recursive: true, force: true });
131
+ continue;
132
+ }
133
+ } catch {
134
+ }
135
+ if (Date.now() > deadline) {
136
+ throw new Error(
137
+ `claims store is locked (another terminalhire process?) \u2014 remove ${LOCK_DIR} if no other process is running`
138
+ );
139
+ }
140
+ sleepSync(LOCK_RETRY_MS);
141
+ }
142
+ }
143
+ try {
144
+ return fn();
145
+ } finally {
146
+ rmSync2(LOCK_DIR, { recursive: true, force: true });
147
+ }
148
+ }
115
149
  function toPushedClaim(claim) {
116
150
  return {
117
151
  kind: claim.kind,
@@ -123,6 +157,9 @@ function toPushedClaim(claim) {
123
157
  updatedAt: claim.updatedAt
124
158
  };
125
159
  }
160
+ function nextPolledState(from, observed) {
161
+ return POLL_TRANSITIONS[observed].has(from) ? observed : from;
162
+ }
126
163
  function nowISO() {
127
164
  return (/* @__PURE__ */ new Date()).toISOString();
128
165
  }
@@ -140,11 +177,19 @@ function readClaims() {
140
177
  }
141
178
  }
142
179
  function writeClaims(claims) {
143
- mkdirSync2(TERMINALHIRE_DIR2, { recursive: true });
144
- const tmp = `${CLAIMS_FILE}.tmp`;
180
+ mkdirSync2(TERMINALHIRE_DIR2, { recursive: true, mode: 448 });
181
+ const tmp = `${CLAIMS_FILE}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
145
182
  const payload = { claims };
146
- writeFileSync2(tmp, JSON.stringify(payload, null, 2), "utf8");
147
- renameSync(tmp, CLAIMS_FILE);
183
+ try {
184
+ writeFileSync2(tmp, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384, flag: "wx" });
185
+ renameSync2(tmp, CLAIMS_FILE);
186
+ } catch (err) {
187
+ try {
188
+ rmSync2(tmp, { force: true });
189
+ } catch {
190
+ }
191
+ throw err;
192
+ }
148
193
  }
149
194
  function findClaim(id) {
150
195
  return readClaims().find((c) => c.id === id) ?? null;
@@ -155,57 +200,67 @@ function listClaims(opts = {}) {
155
200
  return claims.filter((c) => !TERMINAL_STATES.has(c.state));
156
201
  }
157
202
  function recordClaim(rec) {
158
- const claims = readClaims();
159
- if (claims.some((c) => c.id === rec.id)) {
160
- throw new Error(
161
- `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
162
- );
163
- }
164
- const ts = nowISO();
165
- const claim = {
166
- ...rec,
167
- // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
168
- // a caller written before `policy` existed, or a plain-JS caller that skips
169
- // it, still produces a valid record instead of `policy: undefined`.
170
- policy: rec.policy ?? null,
171
- state: "claimed",
172
- worktreePath: null,
173
- branch: null,
174
- prUrl: null,
175
- review: null,
176
- claimedAt: ts,
177
- updatedAt: ts
178
- };
179
- claims.push(claim);
180
- writeClaims(claims);
181
- return claim;
203
+ return withClaimsLock(() => {
204
+ const claims = readClaims();
205
+ if (claims.some((c) => c.id === rec.id)) {
206
+ throw new Error(
207
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
208
+ );
209
+ }
210
+ const ts = nowISO();
211
+ const claim = {
212
+ ...rec,
213
+ // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
214
+ // a caller written before `policy` existed, or a plain-JS caller that skips
215
+ // it, still produces a valid record instead of `policy: undefined`.
216
+ policy: rec.policy ?? null,
217
+ state: "claimed",
218
+ worktreePath: null,
219
+ branch: null,
220
+ prUrl: null,
221
+ review: null,
222
+ claimedAt: ts,
223
+ updatedAt: ts
224
+ };
225
+ claims.push(claim);
226
+ writeClaims(claims);
227
+ return claim;
228
+ });
182
229
  }
183
230
  function updateClaim(id, patch) {
184
- const claims = readClaims();
185
- const idx = claims.findIndex((c) => c.id === id);
186
- if (idx === -1) return null;
187
- claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
188
- writeClaims(claims);
189
- return claims[idx];
231
+ return withClaimsLock(() => {
232
+ const claims = readClaims();
233
+ const idx = claims.findIndex((c) => c.id === id);
234
+ if (idx === -1) return null;
235
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
236
+ writeClaims(claims);
237
+ return claims[idx];
238
+ });
190
239
  }
191
240
  function removeClaim(id) {
192
- const claims = readClaims();
193
- const next = claims.filter((c) => c.id !== id);
194
- if (next.length === claims.length) return false;
195
- writeClaims(next);
196
- return true;
241
+ return withClaimsLock(() => {
242
+ const claims = readClaims();
243
+ const next = claims.filter((c) => c.id !== id);
244
+ if (next.length === claims.length) return false;
245
+ writeClaims(next);
246
+ return true;
247
+ });
197
248
  }
198
249
  function acceptedPRRate(claims = readClaims()) {
199
250
  const total = claims.length;
200
251
  const merged = claims.filter((c) => c.state === "merged").length;
201
252
  return { merged, total, rate: total === 0 ? 0 : merged / total };
202
253
  }
203
- var TERMINALHIRE_DIR2, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
254
+ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS;
204
255
  var init_claims = __esm({
205
256
  "src/claims.ts"() {
206
257
  "use strict";
207
258
  TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
208
259
  CLAIMS_FILE = join2(TERMINALHIRE_DIR2, "claims.json");
260
+ LOCK_DIR = `${CLAIMS_FILE}.lock`;
261
+ LOCK_STALE_MS = Number(process.env.TERMINALHIRE_LOCK_STALE_MS) || 1e4;
262
+ LOCK_RETRY_MS = Number(process.env.TERMINALHIRE_LOCK_RETRY_MS) || 25;
263
+ LOCK_TIMEOUT_MS = Number(process.env.TERMINALHIRE_LOCK_TIMEOUT_MS) || 5e3;
209
264
  PUSHED_CLAIM_FIELDS = [
210
265
  "kind",
211
266
  "repoFullName",
@@ -216,12 +271,17 @@ var init_claims = __esm({
216
271
  "updatedAt"
217
272
  ];
218
273
  TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
274
+ POLL_TRANSITIONS = {
275
+ merged: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready", "submitted", "abandoned"]),
276
+ abandoned: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready", "submitted", "merged"]),
277
+ submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
278
+ };
219
279
  }
220
280
  });
221
281
 
222
282
  // bin/claim-push-bg.js
223
283
  import { createHash } from "crypto";
224
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, rmSync as rmSync2 } from "fs";
284
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, rmSync as rmSync3 } from "fs";
225
285
  import { join as join3 } from "path";
226
286
  import { homedir as homedir3 } from "os";
227
287
 
@@ -236,7 +296,8 @@ import {
236
296
  writeFileSync,
237
297
  mkdirSync,
238
298
  existsSync,
239
- rmSync
299
+ rmSync,
300
+ renameSync
240
301
  } from "fs";
241
302
  import { join } from "path";
242
303
  import { homedir } from "os";
@@ -246,17 +307,22 @@ var KEY_FILE = join(TERMINALHIRE_DIR, "key");
246
307
  var ALGO = "aes-256-gcm";
247
308
  var KEY_BYTES = 32;
248
309
  var IV_BYTES = 12;
310
+ function skipKeychain() {
311
+ return process.env.TERMINALHIRE_NO_KEYCHAIN !== void 0 || process.env.CI !== void 0 || process.env.VITEST !== void 0 || process.env.NODE_ENV === "test";
312
+ }
249
313
  async function loadKey() {
250
- try {
251
- const kt = await Promise.resolve().then(() => __toESM(require_keytar2(), 1));
252
- const stored = await kt.getPassword("terminalhire", "profile-key");
253
- if (stored) return Buffer.from(stored, "hex");
254
- const key2 = randomBytes(KEY_BYTES);
255
- await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
256
- return key2;
257
- } catch {
314
+ if (!skipKeychain()) {
315
+ try {
316
+ const kt = await Promise.resolve().then(() => __toESM(require_keytar2(), 1));
317
+ const stored = await kt.getPassword("terminalhire", "profile-key");
318
+ if (stored) return Buffer.from(stored, "hex");
319
+ const key2 = randomBytes(KEY_BYTES);
320
+ await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
321
+ return key2;
322
+ } catch {
323
+ }
258
324
  }
259
- mkdirSync(TERMINALHIRE_DIR, { recursive: true });
325
+ mkdirSync(TERMINALHIRE_DIR, { recursive: true, mode: 448 });
260
326
  if (existsSync(KEY_FILE)) {
261
327
  return Buffer.from(readFileSync(KEY_FILE, "utf8").trim(), "hex");
262
328
  }
@@ -307,7 +373,7 @@ async function readPushTokenEnc() {
307
373
  }
308
374
  function clearPushTokenEnc() {
309
375
  try {
310
- rmSync2(CLAIM_PUSH_TOKEN_FILE);
376
+ rmSync3(CLAIM_PUSH_TOKEN_FILE);
311
377
  } catch {
312
378
  }
313
379
  }
@@ -324,7 +390,7 @@ function writeAutoMarker(marker) {
324
390
  }
325
391
  function clearAutoMarker() {
326
392
  try {
327
- rmSync2(CLAIM_PUSH_AUTO_MARKER);
393
+ rmSync3(CLAIM_PUSH_AUTO_MARKER);
328
394
  } catch {
329
395
  }
330
396
  }