terminalhire 0.9.0 → 0.9.2

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.
@@ -774,6 +774,45 @@ var init_src = __esm({
774
774
  }
775
775
  });
776
776
 
777
+ // src/web-session.ts
778
+ import {
779
+ chmodSync,
780
+ existsSync,
781
+ mkdirSync,
782
+ readFileSync as readFileSync2,
783
+ rmSync,
784
+ writeFileSync
785
+ } from "fs";
786
+ import { homedir } from "os";
787
+ import { join as join2 } from "path";
788
+ function terminalhireDir() {
789
+ return join2(homedir(), ".terminalhire");
790
+ }
791
+ function webSessionFilePath() {
792
+ return join2(terminalhireDir(), "web-session");
793
+ }
794
+ function readWebSessionFile() {
795
+ try {
796
+ const path = webSessionFilePath();
797
+ if (!existsSync(path)) return null;
798
+ const v = readFileSync2(path, "utf8").trim();
799
+ return v.length > 0 ? v : null;
800
+ } catch {
801
+ return null;
802
+ }
803
+ }
804
+ function readWebSessionCookie() {
805
+ const fromFile = readWebSessionFile();
806
+ if (fromFile) return fromFile;
807
+ const env = process.env["TERMINALHIRE_WEB_SESSION"];
808
+ return typeof env === "string" && env.length > 0 ? env : null;
809
+ }
810
+ var init_web_session = __esm({
811
+ "src/web-session.ts"() {
812
+ "use strict";
813
+ }
814
+ });
815
+
777
816
  // src/profile.ts
778
817
  var profile_exports = {};
779
818
  __export(profile_exports, {
@@ -794,13 +833,13 @@ import {
794
833
  randomBytes as randomBytes2
795
834
  } from "crypto";
796
835
  import {
797
- readFileSync as readFileSync2,
798
- writeFileSync,
799
- mkdirSync,
800
- existsSync
836
+ readFileSync as readFileSync3,
837
+ writeFileSync as writeFileSync2,
838
+ mkdirSync as mkdirSync2,
839
+ existsSync as existsSync2
801
840
  } from "fs";
802
- import { join as join2 } from "path";
803
- import { homedir } from "os";
841
+ import { join as join3 } from "path";
842
+ import { homedir as homedir2 } from "os";
804
843
  async function loadKey() {
805
844
  try {
806
845
  const kt = await import("keytar");
@@ -813,12 +852,12 @@ async function loadKey() {
813
852
  return key2;
814
853
  } catch {
815
854
  }
816
- mkdirSync(TERMINALHIRE_DIR, { recursive: true });
817
- if (existsSync(KEY_FILE)) {
818
- return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
855
+ mkdirSync2(TERMINALHIRE_DIR, { recursive: true });
856
+ if (existsSync2(KEY_FILE)) {
857
+ return Buffer.from(readFileSync3(KEY_FILE, "utf8").trim(), "hex");
819
858
  }
820
859
  const key = randomBytes2(KEY_BYTES);
821
- writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
860
+ writeFileSync2(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
822
861
  return key;
823
862
  }
824
863
  function encrypt(plaintext, key) {
@@ -876,10 +915,10 @@ function migrateTagWeights(profile) {
876
915
  }
877
916
  }
878
917
  async function readProfile() {
879
- if (!existsSync(PROFILE_FILE)) return blankProfile();
918
+ if (!existsSync2(PROFILE_FILE)) return blankProfile();
880
919
  try {
881
920
  const key = await loadKey();
882
- const raw = readFileSync2(PROFILE_FILE, "utf8");
921
+ const raw = readFileSync3(PROFILE_FILE, "utf8");
883
922
  const blob = JSON.parse(raw);
884
923
  const plaintext = decrypt(blob, key);
885
924
  const parsed = JSON.parse(plaintext);
@@ -890,12 +929,12 @@ async function readProfile() {
890
929
  }
891
930
  }
892
931
  async function writeProfile(profile) {
893
- mkdirSync(TERMINALHIRE_DIR, { recursive: true });
932
+ mkdirSync2(TERMINALHIRE_DIR, { recursive: true });
894
933
  const key = await loadKey();
895
934
  profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
896
935
  profile.skillTags = deriveSkillTags(profile.tagWeights);
897
936
  const blob = encrypt(JSON.stringify(profile), key);
898
- writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
937
+ writeFileSync2(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
899
938
  }
900
939
  function accumulateSession(profile, tags, isEmployerContext, inferredSeniority, seniorityIsAuthoritative = false) {
901
940
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -956,13 +995,13 @@ async function removeSavedJob(id) {
956
995
  return true;
957
996
  }
958
997
  async function deleteProfile() {
959
- const { rmSync } = await import("fs");
998
+ const { rmSync: rmSync2 } = await import("fs");
960
999
  try {
961
- rmSync(PROFILE_FILE);
1000
+ rmSync2(PROFILE_FILE);
962
1001
  } catch {
963
1002
  }
964
1003
  try {
965
- rmSync(KEY_FILE);
1004
+ rmSync2(KEY_FILE);
966
1005
  } catch {
967
1006
  }
968
1007
  }
@@ -984,9 +1023,9 @@ var init_profile = __esm({
984
1023
  "src/profile.ts"() {
985
1024
  "use strict";
986
1025
  init_src();
987
- TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
988
- PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
989
- KEY_FILE = join2(TERMINALHIRE_DIR, "key");
1026
+ TERMINALHIRE_DIR = join3(homedir2(), ".terminalhire");
1027
+ PROFILE_FILE = join3(TERMINALHIRE_DIR, "profile.enc");
1028
+ KEY_FILE = join3(TERMINALHIRE_DIR, "key");
990
1029
  ALGO = "aes-256-gcm";
991
1030
  KEY_BYTES = 32;
992
1031
  IV_BYTES = 12;
@@ -1089,10 +1128,9 @@ function defaultIntroDeps() {
1089
1128
  void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
1090
1129
  });
1091
1130
  },
1092
- sessionCookie: () => {
1093
- const v = process.env["TERMINALHIRE_WEB_SESSION"];
1094
- return typeof v === "string" && v.length > 0 ? v : null;
1095
- },
1131
+ // Session source priority: persisted file (`terminalhire link`) FIRST, then the
1132
+ // legacy TERMINALHIRE_WEB_SESSION env, then none.
1133
+ sessionCookie: () => readWebSessionCookie(),
1096
1134
  log: (msg) => console.log(msg),
1097
1135
  errorLog: (msg) => console.error(msg),
1098
1136
  exit: (code) => process.exit(code)
@@ -1164,10 +1202,7 @@ async function runIntroRequest(args, overrides) {
1164
1202
  const cookie = deps.sessionCookie();
1165
1203
  if (!cookie) {
1166
1204
  deps.log("\n No linked web session found on this machine.");
1167
- deps.log(" Sign in at your dashboard first, then re-run with a bridged session.");
1168
- deps.log(` \u2192 ${LINK_BASE}/dashboard
1169
- `);
1170
- deps.openBrowser(`${LINK_BASE}/dashboard`);
1205
+ deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
1171
1206
  deps.exit(0);
1172
1207
  return;
1173
1208
  }
@@ -1187,9 +1222,8 @@ async function runIntroRequest(args, overrides) {
1187
1222
  return;
1188
1223
  }
1189
1224
  if (res.status === 401) {
1190
- deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
1191
- deps.log(` \u2192 ${LINK_BASE}/dashboard
1192
- `);
1225
+ deps.log("\n Your linked web session expired.");
1226
+ deps.log(" Run `terminalhire link` to reconnect this terminal, then re-run.\n");
1193
1227
  deps.exit(1);
1194
1228
  return;
1195
1229
  }
@@ -1220,10 +1254,7 @@ async function runIntroDecision(args, overrides) {
1220
1254
  const cookie = deps.sessionCookie();
1221
1255
  if (!cookie) {
1222
1256
  deps.log("\n No linked web session found on this machine.");
1223
- deps.log(" Sign in at your dashboard first, then re-run.");
1224
- deps.log(` \u2192 ${LINK_BASE}/dashboard
1225
- `);
1226
- deps.openBrowser(`${LINK_BASE}/dashboard`);
1257
+ deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
1227
1258
  deps.exit(0);
1228
1259
  return;
1229
1260
  }
@@ -1266,9 +1297,8 @@ async function runIntroDecision(args, overrides) {
1266
1297
  return;
1267
1298
  }
1268
1299
  if (res.status === 401) {
1269
- deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
1270
- deps.log(` \u2192 ${LINK_BASE}/dashboard
1271
- `);
1300
+ deps.log("\n Your linked web session expired.");
1301
+ deps.log(" Run `terminalhire link` to reconnect this terminal, then re-run.\n");
1272
1302
  deps.exit(1);
1273
1303
  return;
1274
1304
  }
@@ -1307,10 +1337,7 @@ async function runIntroList(overrides) {
1307
1337
  const cookie = deps.sessionCookie();
1308
1338
  if (!cookie) {
1309
1339
  deps.log("\n No linked web session found on this machine.");
1310
- deps.log(" Sign in at your dashboard first, then re-run.");
1311
- deps.log(` \u2192 ${LINK_BASE}/dashboard
1312
- `);
1313
- deps.openBrowser(`${LINK_BASE}/dashboard`);
1340
+ deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
1314
1341
  deps.exit(0);
1315
1342
  return;
1316
1343
  }
@@ -1329,9 +1356,8 @@ async function runIntroList(overrides) {
1329
1356
  return;
1330
1357
  }
1331
1358
  if (res.status === 401) {
1332
- deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
1333
- deps.log(` \u2192 ${LINK_BASE}/dashboard
1334
- `);
1359
+ deps.log("\n Your linked web session expired.");
1360
+ deps.log(" Run `terminalhire link` to reconnect this terminal, then re-run.\n");
1335
1361
  deps.exit(1);
1336
1362
  return;
1337
1363
  }
@@ -1369,6 +1395,7 @@ var init_intro2 = __esm({
1369
1395
  "src/intro.ts"() {
1370
1396
  "use strict";
1371
1397
  init_src();
1398
+ init_web_session();
1372
1399
  LINK_BASE = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
1373
1400
  GH_SESSION_COOKIE = "__jpi_gh_session";
1374
1401
  }
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/web-session.ts
13
+ import {
14
+ chmodSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ readFileSync,
18
+ rmSync,
19
+ writeFileSync
20
+ } from "fs";
21
+ import { homedir } from "os";
22
+ import { join } from "path";
23
+ function terminalhireDir() {
24
+ return join(homedir(), ".terminalhire");
25
+ }
26
+ function webSessionFilePath() {
27
+ return join(terminalhireDir(), "web-session");
28
+ }
29
+ function readWebSessionFile() {
30
+ try {
31
+ const path = webSessionFilePath();
32
+ if (!existsSync(path)) return null;
33
+ const v = readFileSync(path, "utf8").trim();
34
+ return v.length > 0 ? v : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function writeWebSessionFile(token) {
40
+ mkdirSync(terminalhireDir(), { recursive: true });
41
+ const path = webSessionFilePath();
42
+ writeFileSync(path, token, { mode: 384, encoding: "utf8" });
43
+ try {
44
+ chmodSync(path, 384);
45
+ } catch {
46
+ }
47
+ }
48
+ function clearWebSessionFile() {
49
+ try {
50
+ rmSync(webSessionFilePath());
51
+ } catch {
52
+ }
53
+ }
54
+ var init_web_session = __esm({
55
+ "src/web-session.ts"() {
56
+ "use strict";
57
+ }
58
+ });
59
+
60
+ // src/open-url.js
61
+ var open_url_exports = {};
62
+ __export(open_url_exports, {
63
+ openInBrowser: () => openInBrowser
64
+ });
65
+ import { spawn } from "child_process";
66
+ function openInBrowser(url) {
67
+ let cmd;
68
+ let args;
69
+ if (process.platform === "darwin") {
70
+ cmd = "open";
71
+ args = [url];
72
+ } else if (process.platform === "win32") {
73
+ cmd = "cmd";
74
+ args = ["/c", "start", "", url];
75
+ } else {
76
+ cmd = "xdg-open";
77
+ args = [url];
78
+ }
79
+ try {
80
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
81
+ child.on("error", () => {
82
+ });
83
+ child.unref();
84
+ } catch {
85
+ }
86
+ }
87
+ var init_open_url = __esm({
88
+ "src/open-url.js"() {
89
+ "use strict";
90
+ }
91
+ });
92
+
93
+ // src/link.ts
94
+ var link_exports = {};
95
+ __export(link_exports, {
96
+ resolveLoopbackRequest: () => resolveLoopbackRequest,
97
+ runLink: () => runLink,
98
+ runLinkLogout: () => runLinkLogout
99
+ });
100
+ import { createServer } from "http";
101
+ import { randomBytes } from "crypto";
102
+ function resolveLoopbackRequest(rawUrl, expectedNonce) {
103
+ let u;
104
+ try {
105
+ u = new URL(rawUrl, "http://127.0.0.1");
106
+ } catch {
107
+ return { ok: false, reason: "bad_url" };
108
+ }
109
+ const nonce = u.searchParams.get("nonce");
110
+ if (!nonce || nonce !== expectedNonce) return { ok: false, reason: "nonce_mismatch" };
111
+ const token = u.searchParams.get("token");
112
+ if (!token) return { ok: false, reason: "missing_token" };
113
+ return { ok: true, token };
114
+ }
115
+ function defaultStartLoopback(expectedNonce, timeoutMs) {
116
+ return new Promise((resolveHandle) => {
117
+ let settle;
118
+ const result = new Promise((res) => {
119
+ settle = res;
120
+ });
121
+ let done = false;
122
+ const finish = (r) => {
123
+ if (done) return;
124
+ done = true;
125
+ clearTimeout(timer);
126
+ settle(r);
127
+ setImmediate(() => {
128
+ try {
129
+ server.close();
130
+ } catch {
131
+ }
132
+ });
133
+ };
134
+ const server = createServer((req, res) => {
135
+ const outcome = resolveLoopbackRequest(req.url ?? "", expectedNonce);
136
+ res.writeHead(outcome.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
137
+ res.end(outcome.ok ? LINKED_HTML : FAILED_HTML);
138
+ finish(outcome);
139
+ });
140
+ const timer = setTimeout(() => finish({ ok: false, reason: "timeout" }), timeoutMs);
141
+ if (typeof timer.unref === "function") timer.unref();
142
+ server.on("error", () => finish({ ok: false, reason: "listen_error" }));
143
+ server.listen(0, "127.0.0.1", () => {
144
+ const addr = server.address();
145
+ const port = typeof addr === "object" && addr ? addr.port : 0;
146
+ resolveHandle({
147
+ port,
148
+ result,
149
+ close: () => {
150
+ try {
151
+ server.close();
152
+ } catch {
153
+ }
154
+ }
155
+ });
156
+ });
157
+ });
158
+ }
159
+ function defaultLinkDeps() {
160
+ return {
161
+ startLoopback: defaultStartLoopback,
162
+ openBrowser: (url) => {
163
+ void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
164
+ });
165
+ },
166
+ generateNonce: () => randomBytes(16).toString("hex"),
167
+ persistToken: (token) => writeWebSessionFile(token),
168
+ log: (msg) => console.log(msg),
169
+ errorLog: (msg) => console.error(msg),
170
+ exit: (code) => process.exit(code)
171
+ };
172
+ }
173
+ async function runLink(overrides) {
174
+ const deps = { ...defaultLinkDeps(), ...overrides };
175
+ const nonce = deps.generateNonce();
176
+ const handle = await deps.startLoopback(nonce, LINK_TIMEOUT_MS);
177
+ const url = `${LINK_BASE}/api/auth/link?port=${handle.port}&nonce=${encodeURIComponent(nonce)}`;
178
+ deps.log("");
179
+ deps.log(" terminalhire \u2014 link this terminal to your account");
180
+ deps.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
181
+ deps.log(" Opening your browser to approve. If it does not open, paste this URL:");
182
+ deps.log(` \u2192 ${url}`);
183
+ deps.log(" Waiting for approval (this tab closes itself once you approve)\u2026");
184
+ deps.openBrowser(url);
185
+ let outcome;
186
+ try {
187
+ outcome = await handle.result;
188
+ } finally {
189
+ handle.close();
190
+ }
191
+ if (!outcome.ok || !outcome.token) {
192
+ if (outcome.reason === "timeout") {
193
+ deps.errorLog("\n Link timed out \u2014 run `terminalhire link` again.\n");
194
+ } else if (outcome.reason === "nonce_mismatch") {
195
+ deps.errorLog("\n Link rejected (nonce did not match) \u2014 run `terminalhire link` again.\n");
196
+ } else {
197
+ deps.errorLog("\n Link failed \u2014 run `terminalhire link` again.\n");
198
+ }
199
+ deps.exit(1);
200
+ return;
201
+ }
202
+ deps.persistToken(outcome.token);
203
+ deps.log("\n This terminal is now linked to your terminalhire account.");
204
+ deps.log(" Try `terminalhire intro <login>`, `terminalhire chat`, or `terminalhire trajectory --push`.");
205
+ deps.log(" Unlink any time with `terminalhire link --logout`.\n");
206
+ deps.exit(0);
207
+ }
208
+ function defaultLinkLogoutDeps() {
209
+ return {
210
+ fetchImpl: (...args) => globalThis.fetch(...args),
211
+ readSessionFile: () => readWebSessionFile(),
212
+ clearSessionFile: () => clearWebSessionFile(),
213
+ log: (msg) => console.log(msg),
214
+ errorLog: (msg) => console.error(msg),
215
+ exit: (code) => process.exit(code)
216
+ };
217
+ }
218
+ async function runLinkLogout(overrides) {
219
+ const deps = { ...defaultLinkLogoutDeps(), ...overrides };
220
+ const token = deps.readSessionFile();
221
+ if (!token) {
222
+ deps.log("\n No linked web session on this machine \u2014 nothing to unlink.\n");
223
+ deps.exit(0);
224
+ return;
225
+ }
226
+ let revoked = false;
227
+ try {
228
+ const res = await deps.fetchImpl(`${LINK_BASE}/api/auth/session`, {
229
+ method: "DELETE",
230
+ headers: { Cookie: `${GH_SESSION_COOKIE}=${token}` },
231
+ signal: AbortSignal.timeout(1e4)
232
+ });
233
+ revoked = res.ok;
234
+ } catch {
235
+ }
236
+ deps.clearSessionFile();
237
+ if (revoked) {
238
+ deps.log("\n Unlinked \u2014 the session was revoked server-side and removed from this machine.\n");
239
+ } else {
240
+ deps.log("\n Removed the local session from this machine.");
241
+ deps.log(" (Could not reach the server to revoke it \u2014 it expires on its own.)\n");
242
+ }
243
+ deps.exit(0);
244
+ }
245
+ var LINK_BASE, GH_SESSION_COOKIE, LINK_TIMEOUT_MS, LINKED_HTML, FAILED_HTML;
246
+ var init_link = __esm({
247
+ "src/link.ts"() {
248
+ "use strict";
249
+ init_web_session();
250
+ LINK_BASE = "https://www.terminalhire.com";
251
+ GH_SESSION_COOKIE = "__jpi_gh_session";
252
+ LINK_TIMEOUT_MS = 12e4;
253
+ LINKED_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>terminalhire</title></head>
254
+ <body style="font-family:system-ui;padding:2rem;background:#0b0d10;color:#e6e6e6">
255
+ <script>history.replaceState({},'','/');</script>
256
+ <p>CLI linked \u2014 you can close this tab.</p>
257
+ <script>setTimeout(function(){window.close();},400);</script>
258
+ </body></html>`;
259
+ FAILED_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>terminalhire</title></head>
260
+ <body style="font-family:system-ui;padding:2rem;background:#0b0d10;color:#e6e6e6">
261
+ <script>history.replaceState({},'','/');</script>
262
+ <p>Link failed \u2014 return to your terminal and run <code>terminalhire link</code> again.</p>
263
+ </body></html>`;
264
+ }
265
+ });
266
+
267
+ // bin/jpi-link.js
268
+ async function run() {
269
+ try {
270
+ const args = process.argv.slice(2);
271
+ if (args.includes("--logout")) {
272
+ const { runLinkLogout: runLinkLogout2 } = await Promise.resolve().then(() => (init_link(), link_exports));
273
+ await runLinkLogout2();
274
+ return;
275
+ }
276
+ const { runLink: runLink2 } = await Promise.resolve().then(() => (init_link(), link_exports));
277
+ await runLink2();
278
+ } catch (err) {
279
+ console.error("terminalhire link error:", err?.message ?? err);
280
+ process.exit(1);
281
+ }
282
+ }
283
+ export {
284
+ run
285
+ };