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.
- package/README.md +4 -4
- package/dist/bin/claim-push-bg.js +122 -56
- package/dist/bin/jpi-bounties.js +1014 -296
- package/dist/bin/jpi-chat-read.js +118 -50
- package/dist/bin/jpi-chat.js +271 -125
- package/dist/bin/jpi-claim.js +1280 -423
- package/dist/bin/jpi-contribute.js +646 -167
- package/dist/bin/jpi-devs.js +613 -236
- package/dist/bin/jpi-dispatch.js +2354 -762
- package/dist/bin/jpi-hub.js +1136 -337
- package/dist/bin/jpi-inbox.js +271 -125
- package/dist/bin/jpi-init.js +490 -168
- package/dist/bin/jpi-intro.js +252 -112
- package/dist/bin/jpi-jobs.js +631 -254
- package/dist/bin/jpi-learn.js +252 -112
- package/dist/bin/jpi-login.js +651 -258
- package/dist/bin/jpi-profile.js +252 -112
- package/dist/bin/jpi-project.js +461 -162
- package/dist/bin/jpi-protocol.js +451 -0
- package/dist/bin/jpi-refresh.js +809 -366
- package/dist/bin/jpi-repo.js +1858 -0
- package/dist/bin/jpi-save.js +256 -116
- package/dist/bin/jpi-spinner.js +68 -25
- package/dist/bin/jpi-sync.js +258 -118
- package/dist/bin/jpi-trajectory.js +261 -121
- package/dist/bin/peer-connect-prompt.js +2 -2
- package/dist/bin/spinner.js +67 -24
- package/dist/src/acceptance-score.js +16 -22
- package/dist/src/chat-client.js +85 -36
- package/dist/src/chat-keystore.js +85 -36
- package/dist/src/claims.js +100 -40
- package/dist/src/crypto-store.js +248 -0
- package/dist/src/github-auth.js +30 -14
- package/dist/src/intro.js +252 -112
- package/dist/src/profile.js +204 -90
- package/dist/src/protocol.js +380 -0
- package/dist/src/repo-experience.js +1067 -0
- package/dist/src/reputation/fetch.js +68 -25
- package/dist/src/signal.js +67 -24
- package/dist/src/trajectory.js +261 -121
- package/install.js +3 -3
- 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.
|
|
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,
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
rmSync3(CLAIM_PUSH_AUTO_MARKER);
|
|
328
394
|
} catch {
|
|
329
395
|
}
|
|
330
396
|
}
|