terminalhire 0.42.17 → 0.42.19
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 +7 -0
- package/dist/bin/claim-push-bg.js +40 -2
- package/dist/bin/founder-verdict-sync.js +1 -1
- package/dist/bin/jpi-bounties.js +292 -110
- package/dist/bin/jpi-chat-read.js +152 -76
- package/dist/bin/jpi-chat.js +194 -107
- package/dist/bin/jpi-claim.js +881 -150
- package/dist/bin/jpi-contribute.js +197 -104
- package/dist/bin/jpi-decline.js +580 -0
- package/dist/bin/jpi-dispatch.js +1504 -563
- package/dist/bin/jpi-hub.js +196 -79
- package/dist/bin/jpi-inbox.js +194 -107
- package/dist/bin/jpi-intro.js +185 -128
- package/dist/bin/jpi-jobs.js +30 -8
- package/dist/bin/jpi-link.js +119 -118
- package/dist/bin/jpi-mcp-chat.js +153 -76
- package/dist/bin/jpi-mcp.js +1050 -258
- package/dist/bin/jpi-refresh.js +49 -2
- package/dist/bin/jpi-repo.js +40 -2
- package/dist/bin/jpi-run.js +323 -43
- package/dist/bin/jpi-statusline.js +1 -0
- package/dist/bin/jpi-trajectory.js +177 -118
- package/dist/bin/jpi.js +127 -11
- package/dist/bin/pulse-prompt.js +128 -107
- package/dist/src/api-base.js +1 -0
- package/dist/src/chat-client.js +117 -53
- package/dist/src/claims.js +39 -1
- package/dist/src/intro.js +123 -65
- package/dist/src/link.js +110 -110
- package/dist/src/state-dir-pin.js +115 -0
- package/dist/src/trajectory.js +108 -50
- package/dist/src/web-session.js +72 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -310,6 +310,13 @@ node test/zero-egress.test.js
|
|
|
310
310
|
uses that one answer — so reads and writes can no longer disagree about which
|
|
311
311
|
environment you are on.
|
|
312
312
|
|
|
313
|
+
The credential store follows that answer. Pointed at `dev.terminalhire.com` by any
|
|
314
|
+
spelling (`thdev`, the variable, an MCP server entry), the CLI reads and writes
|
|
315
|
+
`~/.terminalhire-dev` unless `TERMINALHIRE_DIR` names a store (`src/state-dir-pin.ts`,
|
|
316
|
+
called first thing in `bin/jpi-dispatch.js`, before any subcommand loads). Production and loopback keep
|
|
317
|
+
`~/.terminalhire`. Before this, only `thdev` switched stores, and every other
|
|
318
|
+
spelling presented the production session to dev.
|
|
319
|
+
|
|
313
320
|
The override is an **allowlist**, not a free-form URL, because this same base decides
|
|
314
321
|
where your push token is presented. Only these are accepted:
|
|
315
322
|
|
|
@@ -91,10 +91,14 @@ var init_state_dir = __esm({
|
|
|
91
91
|
var claims_exports = {};
|
|
92
92
|
__export(claims_exports, {
|
|
93
93
|
CLAIM_STATES: () => CLAIM_STATES,
|
|
94
|
+
CLAIM_TURN_LABEL_WIDTH: () => CLAIM_TURN_LABEL_WIDTH,
|
|
94
95
|
PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
|
|
95
96
|
acceptedPRRate: () => acceptedPRRate,
|
|
97
|
+
claimTurn: () => claimTurn,
|
|
98
|
+
claimTurnLabel: () => claimTurnLabel,
|
|
96
99
|
countAwaitingFounderApproval: () => countAwaitingFounderApproval,
|
|
97
100
|
findClaim: () => findClaim,
|
|
101
|
+
formatAcceptedPRRate: () => formatAcceptedPRRate,
|
|
98
102
|
listClaims: () => listClaims,
|
|
99
103
|
nextPolledState: () => nextPolledState,
|
|
100
104
|
readClaims: () => readClaims,
|
|
@@ -149,6 +153,12 @@ function withClaimsLock(fn) {
|
|
|
149
153
|
rmSync2(LOCK_DIR, { recursive: true, force: true });
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
function claimTurn(state) {
|
|
157
|
+
return CLAIM_TURN_BY_STATE[state] ?? "you";
|
|
158
|
+
}
|
|
159
|
+
function claimTurnLabel(state) {
|
|
160
|
+
return CLAIM_TURN_LABEL[claimTurn(state)];
|
|
161
|
+
}
|
|
152
162
|
function toPushedClaim(claim) {
|
|
153
163
|
return {
|
|
154
164
|
kind: claim.kind,
|
|
@@ -303,9 +313,18 @@ function countAwaitingFounderApproval(claims = readClaims()) {
|
|
|
303
313
|
function acceptedPRRate(claims = readClaims()) {
|
|
304
314
|
const total = claims.length;
|
|
305
315
|
const merged = claims.filter((c) => c.state === "merged").length;
|
|
306
|
-
|
|
316
|
+
const decided = claims.filter((c) => DECIDED_STATES.has(c.state)).length;
|
|
317
|
+
return { merged, decided, inFlight: total - decided, total, rate: decided === 0 ? 0 : merged / decided };
|
|
318
|
+
}
|
|
319
|
+
function formatAcceptedPRRate(rate) {
|
|
320
|
+
const inFlight = rate.inFlight > 0 ? ` \xB7 ${rate.inFlight} in flight` : "";
|
|
321
|
+
if (rate.decided === 0) {
|
|
322
|
+
return `Accepted-PR rate: no claims decided yet${inFlight}`;
|
|
323
|
+
}
|
|
324
|
+
const pct = Math.round(rate.rate * 100);
|
|
325
|
+
return `Accepted-PR rate: ${rate.merged}/${rate.decided} decided claims merged (${pct}%)${inFlight}`;
|
|
307
326
|
}
|
|
308
|
-
var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS;
|
|
327
|
+
var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, CLAIM_TURN_BY_STATE, CLAIM_TURN_LABEL, CLAIM_TURN_LABEL_WIDTH, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS, DECIDED_STATES;
|
|
309
328
|
var init_claims = __esm({
|
|
310
329
|
"src/claims.ts"() {
|
|
311
330
|
"use strict";
|
|
@@ -332,6 +351,24 @@ var init_claims = __esm({
|
|
|
332
351
|
"abandoned"
|
|
333
352
|
// released, or PR closed unmerged
|
|
334
353
|
]);
|
|
354
|
+
CLAIM_TURN_BY_STATE = Object.freeze({
|
|
355
|
+
claimed: "you",
|
|
356
|
+
working: "you",
|
|
357
|
+
"in-review": "you",
|
|
358
|
+
ready: "you",
|
|
359
|
+
submitted: "poster",
|
|
360
|
+
merged: "merged",
|
|
361
|
+
abandoned: "closed"
|
|
362
|
+
});
|
|
363
|
+
CLAIM_TURN_LABEL = Object.freeze({
|
|
364
|
+
you: "waiting on you",
|
|
365
|
+
poster: "waiting on poster",
|
|
366
|
+
merged: "merged",
|
|
367
|
+
closed: "closed"
|
|
368
|
+
});
|
|
369
|
+
CLAIM_TURN_LABEL_WIDTH = Math.max(
|
|
370
|
+
...Object.values(CLAIM_TURN_LABEL).map((label) => label.length)
|
|
371
|
+
);
|
|
335
372
|
PUSHED_CLAIM_FIELDS = [
|
|
336
373
|
"kind",
|
|
337
374
|
"repoFullName",
|
|
@@ -363,6 +400,7 @@ var init_claims = __esm({
|
|
|
363
400
|
};
|
|
364
401
|
WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
|
|
365
402
|
CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
|
|
403
|
+
DECIDED_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
|
|
366
404
|
}
|
|
367
405
|
});
|
|
368
406
|
|
|
@@ -182,7 +182,7 @@ async function syncFounderVerdicts({
|
|
|
182
182
|
const applied = [];
|
|
183
183
|
for (const t of plan) {
|
|
184
184
|
try {
|
|
185
|
-
claimsModule.updateClaim(t.id, { state: t.to });
|
|
185
|
+
claimsModule.updateClaim(t.id, { state: t.to, posterVerdict: t.verdict });
|
|
186
186
|
applied.push(t);
|
|
187
187
|
const line = buildVerdictNotice(t);
|
|
188
188
|
if (line) log(line);
|
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -10550,6 +10550,187 @@ var init_state_dir = __esm({
|
|
|
10550
10550
|
}
|
|
10551
10551
|
});
|
|
10552
10552
|
|
|
10553
|
+
// src/api-base.ts
|
|
10554
|
+
var api_base_exports = {};
|
|
10555
|
+
__export(api_base_exports, {
|
|
10556
|
+
ApiBaseError: () => ApiBaseError,
|
|
10557
|
+
DEV_API_BASE: () => DEV_API_BASE,
|
|
10558
|
+
DEV_STATE_DIR_NAME: () => DEV_STATE_DIR_NAME,
|
|
10559
|
+
PROD_API_BASE: () => PROD_API_BASE,
|
|
10560
|
+
__resetDevMarkerLatchForTests: () => __resetDevMarkerLatchForTests,
|
|
10561
|
+
formatDevMarker: () => formatDevMarker,
|
|
10562
|
+
isLoopbackOrigin: () => isLoopbackOrigin,
|
|
10563
|
+
isNonProdApiBase: () => isNonProdApiBase,
|
|
10564
|
+
pinToDevApiBase: () => pinToDevApiBase,
|
|
10565
|
+
printDevMarkerIfNeeded: () => printDevMarkerIfNeeded,
|
|
10566
|
+
resolveApiBase: () => resolveApiBase,
|
|
10567
|
+
resolveOAuthBase: () => resolveOAuthBase,
|
|
10568
|
+
warnSharedCredentialsIfNonProd: () => warnSharedCredentialsIfNonProd
|
|
10569
|
+
});
|
|
10570
|
+
import { homedir as homedir2 } from "os";
|
|
10571
|
+
import { join as join3 } from "path";
|
|
10572
|
+
function sanitizeOverrideForError(raw) {
|
|
10573
|
+
try {
|
|
10574
|
+
const url = new URL(raw);
|
|
10575
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
10576
|
+
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
10577
|
+
}
|
|
10578
|
+
if (url.username !== "" || url.password !== "") {
|
|
10579
|
+
return `${url.protocol}//***@${url.host}`;
|
|
10580
|
+
}
|
|
10581
|
+
return url.origin;
|
|
10582
|
+
} catch {
|
|
10583
|
+
return "(unparseable override)";
|
|
10584
|
+
}
|
|
10585
|
+
}
|
|
10586
|
+
function isLoopbackOrigin(origin) {
|
|
10587
|
+
try {
|
|
10588
|
+
const host = new URL(origin).hostname;
|
|
10589
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
10590
|
+
} catch {
|
|
10591
|
+
return false;
|
|
10592
|
+
}
|
|
10593
|
+
}
|
|
10594
|
+
function localApiAllowed(env) {
|
|
10595
|
+
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
10596
|
+
}
|
|
10597
|
+
function pinToDevApiBase(env = process.env) {
|
|
10598
|
+
env["TERMINALHIRE_API_URL"] = DEV_API_BASE;
|
|
10599
|
+
env["TERMINALHIRE_DIR"] = env["TERMINALHIRE_DIR"] || join3(homedir2(), DEV_STATE_DIR_NAME);
|
|
10600
|
+
return DEV_API_BASE;
|
|
10601
|
+
}
|
|
10602
|
+
function resolveApiBase(env = process.env) {
|
|
10603
|
+
for (const key of ENV_KEYS) {
|
|
10604
|
+
const raw = env[key];
|
|
10605
|
+
if (typeof raw !== "string") continue;
|
|
10606
|
+
const trimmed = raw.trim();
|
|
10607
|
+
if (trimmed === "") continue;
|
|
10608
|
+
const normalized = normalizeOverride(trimmed);
|
|
10609
|
+
if (normalized === null) {
|
|
10610
|
+
throw new ApiBaseError(
|
|
10611
|
+
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
10612
|
+
);
|
|
10613
|
+
}
|
|
10614
|
+
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
10615
|
+
throw new ApiBaseError(
|
|
10616
|
+
`terminalhire: ${key}=${normalized} is a loopback origin. Set ${ALLOW_LOCAL_API_KEY}=1 to talk to a local web app on purpose. Refusing so stored credentials cannot be exfiltrated to localhost by a poisoned override.`
|
|
10617
|
+
);
|
|
10618
|
+
}
|
|
10619
|
+
return normalized;
|
|
10620
|
+
}
|
|
10621
|
+
return PROD_API_BASE;
|
|
10622
|
+
}
|
|
10623
|
+
function resolveOAuthBase(env = process.env) {
|
|
10624
|
+
const base = resolveApiBase(env);
|
|
10625
|
+
if (OAUTH_ALLOWED_ORIGINS.includes(base)) return base;
|
|
10626
|
+
if (env[ALLOW_LOCAL_OAUTH_KEY] === "1") return base;
|
|
10627
|
+
throw new ApiBaseError(
|
|
10628
|
+
`terminalhire: the API base is ${base}, which is not a trusted origin for a browser sign-in. Point the CLI at ${DEV_API_BASE} for an end-to-end login, or set ${ALLOW_LOCAL_OAUTH_KEY}=1 (with ${ALLOW_LOCAL_API_KEY}=1) if you are running the web app locally on purpose. Refusing to open production sign-in while the API is local.`
|
|
10629
|
+
);
|
|
10630
|
+
}
|
|
10631
|
+
function resolveApiBaseOrProd(env = process.env) {
|
|
10632
|
+
try {
|
|
10633
|
+
return resolveApiBase(env);
|
|
10634
|
+
} catch {
|
|
10635
|
+
return PROD_API_BASE;
|
|
10636
|
+
}
|
|
10637
|
+
}
|
|
10638
|
+
function normalizeOverride(raw) {
|
|
10639
|
+
let url;
|
|
10640
|
+
try {
|
|
10641
|
+
url = new URL(raw);
|
|
10642
|
+
} catch {
|
|
10643
|
+
return null;
|
|
10644
|
+
}
|
|
10645
|
+
if (url.username !== "" || url.password !== "") return null;
|
|
10646
|
+
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
10647
|
+
if (expectedProtocol === void 0) return null;
|
|
10648
|
+
if (url.protocol !== expectedProtocol) return null;
|
|
10649
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
10650
|
+
return null;
|
|
10651
|
+
}
|
|
10652
|
+
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
10653
|
+
if (rewrite !== void 0) return rewrite;
|
|
10654
|
+
return url.origin;
|
|
10655
|
+
}
|
|
10656
|
+
function isNonProdApiBase(base = resolveApiBaseOrProd()) {
|
|
10657
|
+
return base !== PROD_API_BASE;
|
|
10658
|
+
}
|
|
10659
|
+
function formatDevMarker(base = resolveApiBaseOrProd()) {
|
|
10660
|
+
if (!isNonProdApiBase(base)) return null;
|
|
10661
|
+
let host = base;
|
|
10662
|
+
try {
|
|
10663
|
+
host = new URL(base).host;
|
|
10664
|
+
} catch {
|
|
10665
|
+
}
|
|
10666
|
+
return `[dev \u2192 ${host}]`;
|
|
10667
|
+
}
|
|
10668
|
+
function printDevMarkerIfNeeded(stream = process.stderr) {
|
|
10669
|
+
if (markerPrinted) return;
|
|
10670
|
+
const marker = formatDevMarker();
|
|
10671
|
+
if (marker === null) return;
|
|
10672
|
+
markerPrinted = true;
|
|
10673
|
+
try {
|
|
10674
|
+
stream.write(`${marker}
|
|
10675
|
+
`);
|
|
10676
|
+
} catch {
|
|
10677
|
+
}
|
|
10678
|
+
}
|
|
10679
|
+
function __resetDevMarkerLatchForTests() {
|
|
10680
|
+
markerPrinted = false;
|
|
10681
|
+
}
|
|
10682
|
+
function warnSharedCredentialsIfNonProd(base, stream = process.stderr) {
|
|
10683
|
+
if (!isNonProdApiBase(base)) return;
|
|
10684
|
+
if (usingSeparateStateDir()) return;
|
|
10685
|
+
try {
|
|
10686
|
+
stream.write(
|
|
10687
|
+
"terminalhire: non-prod API base \u2014 using the same local session/push credentials as prod; do not mix environments casually.\n"
|
|
10688
|
+
);
|
|
10689
|
+
} catch {
|
|
10690
|
+
}
|
|
10691
|
+
}
|
|
10692
|
+
function usingSeparateStateDir(env = process.env) {
|
|
10693
|
+
const dir = env["TERMINALHIRE_DIR"];
|
|
10694
|
+
if (dir === void 0 || dir === "") return false;
|
|
10695
|
+
return dir.endsWith(DEV_STATE_DIR_NAME);
|
|
10696
|
+
}
|
|
10697
|
+
var PROD_API_BASE, DEV_API_BASE, DEV_STATE_DIR_NAME, ApiBaseError, ALLOWED_HOSTS, OAUTH_ALLOWED_ORIGINS, ALLOW_LOCAL_OAUTH_KEY, ALLOW_LOCAL_API_KEY, ALLOWED_DESCRIPTION, CANONICAL_REWRITES, ENV_KEYS, markerPrinted;
|
|
10698
|
+
var init_api_base = __esm({
|
|
10699
|
+
"src/api-base.ts"() {
|
|
10700
|
+
"use strict";
|
|
10701
|
+
PROD_API_BASE = "https://terminalhire.com";
|
|
10702
|
+
DEV_API_BASE = "https://dev.terminalhire.com";
|
|
10703
|
+
DEV_STATE_DIR_NAME = ".terminalhire-dev";
|
|
10704
|
+
ApiBaseError = class extends Error {
|
|
10705
|
+
constructor(message) {
|
|
10706
|
+
super(message);
|
|
10707
|
+
this.name = "ApiBaseError";
|
|
10708
|
+
}
|
|
10709
|
+
};
|
|
10710
|
+
ALLOWED_HOSTS = {
|
|
10711
|
+
"terminalhire.com": "https:",
|
|
10712
|
+
"www.terminalhire.com": "https:",
|
|
10713
|
+
"dev.terminalhire.com": "https:",
|
|
10714
|
+
localhost: "http:",
|
|
10715
|
+
"127.0.0.1": "http:"
|
|
10716
|
+
};
|
|
10717
|
+
OAUTH_ALLOWED_ORIGINS = [PROD_API_BASE, DEV_API_BASE];
|
|
10718
|
+
ALLOW_LOCAL_OAUTH_KEY = "TERMINALHIRE_ALLOW_LOCAL_OAUTH";
|
|
10719
|
+
ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
10720
|
+
ALLOWED_DESCRIPTION = [
|
|
10721
|
+
PROD_API_BASE,
|
|
10722
|
+
DEV_API_BASE,
|
|
10723
|
+
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
10724
|
+
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
10725
|
+
].join(", ");
|
|
10726
|
+
CANONICAL_REWRITES = {
|
|
10727
|
+
"www.terminalhire.com": PROD_API_BASE
|
|
10728
|
+
};
|
|
10729
|
+
ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
10730
|
+
markerPrinted = false;
|
|
10731
|
+
}
|
|
10732
|
+
});
|
|
10733
|
+
|
|
10553
10734
|
// bin/founder-pin.js
|
|
10554
10735
|
function isPinnedFounderBounty(j) {
|
|
10555
10736
|
return j?.bounty?.bountySource === "founder" && j?.bounty?.claimable === true;
|
|
@@ -11254,10 +11435,14 @@ var init_repo_experience = __esm({
|
|
|
11254
11435
|
var claims_exports = {};
|
|
11255
11436
|
__export(claims_exports, {
|
|
11256
11437
|
CLAIM_STATES: () => CLAIM_STATES,
|
|
11438
|
+
CLAIM_TURN_LABEL_WIDTH: () => CLAIM_TURN_LABEL_WIDTH,
|
|
11257
11439
|
PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
|
|
11258
11440
|
acceptedPRRate: () => acceptedPRRate,
|
|
11441
|
+
claimTurn: () => claimTurn,
|
|
11442
|
+
claimTurnLabel: () => claimTurnLabel,
|
|
11259
11443
|
countAwaitingFounderApproval: () => countAwaitingFounderApproval,
|
|
11260
11444
|
findClaim: () => findClaim,
|
|
11445
|
+
formatAcceptedPRRate: () => formatAcceptedPRRate,
|
|
11261
11446
|
listClaims: () => listClaims,
|
|
11262
11447
|
nextPolledState: () => nextPolledState,
|
|
11263
11448
|
readClaims: () => readClaims,
|
|
@@ -11312,6 +11497,12 @@ function withClaimsLock(fn) {
|
|
|
11312
11497
|
rmSync2(LOCK_DIR, { recursive: true, force: true });
|
|
11313
11498
|
}
|
|
11314
11499
|
}
|
|
11500
|
+
function claimTurn(state) {
|
|
11501
|
+
return CLAIM_TURN_BY_STATE[state] ?? "you";
|
|
11502
|
+
}
|
|
11503
|
+
function claimTurnLabel(state) {
|
|
11504
|
+
return CLAIM_TURN_LABEL[claimTurn(state)];
|
|
11505
|
+
}
|
|
11315
11506
|
function toPushedClaim(claim) {
|
|
11316
11507
|
return {
|
|
11317
11508
|
kind: claim.kind,
|
|
@@ -11466,9 +11657,18 @@ function countAwaitingFounderApproval(claims = readClaims()) {
|
|
|
11466
11657
|
function acceptedPRRate(claims = readClaims()) {
|
|
11467
11658
|
const total = claims.length;
|
|
11468
11659
|
const merged = claims.filter((c) => c.state === "merged").length;
|
|
11469
|
-
|
|
11660
|
+
const decided = claims.filter((c) => DECIDED_STATES.has(c.state)).length;
|
|
11661
|
+
return { merged, decided, inFlight: total - decided, total, rate: decided === 0 ? 0 : merged / decided };
|
|
11662
|
+
}
|
|
11663
|
+
function formatAcceptedPRRate(rate) {
|
|
11664
|
+
const inFlight = rate.inFlight > 0 ? ` \xB7 ${rate.inFlight} in flight` : "";
|
|
11665
|
+
if (rate.decided === 0) {
|
|
11666
|
+
return `Accepted-PR rate: no claims decided yet${inFlight}`;
|
|
11667
|
+
}
|
|
11668
|
+
const pct = Math.round(rate.rate * 100);
|
|
11669
|
+
return `Accepted-PR rate: ${rate.merged}/${rate.decided} decided claims merged (${pct}%)${inFlight}`;
|
|
11470
11670
|
}
|
|
11471
|
-
var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
|
|
11671
|
+
var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, CLAIM_TURN_BY_STATE, CLAIM_TURN_LABEL, CLAIM_TURN_LABEL_WIDTH, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2, DECIDED_STATES;
|
|
11472
11672
|
var init_claims = __esm({
|
|
11473
11673
|
"src/claims.ts"() {
|
|
11474
11674
|
"use strict";
|
|
@@ -11495,6 +11695,24 @@ var init_claims = __esm({
|
|
|
11495
11695
|
"abandoned"
|
|
11496
11696
|
// released, or PR closed unmerged
|
|
11497
11697
|
]);
|
|
11698
|
+
CLAIM_TURN_BY_STATE = Object.freeze({
|
|
11699
|
+
claimed: "you",
|
|
11700
|
+
working: "you",
|
|
11701
|
+
"in-review": "you",
|
|
11702
|
+
ready: "you",
|
|
11703
|
+
submitted: "poster",
|
|
11704
|
+
merged: "merged",
|
|
11705
|
+
abandoned: "closed"
|
|
11706
|
+
});
|
|
11707
|
+
CLAIM_TURN_LABEL = Object.freeze({
|
|
11708
|
+
you: "waiting on you",
|
|
11709
|
+
poster: "waiting on poster",
|
|
11710
|
+
merged: "merged",
|
|
11711
|
+
closed: "closed"
|
|
11712
|
+
});
|
|
11713
|
+
CLAIM_TURN_LABEL_WIDTH = Math.max(
|
|
11714
|
+
...Object.values(CLAIM_TURN_LABEL).map((label) => label.length)
|
|
11715
|
+
);
|
|
11498
11716
|
PUSHED_CLAIM_FIELDS = [
|
|
11499
11717
|
"kind",
|
|
11500
11718
|
"repoFullName",
|
|
@@ -11526,6 +11744,7 @@ var init_claims = __esm({
|
|
|
11526
11744
|
};
|
|
11527
11745
|
WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
|
|
11528
11746
|
CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
|
|
11747
|
+
DECIDED_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
|
|
11529
11748
|
}
|
|
11530
11749
|
});
|
|
11531
11750
|
|
|
@@ -11747,12 +11966,16 @@ var init_founder_paid_badge = __esm({
|
|
|
11747
11966
|
var web_session_exports = {};
|
|
11748
11967
|
__export(web_session_exports, {
|
|
11749
11968
|
clearWebSessionFile: () => clearWebSessionFile,
|
|
11969
|
+
hostLabel: () => hostLabel,
|
|
11970
|
+
missingSessionLines: () => missingSessionLines,
|
|
11750
11971
|
readWebSessionCookie: () => readWebSessionCookie,
|
|
11751
11972
|
readWebSessionFile: () => readWebSessionFile,
|
|
11752
11973
|
readWebSessionRecord: () => readWebSessionRecord,
|
|
11974
|
+
refusedSessionLines: () => refusedSessionLines,
|
|
11753
11975
|
webSessionCookieForHost: () => webSessionCookieForHost,
|
|
11754
11976
|
webSessionFilePath: () => webSessionFilePath,
|
|
11755
11977
|
webSessionForHost: () => webSessionForHost,
|
|
11978
|
+
withSessionDeps: () => withSessionDeps,
|
|
11756
11979
|
writeWebSessionFile: () => writeWebSessionFile
|
|
11757
11980
|
});
|
|
11758
11981
|
import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
@@ -11792,6 +12015,7 @@ function readWebSessionFile() {
|
|
|
11792
12015
|
return readWebSessionRecord()?.token ?? null;
|
|
11793
12016
|
}
|
|
11794
12017
|
function webSessionForHost(apiBase) {
|
|
12018
|
+
assertBase(apiBase, "webSessionForHost");
|
|
11795
12019
|
const record = readWebSessionRecord();
|
|
11796
12020
|
if (!record) return { cookie: null, mismatch: null };
|
|
11797
12021
|
if (record.host !== null && record.host !== apiBase) {
|
|
@@ -11805,11 +12029,50 @@ function webSessionCookieForHost(apiBase) {
|
|
|
11805
12029
|
const env = process.env["TERMINALHIRE_WEB_SESSION"];
|
|
11806
12030
|
return { cookie: typeof env === "string" && env.length > 0 ? env : null, mismatch: null };
|
|
11807
12031
|
}
|
|
11808
|
-
function
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
12032
|
+
function assertBase(apiBase, fn) {
|
|
12033
|
+
if (typeof apiBase !== "string" || apiBase.length === 0) {
|
|
12034
|
+
throw new TypeError(
|
|
12035
|
+
`${fn}(apiBase) requires the destination base as a non-empty string; received ${apiBase === "" ? "''" : String(apiBase)}. This is a wiring bug in the calling command, not a developer misconfiguration: pass the same resolveApiBase() value the request is sent to, so the session's host affinity can be checked (TERM-991).`
|
|
12036
|
+
);
|
|
12037
|
+
}
|
|
12038
|
+
}
|
|
12039
|
+
function hostLabel(base) {
|
|
12040
|
+
return String(base ?? "").replace(/^https?:\/\//, "");
|
|
12041
|
+
}
|
|
12042
|
+
function missingSessionLines(mismatch) {
|
|
12043
|
+
if (!mismatch) {
|
|
12044
|
+
return [
|
|
12045
|
+
"No linked web session found on this machine.",
|
|
12046
|
+
"Run `terminalhire link` to connect this terminal to your account, then re-run."
|
|
12047
|
+
];
|
|
12048
|
+
}
|
|
12049
|
+
const linked = hostLabel(mismatch.linkedHost);
|
|
12050
|
+
const current = hostLabel(mismatch.currentHost);
|
|
12051
|
+
const reach = mismatch.linkedHost === PROD_API_BASE ? `unset TERMINALHIRE_API_URL to reach ${linked}` : isLoopbackOrigin(mismatch.linkedHost) ? `set TERMINALHIRE_ALLOW_LOCAL_API=1 TERMINALHIRE_API_URL=${mismatch.linkedHost} to reach ${linked}` : `set TERMINALHIRE_API_URL=${mismatch.linkedHost} to reach ${linked}`;
|
|
12052
|
+
const link = isLoopbackOrigin(mismatch.currentHost) ? "`TERMINALHIRE_ALLOW_LOCAL_OAUTH=1 terminalhire link`" : "`terminalhire link`";
|
|
12053
|
+
return [
|
|
12054
|
+
`This terminal is on ${current}, but your linked session belongs to ${linked}. Nothing was sent.`,
|
|
12055
|
+
`Either ${reach}, or run ${link} to link this terminal to ${current} instead.`
|
|
12056
|
+
];
|
|
12057
|
+
}
|
|
12058
|
+
function withSessionDeps(defaults, overrides) {
|
|
12059
|
+
const merged = { ...defaults, ...overrides };
|
|
12060
|
+
if (overrides?.sessionCookie && !overrides.sessionMismatch) merged.sessionMismatch = () => null;
|
|
12061
|
+
return merged;
|
|
12062
|
+
}
|
|
12063
|
+
function refusedSessionLines(apiBase) {
|
|
12064
|
+
return [
|
|
12065
|
+
`${hostLabel(apiBase)} refused your linked session.`,
|
|
12066
|
+
"Run `terminalhire link` to link this terminal again, then re-run."
|
|
12067
|
+
];
|
|
12068
|
+
}
|
|
12069
|
+
function readWebSessionCookie(apiBase) {
|
|
12070
|
+
if (typeof apiBase !== "string" || apiBase.length === 0) {
|
|
12071
|
+
throw new TypeError(
|
|
12072
|
+
`readWebSessionCookie(apiBase) requires the destination base as a non-empty string; received ${apiBase === "" ? "''" : String(apiBase)}. This is a wiring bug in the calling command, not a developer misconfiguration: pass the same resolveApiBase() value the request is sent to, so the session's host affinity can be checked (TERM-991).`
|
|
12073
|
+
);
|
|
12074
|
+
}
|
|
12075
|
+
return webSessionCookieForHost(apiBase).cookie;
|
|
11813
12076
|
}
|
|
11814
12077
|
function writeWebSessionFile(token, host) {
|
|
11815
12078
|
ensureStateDirForSecret(terminalhireDir());
|
|
@@ -11830,6 +12093,7 @@ function clearWebSessionFile() {
|
|
|
11830
12093
|
var init_web_session = __esm({
|
|
11831
12094
|
"src/web-session.ts"() {
|
|
11832
12095
|
"use strict";
|
|
12096
|
+
init_api_base();
|
|
11833
12097
|
init_state_dir();
|
|
11834
12098
|
}
|
|
11835
12099
|
});
|
|
@@ -11938,15 +12202,27 @@ async function sendDecline(bountyId, reason) {
|
|
|
11938
12202
|
console.error("Not one of the answers \u2014 nothing sent.");
|
|
11939
12203
|
return false;
|
|
11940
12204
|
}
|
|
11941
|
-
|
|
11942
|
-
|
|
12205
|
+
let apiUrl;
|
|
12206
|
+
try {
|
|
12207
|
+
const { resolveApiBase: resolveApiBase2 } = await Promise.resolve().then(() => (init_api_base(), api_base_exports));
|
|
12208
|
+
apiUrl = resolveApiBase2();
|
|
12209
|
+
} catch (err) {
|
|
12210
|
+
console.log(`
|
|
12211
|
+
${err instanceof Error ? err.message : String(err)}`);
|
|
12212
|
+
return false;
|
|
12213
|
+
}
|
|
12214
|
+
const { missingSessionLines: missingSessionLines2, webSessionCookieForHost: webSessionCookieForHost2 } = await Promise.resolve().then(() => (init_web_session(), web_session_exports));
|
|
12215
|
+
const { cookie, mismatch } = webSessionCookieForHost2(apiUrl);
|
|
11943
12216
|
if (!cookie) {
|
|
11944
|
-
console.log(
|
|
12217
|
+
if (mismatch) console.log(`
|
|
12218
|
+
${missingSessionLines2(mismatch).join("\n")}`);
|
|
12219
|
+
else
|
|
12220
|
+
console.log("\nRun `terminalhire link` first \u2014 a poster needs to know the answer is real.");
|
|
11945
12221
|
return false;
|
|
11946
12222
|
}
|
|
11947
12223
|
if (!await ensureDeclineConsent(bountyId, reason)) return false;
|
|
11948
12224
|
try {
|
|
11949
|
-
const res = await fetch(`${
|
|
12225
|
+
const res = await fetch(`${apiUrl}/api/posting/decline`, {
|
|
11950
12226
|
method: "POST",
|
|
11951
12227
|
headers: {
|
|
11952
12228
|
"content-type": "application/json",
|
|
@@ -11975,11 +12251,10 @@ async function runDeclinePrompt(bountyId, { ask = prompt } = {}) {
|
|
|
11975
12251
|
if (!reason) return false;
|
|
11976
12252
|
return sendDecline(bountyId, reason);
|
|
11977
12253
|
}
|
|
11978
|
-
var
|
|
12254
|
+
var GH_SESSION_COOKIE, DECLINE_CHOICES;
|
|
11979
12255
|
var init_jpi_decline = __esm({
|
|
11980
12256
|
"bin/jpi-decline.js"() {
|
|
11981
12257
|
"use strict";
|
|
11982
|
-
API_URL = process.env["TERMINALHIRE_API_URL"] || "https://terminalhire.com";
|
|
11983
12258
|
GH_SESSION_COOKIE = "__jpi_gh_session";
|
|
11984
12259
|
DECLINE_CHOICES = [
|
|
11985
12260
|
{ key: "1", reason: "price_low", label: "the price is too low for the work" },
|
|
@@ -12211,106 +12486,13 @@ function linkTitle(title, url) {
|
|
|
12211
12486
|
return href ? `${safeTitle} (${href})` : safeTitle;
|
|
12212
12487
|
}
|
|
12213
12488
|
|
|
12214
|
-
// src/api-base.ts
|
|
12215
|
-
import { homedir as homedir2 } from "os";
|
|
12216
|
-
import { join as join3 } from "path";
|
|
12217
|
-
var PROD_API_BASE = "https://terminalhire.com";
|
|
12218
|
-
var DEV_API_BASE = "https://dev.terminalhire.com";
|
|
12219
|
-
var ApiBaseError = class extends Error {
|
|
12220
|
-
constructor(message) {
|
|
12221
|
-
super(message);
|
|
12222
|
-
this.name = "ApiBaseError";
|
|
12223
|
-
}
|
|
12224
|
-
};
|
|
12225
|
-
var ALLOWED_HOSTS = {
|
|
12226
|
-
"terminalhire.com": "https:",
|
|
12227
|
-
"www.terminalhire.com": "https:",
|
|
12228
|
-
"dev.terminalhire.com": "https:",
|
|
12229
|
-
localhost: "http:",
|
|
12230
|
-
"127.0.0.1": "http:"
|
|
12231
|
-
};
|
|
12232
|
-
var ALLOW_LOCAL_API_KEY = "TERMINALHIRE_ALLOW_LOCAL_API";
|
|
12233
|
-
var ALLOWED_DESCRIPTION = [
|
|
12234
|
-
PROD_API_BASE,
|
|
12235
|
-
DEV_API_BASE,
|
|
12236
|
-
`http://localhost:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`,
|
|
12237
|
-
`http://127.0.0.1:<port> (requires ${ALLOW_LOCAL_API_KEY}=1)`
|
|
12238
|
-
].join(", ");
|
|
12239
|
-
var CANONICAL_REWRITES = {
|
|
12240
|
-
"www.terminalhire.com": PROD_API_BASE
|
|
12241
|
-
};
|
|
12242
|
-
var ENV_KEYS = ["TERMINALHIRE_API_URL", "JPI_API_URL"];
|
|
12243
|
-
function sanitizeOverrideForError(raw) {
|
|
12244
|
-
try {
|
|
12245
|
-
const url = new URL(raw);
|
|
12246
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
12247
|
-
return `(disallowed scheme: ${url.protocol.slice(0, -1)})`;
|
|
12248
|
-
}
|
|
12249
|
-
if (url.username !== "" || url.password !== "") {
|
|
12250
|
-
return `${url.protocol}//***@${url.host}`;
|
|
12251
|
-
}
|
|
12252
|
-
return url.origin;
|
|
12253
|
-
} catch {
|
|
12254
|
-
return "(unparseable override)";
|
|
12255
|
-
}
|
|
12256
|
-
}
|
|
12257
|
-
function isLoopbackOrigin(origin) {
|
|
12258
|
-
try {
|
|
12259
|
-
const host = new URL(origin).hostname;
|
|
12260
|
-
return host === "localhost" || host === "127.0.0.1";
|
|
12261
|
-
} catch {
|
|
12262
|
-
return false;
|
|
12263
|
-
}
|
|
12264
|
-
}
|
|
12265
|
-
function localApiAllowed(env) {
|
|
12266
|
-
return env[ALLOW_LOCAL_API_KEY] === "1";
|
|
12267
|
-
}
|
|
12268
|
-
function resolveApiBase(env = process.env) {
|
|
12269
|
-
for (const key of ENV_KEYS) {
|
|
12270
|
-
const raw = env[key];
|
|
12271
|
-
if (typeof raw !== "string") continue;
|
|
12272
|
-
const trimmed = raw.trim();
|
|
12273
|
-
if (trimmed === "") continue;
|
|
12274
|
-
const normalized = normalizeOverride(trimmed);
|
|
12275
|
-
if (normalized === null) {
|
|
12276
|
-
throw new ApiBaseError(
|
|
12277
|
-
`terminalhire: ${key}=${sanitizeOverrideForError(trimmed)} is not an allowed API host (allowed: ${ALLOWED_DESCRIPTION}). Refusing to continue so we do not silently hit production.`
|
|
12278
|
-
);
|
|
12279
|
-
}
|
|
12280
|
-
if (isLoopbackOrigin(normalized) && !localApiAllowed(env)) {
|
|
12281
|
-
throw new ApiBaseError(
|
|
12282
|
-
`terminalhire: ${key}=${normalized} is a loopback origin. Set ${ALLOW_LOCAL_API_KEY}=1 to talk to a local web app on purpose. Refusing so stored credentials cannot be exfiltrated to localhost by a poisoned override.`
|
|
12283
|
-
);
|
|
12284
|
-
}
|
|
12285
|
-
return normalized;
|
|
12286
|
-
}
|
|
12287
|
-
return PROD_API_BASE;
|
|
12288
|
-
}
|
|
12289
|
-
function normalizeOverride(raw) {
|
|
12290
|
-
let url;
|
|
12291
|
-
try {
|
|
12292
|
-
url = new URL(raw);
|
|
12293
|
-
} catch {
|
|
12294
|
-
return null;
|
|
12295
|
-
}
|
|
12296
|
-
if (url.username !== "" || url.password !== "") return null;
|
|
12297
|
-
const expectedProtocol = ALLOWED_HOSTS[url.hostname];
|
|
12298
|
-
if (expectedProtocol === void 0) return null;
|
|
12299
|
-
if (url.protocol !== expectedProtocol) return null;
|
|
12300
|
-
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.port !== "") {
|
|
12301
|
-
return null;
|
|
12302
|
-
}
|
|
12303
|
-
const rewrite = CANONICAL_REWRITES[url.hostname];
|
|
12304
|
-
if (rewrite !== void 0) return rewrite;
|
|
12305
|
-
return url.origin;
|
|
12306
|
-
}
|
|
12307
|
-
|
|
12308
12489
|
// bin/jpi-bounties.js
|
|
12490
|
+
init_api_base();
|
|
12309
12491
|
init_founder_pin();
|
|
12310
12492
|
var TERMINALHIRE_DIR7 = process.env.TERMINALHIRE_DIR || join13(homedir10(), ".terminalhire");
|
|
12311
12493
|
var INDEX_CACHE_FILE2 = join13(TERMINALHIRE_DIR7, "index-cache.json");
|
|
12312
12494
|
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
12313
|
-
var
|
|
12495
|
+
var API_URL = resolveApiBase();
|
|
12314
12496
|
var RANK_MODE = process.env["TERMINALHIRE_BOUNTY_RANK"] ?? "winnability";
|
|
12315
12497
|
var CONTINUITY_RANK_DISABLED = process.env["TERMINALHIRE_NO_CONTINUITY_RANK"] === "1";
|
|
12316
12498
|
var DEFAULT_LIMIT = 15;
|
|
@@ -12331,7 +12513,7 @@ function readIndexCache() {
|
|
|
12331
12513
|
}
|
|
12332
12514
|
async function fetchIndex(quiet = false) {
|
|
12333
12515
|
const { index, reason, source, cachedAt } = await revalidateIndex({
|
|
12334
|
-
apiUrl:
|
|
12516
|
+
apiUrl: API_URL,
|
|
12335
12517
|
fetchImpl: (url, init) => fetch(url, init),
|
|
12336
12518
|
readEntry: readCacheEntry,
|
|
12337
12519
|
writeEntry: updateIndexCache
|
|
@@ -12482,7 +12664,7 @@ function classifyEmptyStatus({ preFilterCount, postFilterCount, priced }) {
|
|
|
12482
12664
|
return "empty";
|
|
12483
12665
|
}
|
|
12484
12666
|
async function getBounties({ quiet = false, offline = false, priced = PRICED_ONLY } = {}) {
|
|
12485
|
-
if (!quiet) console.log(`Fetching bounty index from ${
|
|
12667
|
+
if (!quiet) console.log(`Fetching bounty index from ${API_URL}/api/index...`);
|
|
12486
12668
|
const index = offline ? readIndexCache() : await fetchIndex(quiet);
|
|
12487
12669
|
if (offline && !index) return { status: "no-cache" };
|
|
12488
12670
|
let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
|