terminalhire 0.36.1 → 0.37.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 (62) hide show
  1. package/dist/bin/cache-store.js +52 -2
  2. package/dist/bin/claim-push-bg.js +95 -23
  3. package/dist/bin/directory.js +151 -0
  4. package/dist/bin/job-status-store.js +856 -0
  5. package/dist/bin/jpi-bounties.js +2243 -1036
  6. package/dist/bin/jpi-chat-read.js +298 -95
  7. package/dist/bin/jpi-chat.js +311 -117
  8. package/dist/bin/jpi-claim.js +2716 -1153
  9. package/dist/bin/jpi-config.js +52 -2
  10. package/dist/bin/jpi-contribute.js +1549 -110
  11. package/dist/bin/jpi-devs.js +2207 -1027
  12. package/dist/bin/jpi-dispatch.js +8138 -11833
  13. package/dist/bin/jpi-hub.js +2304 -1071
  14. package/dist/bin/jpi-inbox.js +311 -117
  15. package/dist/bin/jpi-init.js +2103 -919
  16. package/dist/bin/jpi-intro.js +237 -78
  17. package/dist/bin/jpi-jobs.js +2940 -1073
  18. package/dist/bin/jpi-learn.js +235 -70
  19. package/dist/bin/jpi-link.js +63 -13
  20. package/dist/bin/jpi-login.js +2226 -1047
  21. package/dist/bin/jpi-mcp-chat.js +430 -5949
  22. package/dist/bin/jpi-mcp.js +429 -5958
  23. package/dist/bin/jpi-profile.js +235 -70
  24. package/dist/bin/jpi-project.js +2103 -919
  25. package/dist/bin/jpi-protocol.js +128 -17
  26. package/dist/bin/jpi-refresh.js +2338 -1107
  27. package/dist/bin/jpi-repo.js +268 -76
  28. package/dist/bin/jpi-save.js +235 -70
  29. package/dist/bin/jpi-spinner.js +208 -76
  30. package/dist/bin/jpi-sync.js +251 -77
  31. package/dist/bin/jpi-trajectory.js +270 -94
  32. package/dist/bin/jpi-update.js +128 -16
  33. package/dist/bin/jpi.js +56 -5
  34. package/dist/bin/peer-connect-prompt.js +52 -2
  35. package/dist/bin/pulse-prompt.js +53 -10
  36. package/dist/bin/spinner-io.js +181 -0
  37. package/dist/bin/spinner-render.js +1260 -0
  38. package/dist/bin/spinner-seen.js +154 -0
  39. package/dist/bin/spinner.js +197 -61
  40. package/dist/bin/version-nudge.js +57 -3
  41. package/dist/src/chat-client.js +165 -62
  42. package/dist/src/chat-keystore.js +149 -49
  43. package/dist/src/claims.js +83 -7
  44. package/dist/src/config.js +52 -2
  45. package/dist/src/crypto-store.js +62 -22
  46. package/dist/src/github-auth.js +67 -19
  47. package/dist/src/intro.js +237 -78
  48. package/dist/src/link.js +54 -11
  49. package/dist/src/policy-acks.js +112 -0
  50. package/dist/src/profile.js +156 -55
  51. package/dist/src/protocol.js +123 -17
  52. package/dist/src/repo-experience.js +156 -55
  53. package/dist/src/repo-policy.js +60 -10
  54. package/dist/src/reputation/audit.js +0 -1
  55. package/dist/src/reputation/fetch.js +201 -34
  56. package/dist/src/signal.js +94 -33
  57. package/dist/src/state-dir.js +54 -0
  58. package/dist/src/trajectory.js +270 -94
  59. package/dist/src/web-session.js +52 -9
  60. package/install.js +169 -27
  61. package/package.json +1 -1
  62. package/statusline-install.js +153 -20
@@ -1,7 +1,57 @@
1
1
  // bin/cache-store.js
2
- import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
2
+ import { readFileSync, writeFileSync, renameSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
+
6
+ // src/state-dir.ts
7
+ import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
8
+ var STATE_DIR_MODE = 448;
9
+ var STATE_DIR_OK = "ok";
10
+ var STATE_DIR_SYMLINK = "symlink";
11
+ var STATE_DIR_UNVERIFIED = "unverified";
12
+ var warnedDirs = /* @__PURE__ */ new Set();
13
+ function warnStateDirOnce(dir, message) {
14
+ if (warnedDirs.has(dir)) return;
15
+ warnedDirs.add(dir);
16
+ try {
17
+ process.stderr.write(message);
18
+ } catch {
19
+ }
20
+ }
21
+ function ensureStateDir(dir) {
22
+ mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
23
+ const noFollow = constants.O_NOFOLLOW ?? 0;
24
+ let fd;
25
+ try {
26
+ fd = openSync(dir, constants.O_RDONLY | noFollow);
27
+ } catch (err) {
28
+ if (err?.code === "ELOOP") {
29
+ warnStateDirOnce(
30
+ dir,
31
+ `terminalhire: ${dir} is a symlink \u2014 leaving its permissions alone; the 0700 guarantee on the state directory is NOT enforced.
32
+ `
33
+ );
34
+ return STATE_DIR_SYMLINK;
35
+ }
36
+ return STATE_DIR_UNVERIFIED;
37
+ }
38
+ try {
39
+ const currentMode = fstatSync(fd).mode & 511;
40
+ if ((currentMode & ~STATE_DIR_MODE) !== 0) {
41
+ fchmodSync(fd, currentMode & STATE_DIR_MODE);
42
+ }
43
+ return STATE_DIR_OK;
44
+ } catch {
45
+ return STATE_DIR_UNVERIFIED;
46
+ } finally {
47
+ try {
48
+ closeSync(fd);
49
+ } catch {
50
+ }
51
+ }
52
+ }
53
+
54
+ // bin/cache-store.js
5
55
  var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
6
56
  var INDEX_CACHE_FILE = join(TERMINALHIRE_DIR, "index-cache.json");
7
57
  var SCHEMA_VERSION = 1;
@@ -14,7 +64,7 @@ function readCacheEntry() {
14
64
  }
15
65
  }
16
66
  function updateIndexCache(patch) {
17
- mkdirSync(TERMINALHIRE_DIR, { recursive: true });
67
+ ensureStateDir(TERMINALHIRE_DIR);
18
68
  const existing = readCacheEntry() ?? {};
19
69
  const entry = {
20
70
  ...existing,
@@ -8,6 +8,60 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/state-dir.ts
12
+ import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
13
+ function warnStateDirOnce(dir, message) {
14
+ if (warnedDirs.has(dir)) return;
15
+ warnedDirs.add(dir);
16
+ try {
17
+ process.stderr.write(message);
18
+ } catch {
19
+ }
20
+ }
21
+ function ensureStateDir(dir) {
22
+ mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
23
+ const noFollow = constants.O_NOFOLLOW ?? 0;
24
+ let fd;
25
+ try {
26
+ fd = openSync(dir, constants.O_RDONLY | noFollow);
27
+ } catch (err) {
28
+ if (err?.code === "ELOOP") {
29
+ warnStateDirOnce(
30
+ dir,
31
+ `terminalhire: ${dir} is a symlink \u2014 leaving its permissions alone; the 0700 guarantee on the state directory is NOT enforced.
32
+ `
33
+ );
34
+ return STATE_DIR_SYMLINK;
35
+ }
36
+ return STATE_DIR_UNVERIFIED;
37
+ }
38
+ try {
39
+ const currentMode = fstatSync(fd).mode & 511;
40
+ if ((currentMode & ~STATE_DIR_MODE) !== 0) {
41
+ fchmodSync(fd, currentMode & STATE_DIR_MODE);
42
+ }
43
+ return STATE_DIR_OK;
44
+ } catch {
45
+ return STATE_DIR_UNVERIFIED;
46
+ } finally {
47
+ try {
48
+ closeSync(fd);
49
+ } catch {
50
+ }
51
+ }
52
+ }
53
+ var STATE_DIR_MODE, STATE_DIR_OK, STATE_DIR_SYMLINK, STATE_DIR_UNVERIFIED, warnedDirs;
54
+ var init_state_dir = __esm({
55
+ "src/state-dir.ts"() {
56
+ "use strict";
57
+ STATE_DIR_MODE = 448;
58
+ STATE_DIR_OK = "ok";
59
+ STATE_DIR_SYMLINK = "symlink";
60
+ STATE_DIR_UNVERIFIED = "unverified";
61
+ warnedDirs = /* @__PURE__ */ new Set();
62
+ }
63
+ });
64
+
11
65
  // src/claims.ts
12
66
  var claims_exports = {};
13
67
  __export(claims_exports, {
@@ -22,7 +76,15 @@ __export(claims_exports, {
22
76
  toPushedClaim: () => toPushedClaim,
23
77
  updateClaim: () => updateClaim
24
78
  });
25
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, renameSync as renameSync2, existsSync as existsSync2, rmSync as rmSync2, statSync } from "fs";
79
+ import {
80
+ readFileSync as readFileSync2,
81
+ writeFileSync as writeFileSync2,
82
+ mkdirSync as mkdirSync2,
83
+ renameSync as renameSync2,
84
+ existsSync as existsSync2,
85
+ rmSync as rmSync2,
86
+ statSync
87
+ } from "fs";
26
88
  import { randomBytes as randomBytes2 } from "crypto";
27
89
  import { join as join2 } from "path";
28
90
  import { homedir as homedir2 } from "os";
@@ -30,7 +92,7 @@ function sleepSync(ms) {
30
92
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
31
93
  }
32
94
  function withClaimsLock(fn) {
33
- mkdirSync2(TERMINALHIRE_DIR2, { recursive: true, mode: 448 });
95
+ ensureStateDir(TERMINALHIRE_DIR2);
34
96
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
35
97
  for (; ; ) {
36
98
  try {
@@ -89,11 +151,15 @@ function readClaims() {
89
151
  }
90
152
  }
91
153
  function writeClaims(claims) {
92
- mkdirSync2(TERMINALHIRE_DIR2, { recursive: true, mode: 448 });
154
+ ensureStateDir(TERMINALHIRE_DIR2);
93
155
  const tmp = `${CLAIMS_FILE}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
94
156
  const payload = { claims };
95
157
  try {
96
- writeFileSync2(tmp, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384, flag: "wx" });
158
+ writeFileSync2(tmp, JSON.stringify(payload, null, 2), {
159
+ encoding: "utf8",
160
+ mode: 384,
161
+ flag: "wx"
162
+ });
97
163
  renameSync2(tmp, CLAIMS_FILE);
98
164
  } catch (err) {
99
165
  try {
@@ -167,6 +233,7 @@ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK
167
233
  var init_claims = __esm({
168
234
  "src/claims.ts"() {
169
235
  "use strict";
236
+ init_state_dir();
170
237
  TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
171
238
  CLAIMS_FILE = join2(TERMINALHIRE_DIR2, "claims.json");
172
239
  LOCK_DIR = `${CLAIMS_FILE}.lock`;
@@ -184,8 +251,22 @@ var init_claims = __esm({
184
251
  ];
185
252
  TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
186
253
  POLL_TRANSITIONS = {
187
- merged: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready", "submitted", "abandoned"]),
188
- abandoned: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready", "submitted", "merged"]),
254
+ merged: /* @__PURE__ */ new Set([
255
+ "claimed",
256
+ "working",
257
+ "in-review",
258
+ "ready",
259
+ "submitted",
260
+ "abandoned"
261
+ ]),
262
+ abandoned: /* @__PURE__ */ new Set([
263
+ "claimed",
264
+ "working",
265
+ "in-review",
266
+ "ready",
267
+ "submitted",
268
+ "merged"
269
+ ]),
189
270
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
190
271
  };
191
272
  }
@@ -193,24 +274,14 @@ var init_claims = __esm({
193
274
 
194
275
  // bin/claim-push-bg.js
195
276
  import { createHash } from "crypto";
196
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3, rmSync as rmSync3 } from "fs";
277
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync as rmSync3 } from "fs";
197
278
  import { join as join3 } from "path";
198
279
  import { homedir as homedir3 } from "os";
199
280
 
200
281
  // src/github-auth.ts
201
- import {
202
- createCipheriv,
203
- createDecipheriv,
204
- randomBytes
205
- } from "crypto";
206
- import {
207
- readFileSync,
208
- writeFileSync,
209
- mkdirSync,
210
- existsSync,
211
- rmSync,
212
- renameSync
213
- } from "fs";
282
+ init_state_dir();
283
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
284
+ import { readFileSync, writeFileSync, existsSync, rmSync, renameSync } from "fs";
214
285
  import { join } from "path";
215
286
  import { homedir } from "os";
216
287
  var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
@@ -220,7 +291,7 @@ var ALGO = "aes-256-gcm";
220
291
  var KEY_BYTES = 32;
221
292
  var IV_BYTES = 12;
222
293
  async function loadKey() {
223
- mkdirSync(TERMINALHIRE_DIR, { recursive: true, mode: 448 });
294
+ ensureStateDir(TERMINALHIRE_DIR);
224
295
  if (existsSync(KEY_FILE)) {
225
296
  return Buffer.from(readFileSync(KEY_FILE, "utf8").trim(), "hex");
226
297
  }
@@ -246,6 +317,7 @@ function decrypt(blob, key) {
246
317
  }
247
318
 
248
319
  // bin/claim-push-bg.js
320
+ init_state_dir();
249
321
  var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join3(homedir3(), ".terminalhire");
250
322
  var CLAIM_PUSH_AUTO_MARKER = join3(TERMINALHIRE_DIR3, "claim-push-auto.json");
251
323
  var CLAIM_PUSH_TOKEN_FILE = join3(TERMINALHIRE_DIR3, "claim-push-token.enc");
@@ -254,7 +326,7 @@ var CLAIM_SYNC_BASE = "https://terminalhire.com";
254
326
  var AUTO_CONSENT_VERSION = 2;
255
327
  var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
256
328
  async function writePushTokenEnc(rawToken) {
257
- mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
329
+ ensureStateDir(TERMINALHIRE_DIR3);
258
330
  const key = await loadKey();
259
331
  const blob = encrypt(rawToken, key);
260
332
  writeFileSync3(CLAIM_PUSH_TOKEN_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
@@ -283,7 +355,7 @@ function readAutoMarker() {
283
355
  }
284
356
  }
285
357
  function writeAutoMarker(marker) {
286
- mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
358
+ ensureStateDir(TERMINALHIRE_DIR3);
287
359
  writeFileSync3(CLAIM_PUSH_AUTO_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
288
360
  }
289
361
  function clearAutoMarker() {
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/directory.js
4
+ import { readFileSync, writeFileSync, renameSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+
8
+ // src/state-dir.ts
9
+ import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
10
+ var STATE_DIR_MODE = 448;
11
+ var STATE_DIR_OK = "ok";
12
+ var STATE_DIR_SYMLINK = "symlink";
13
+ var STATE_DIR_UNVERIFIED = "unverified";
14
+ var warnedDirs = /* @__PURE__ */ new Set();
15
+ function warnStateDirOnce(dir, message) {
16
+ if (warnedDirs.has(dir)) return;
17
+ warnedDirs.add(dir);
18
+ try {
19
+ process.stderr.write(message);
20
+ } catch {
21
+ }
22
+ }
23
+ function ensureStateDir(dir) {
24
+ mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
25
+ const noFollow = constants.O_NOFOLLOW ?? 0;
26
+ let fd;
27
+ try {
28
+ fd = openSync(dir, constants.O_RDONLY | noFollow);
29
+ } catch (err) {
30
+ if (err?.code === "ELOOP") {
31
+ warnStateDirOnce(
32
+ dir,
33
+ `terminalhire: ${dir} is a symlink \u2014 leaving its permissions alone; the 0700 guarantee on the state directory is NOT enforced.
34
+ `
35
+ );
36
+ return STATE_DIR_SYMLINK;
37
+ }
38
+ return STATE_DIR_UNVERIFIED;
39
+ }
40
+ try {
41
+ const currentMode = fstatSync(fd).mode & 511;
42
+ if ((currentMode & ~STATE_DIR_MODE) !== 0) {
43
+ fchmodSync(fd, currentMode & STATE_DIR_MODE);
44
+ }
45
+ return STATE_DIR_OK;
46
+ } catch {
47
+ return STATE_DIR_UNVERIFIED;
48
+ } finally {
49
+ try {
50
+ closeSync(fd);
51
+ } catch {
52
+ }
53
+ }
54
+ }
55
+
56
+ // bin/directory.js
57
+ var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
58
+ var DIRECTORY_CACHE_FILE = join(TERMINALHIRE_DIR, "directory-cache.json");
59
+ var PROJECT_FILE = join(TERMINALHIRE_DIR, "project.json");
60
+ var INDEX_TTL_MS = 15 * 60 * 1e3;
61
+ var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
62
+ function readDirectoryCache() {
63
+ try {
64
+ const entry = JSON.parse(readFileSync(DIRECTORY_CACHE_FILE, "utf8"));
65
+ if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && Date.now() - entry.ts < INDEX_TTL_MS) {
66
+ return { index: entry.index, ts: entry.ts };
67
+ }
68
+ return null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function writeDirectoryCache(index) {
74
+ ensureStateDir(TERMINALHIRE_DIR);
75
+ writeFileSync(DIRECTORY_CACHE_FILE, JSON.stringify({ ts: Date.now(), index }), "utf8");
76
+ }
77
+ function readProject() {
78
+ try {
79
+ return JSON.parse(readFileSync(PROJECT_FILE, "utf8"));
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ function writeProject(patch) {
85
+ const existing = readProject() ?? {};
86
+ const merged = { ...existing, ...patch };
87
+ if (!merged.createdAt) merged.createdAt = (/* @__PURE__ */ new Date()).toISOString();
88
+ ensureStateDir(TERMINALHIRE_DIR);
89
+ const tmpFile = `${PROJECT_FILE}.tmp`;
90
+ writeFileSync(tmpFile, JSON.stringify(merged, null, 2), "utf8");
91
+ renameSync(tmpFile, PROJECT_FILE);
92
+ return merged;
93
+ }
94
+ function relativeTime(ts) {
95
+ const secs = Math.max(0, Math.round((Date.now() - ts) / 1e3));
96
+ if (secs < 60) return `${secs}s ago`;
97
+ const mins = Math.round(secs / 60);
98
+ return mins < 60 ? `${mins}m ago` : `${Math.round(mins / 60)}h ago`;
99
+ }
100
+ async function fetchDirectory({ quiet = false } = {}) {
101
+ const cached = readDirectoryCache();
102
+ if (cached) {
103
+ if (!quiet) console.log(`\u2713 Using cached directory (updated ${relativeTime(cached.ts)})`);
104
+ return cached.index;
105
+ }
106
+ if (!quiet) console.log(`\u21BB Refreshing builder directory from ${API_URL}/api/directory...`);
107
+ const res = await fetch(`${API_URL}/api/directory`, { signal: AbortSignal.timeout(1e4) });
108
+ if (!res.ok) throw new Error(`/api/directory returned ${res.status}`);
109
+ const index = await res.json();
110
+ writeDirectoryCache(index);
111
+ return index;
112
+ }
113
+ function reportMatched(results, fetchImpl = fetch) {
114
+ try {
115
+ const logins = [
116
+ ...new Set(
117
+ results.map((r) => r?.job?.company).filter((login) => typeof login === "string" && login.length > 0)
118
+ )
119
+ ];
120
+ if (logins.length === 0) return;
121
+ return Promise.resolve(
122
+ fetchImpl(`${API_URL}/api/directory/matched`, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({ matched: logins }),
126
+ signal: AbortSignal.timeout(3e3)
127
+ })
128
+ ).catch(() => {
129
+ });
130
+ } catch {
131
+ }
132
+ }
133
+ function excludeOwnCard(results, ownLogin) {
134
+ if (!Array.isArray(results)) return results;
135
+ if (typeof ownLogin !== "string" || ownLogin.length === 0) return results;
136
+ const own = ownLogin.toLowerCase();
137
+ return results.filter((r) => {
138
+ const handle = r?.job?.company;
139
+ return typeof handle !== "string" || handle.toLowerCase() !== own;
140
+ });
141
+ }
142
+ export {
143
+ excludeOwnCard,
144
+ fetchDirectory,
145
+ readDirectoryCache,
146
+ readProject,
147
+ relativeTime,
148
+ reportMatched,
149
+ writeDirectoryCache,
150
+ writeProject
151
+ };