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.
@@ -1943,14 +1943,14 @@ async function mapWithConcurrency(items, limit, fn) {
1943
1943
  if (items.length === 0) return results;
1944
1944
  const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1945
1945
  let next = 0;
1946
- async function run18() {
1946
+ async function run19() {
1947
1947
  for (; ; ) {
1948
1948
  const i = next++;
1949
1949
  if (i >= items.length) return;
1950
1950
  results[i] = await fn(items[i], i);
1951
1951
  }
1952
1952
  }
1953
- await Promise.all(Array.from({ length: workers }, run18));
1953
+ await Promise.all(Array.from({ length: workers }, run19));
1954
1954
  return results;
1955
1955
  }
1956
1956
  var init_concurrency = __esm({
@@ -4603,7 +4603,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
4603
4603
  });
4604
4604
  const { prehash } = eddsaOpts;
4605
4605
  const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
4606
- const randomBytes5 = eddsaOpts.randomBytes || randomBytes2;
4606
+ const randomBytes6 = eddsaOpts.randomBytes || randomBytes2;
4607
4607
  const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
4608
4608
  const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
4609
4609
  _abool2(phflag, "phflag");
@@ -4685,7 +4685,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
4685
4685
  signature: 2 * _size,
4686
4686
  seed: _size
4687
4687
  };
4688
- function randomSecretKey(seed = randomBytes5(lengths.seed)) {
4688
+ function randomSecretKey(seed = randomBytes6(lengths.seed)) {
4689
4689
  return _abytes2(seed, lengths.seed, "seed");
4690
4690
  }
4691
4691
  function keygen(seed) {
@@ -6370,13 +6370,13 @@ async function removeSavedJob(id) {
6370
6370
  return true;
6371
6371
  }
6372
6372
  async function deleteProfile() {
6373
- const { rmSync: rmSync4 } = await import("fs");
6373
+ const { rmSync: rmSync5 } = await import("fs");
6374
6374
  try {
6375
- rmSync4(PROFILE_FILE);
6375
+ rmSync5(PROFILE_FILE);
6376
6376
  } catch {
6377
6377
  }
6378
6378
  try {
6379
- rmSync4(KEY_FILE2);
6379
+ rmSync5(KEY_FILE2);
6380
6380
  } catch {
6381
6381
  }
6382
6382
  }
@@ -6613,11 +6613,11 @@ async function runLogin() {
6613
6613
  if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
6614
6614
  const { createRequire: createRequire2 } = await import("module");
6615
6615
  const { fileURLToPath: fileURLToPath7 } = await import("url");
6616
- const { join: join24, dirname: dirname3 } = await import("path");
6616
+ const { join: join25, dirname: dirname3 } = await import("path");
6617
6617
  const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
6618
- const fixturePath = join24(__dirname6, "../../fixtures/github-sample.json");
6619
- const { readFileSync: readFileSync22 } = await import("fs");
6620
- ghProfile = JSON.parse(readFileSync22(fixturePath, "utf8"));
6618
+ const fixturePath = join25(__dirname6, "../../fixtures/github-sample.json");
6619
+ const { readFileSync: readFileSync23 } = await import("fs");
6620
+ ghProfile = JSON.parse(readFileSync23(fixturePath, "utf8"));
6621
6621
  } else {
6622
6622
  ghProfile = await fetchGitHubProfile2(login, token);
6623
6623
  }
@@ -8377,7 +8377,7 @@ function finalize(build) {
8377
8377
  };
8378
8378
  }
8379
8379
  function reconstruct(files, opts = {}) {
8380
- const join24 = opts.joinSidechains !== false;
8380
+ const join25 = opts.joinSidechains !== false;
8381
8381
  const mains = [];
8382
8382
  const sidechains = [];
8383
8383
  for (const file of files) {
@@ -8402,7 +8402,7 @@ function reconstruct(files, opts = {}) {
8402
8402
  }
8403
8403
  const orphanedSidechainPaths = [];
8404
8404
  const joinedPaths = /* @__PURE__ */ new Set();
8405
- if (join24) {
8405
+ if (join25) {
8406
8406
  const sidechainsBySession = /* @__PURE__ */ new Map();
8407
8407
  for (const sc of sidechains) {
8408
8408
  const acc = sidechainsBySession.get(sc.sessionId) ?? [];
@@ -8889,12 +8889,12 @@ function deriveRecoveryDepth(episodes, nodesByUuid) {
8889
8889
  continue;
8890
8890
  }
8891
8891
  spansConsidered++;
8892
- let run18 = 0;
8892
+ let run19 = 0;
8893
8893
  const closeChain = () => {
8894
- if (run18 > 0) {
8894
+ if (run19 > 0) {
8895
8895
  recoveryChains++;
8896
- totalDepth += run18;
8897
- run18 = 0;
8896
+ totalDepth += run19;
8897
+ run19 = 0;
8898
8898
  }
8899
8899
  };
8900
8900
  for (const uuid of episode.mainNodeUuids) {
@@ -8903,9 +8903,9 @@ function deriveRecoveryDepth(episodes, nodesByUuid) {
8903
8903
  continue;
8904
8904
  }
8905
8905
  if (node.kind === "tool_result" /* ToolResult */ && node.isError) {
8906
- run18++;
8907
- if (run18 > maxConsecutiveErrors) {
8908
- maxConsecutiveErrors = run18;
8906
+ run19++;
8907
+ if (run19 > maxConsecutiveErrors) {
8908
+ maxConsecutiveErrors = run19;
8909
8909
  }
8910
8910
  } else if (node.kind === "tool_result" /* ToolResult */ && !node.isError) {
8911
8911
  closeChain();
@@ -8947,6 +8947,60 @@ var init_episodes = __esm({
8947
8947
  }
8948
8948
  });
8949
8949
 
8950
+ // src/web-session.ts
8951
+ import {
8952
+ chmodSync as chmodSync2,
8953
+ existsSync as existsSync7,
8954
+ mkdirSync as mkdirSync9,
8955
+ readFileSync as readFileSync11,
8956
+ rmSync as rmSync2,
8957
+ writeFileSync as writeFileSync9
8958
+ } from "fs";
8959
+ import { homedir as homedir10 } from "os";
8960
+ import { join as join11 } from "path";
8961
+ function terminalhireDir() {
8962
+ return join11(homedir10(), ".terminalhire");
8963
+ }
8964
+ function webSessionFilePath() {
8965
+ return join11(terminalhireDir(), "web-session");
8966
+ }
8967
+ function readWebSessionFile() {
8968
+ try {
8969
+ const path = webSessionFilePath();
8970
+ if (!existsSync7(path)) return null;
8971
+ const v = readFileSync11(path, "utf8").trim();
8972
+ return v.length > 0 ? v : null;
8973
+ } catch {
8974
+ return null;
8975
+ }
8976
+ }
8977
+ function readWebSessionCookie() {
8978
+ const fromFile = readWebSessionFile();
8979
+ if (fromFile) return fromFile;
8980
+ const env = process.env["TERMINALHIRE_WEB_SESSION"];
8981
+ return typeof env === "string" && env.length > 0 ? env : null;
8982
+ }
8983
+ function writeWebSessionFile(token) {
8984
+ mkdirSync9(terminalhireDir(), { recursive: true });
8985
+ const path = webSessionFilePath();
8986
+ writeFileSync9(path, token, { mode: 384, encoding: "utf8" });
8987
+ try {
8988
+ chmodSync2(path, 384);
8989
+ } catch {
8990
+ }
8991
+ }
8992
+ function clearWebSessionFile() {
8993
+ try {
8994
+ rmSync2(webSessionFilePath());
8995
+ } catch {
8996
+ }
8997
+ }
8998
+ var init_web_session = __esm({
8999
+ "src/web-session.ts"() {
9000
+ "use strict";
9001
+ }
9002
+ });
9003
+
8950
9004
  // src/trajectory.ts
8951
9005
  var trajectory_exports = {};
8952
9006
  __export(trajectory_exports, {
@@ -8956,14 +9010,14 @@ __export(trajectory_exports, {
8956
9010
  runTrajectoryPush: () => runTrajectoryPush
8957
9011
  });
8958
9012
  import {
8959
- existsSync as existsSync7,
8960
- mkdirSync as mkdirSync9,
8961
- readFileSync as readFileSync11,
9013
+ existsSync as existsSync8,
9014
+ mkdirSync as mkdirSync10,
9015
+ readFileSync as readFileSync12,
8962
9016
  readdirSync,
8963
- writeFileSync as writeFileSync9
9017
+ writeFileSync as writeFileSync10
8964
9018
  } from "fs";
8965
- import { homedir as homedir10 } from "os";
8966
- import { join as join11 } from "path";
9019
+ import { homedir as homedir11 } from "os";
9020
+ import { join as join12 } from "path";
8967
9021
  function isRecord4(value) {
8968
9022
  return typeof value === "object" && value !== null && !Array.isArray(value);
8969
9023
  }
@@ -8995,7 +9049,7 @@ function findJsonlFiles(dir) {
8995
9049
  return out;
8996
9050
  }
8997
9051
  for (const entry of entries) {
8998
- const full = join11(dir, entry.name);
9052
+ const full = join12(dir, entry.name);
8999
9053
  if (entry.isDirectory()) {
9000
9054
  out.push(...findJsonlFiles(full));
9001
9055
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -9009,7 +9063,7 @@ function loadCorpus(paths) {
9009
9063
  for (const path of paths) {
9010
9064
  let text;
9011
9065
  try {
9012
- text = readFileSync11(path, "utf8");
9066
+ text = readFileSync12(path, "utf8");
9013
9067
  } catch {
9014
9068
  continue;
9015
9069
  }
@@ -9108,12 +9162,12 @@ function renderMarkdown(view) {
9108
9162
  return lines.join("\n");
9109
9163
  }
9110
9164
  function writeExportArtifacts(score, markdown) {
9111
- const dir = join11(homedir10(), ".terminalhire");
9112
- mkdirSync9(dir, { recursive: true });
9113
- const jsonPath = join11(dir, "trajectory-export.json");
9114
- const mdPath = join11(dir, "trajectory-export.md");
9115
- writeFileSync9(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
9116
- writeFileSync9(mdPath, markdown, "utf8");
9165
+ const dir = join12(homedir11(), ".terminalhire");
9166
+ mkdirSync10(dir, { recursive: true });
9167
+ const jsonPath = join12(dir, "trajectory-export.json");
9168
+ const mdPath = join12(dir, "trajectory-export.md");
9169
+ writeFileSync10(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
9170
+ writeFileSync10(mdPath, markdown, "utf8");
9117
9171
  return { jsonPath, mdPath };
9118
9172
  }
9119
9173
  function renderInward(allNodes, view, files) {
@@ -9132,8 +9186,8 @@ function renderInward(allNodes, view, files) {
9132
9186
  console.log("");
9133
9187
  }
9134
9188
  function buildTrajectory() {
9135
- const projectsDir = join11(homedir10(), ".claude", "projects");
9136
- if (!existsSync7(projectsDir)) return null;
9189
+ const projectsDir = join12(homedir11(), ".claude", "projects");
9190
+ if (!existsSync8(projectsDir)) return null;
9137
9191
  const paths = findJsonlFiles(projectsDir);
9138
9192
  if (paths.length === 0) return null;
9139
9193
  const files = loadCorpus(paths);
@@ -9220,10 +9274,9 @@ function defaultPushDeps() {
9220
9274
  void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
9221
9275
  });
9222
9276
  },
9223
- sessionCookie: () => {
9224
- const v = process.env["TERMINALHIRE_WEB_SESSION"];
9225
- return typeof v === "string" && v.length > 0 ? v : null;
9226
- },
9277
+ // Session source priority: persisted file (`terminalhire link`) FIRST, then the
9278
+ // legacy TERMINALHIRE_WEB_SESSION env, then none.
9279
+ sessionCookie: () => readWebSessionCookie(),
9227
9280
  log: (msg) => console.log(msg),
9228
9281
  errorLog: (msg) => console.error(msg),
9229
9282
  exit: (code) => process.exit(code)
@@ -9382,6 +9435,7 @@ var init_trajectory = __esm({
9382
9435
  "src/trajectory.ts"() {
9383
9436
  "use strict";
9384
9437
  init_episodes();
9438
+ init_web_session();
9385
9439
  CAP_LABELS = {
9386
9440
  "cap:ui-automation": "UI automation",
9387
9441
  "cap:deploys": "Deployment",
@@ -9471,10 +9525,9 @@ function defaultIntroDeps() {
9471
9525
  void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
9472
9526
  });
9473
9527
  },
9474
- sessionCookie: () => {
9475
- const v = process.env["TERMINALHIRE_WEB_SESSION"];
9476
- return typeof v === "string" && v.length > 0 ? v : null;
9477
- },
9528
+ // Session source priority: persisted file (`terminalhire link`) FIRST, then the
9529
+ // legacy TERMINALHIRE_WEB_SESSION env, then none.
9530
+ sessionCookie: () => readWebSessionCookie(),
9478
9531
  log: (msg) => console.log(msg),
9479
9532
  errorLog: (msg) => console.error(msg),
9480
9533
  exit: (code) => process.exit(code)
@@ -9546,10 +9599,7 @@ async function runIntroRequest(args5, overrides) {
9546
9599
  const cookie = deps.sessionCookie();
9547
9600
  if (!cookie) {
9548
9601
  deps.log("\n No linked web session found on this machine.");
9549
- deps.log(" Sign in at your dashboard first, then re-run with a bridged session.");
9550
- deps.log(` \u2192 ${LINK_BASE2}/dashboard
9551
- `);
9552
- deps.openBrowser(`${LINK_BASE2}/dashboard`);
9602
+ deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
9553
9603
  deps.exit(0);
9554
9604
  return;
9555
9605
  }
@@ -9569,9 +9619,8 @@ async function runIntroRequest(args5, overrides) {
9569
9619
  return;
9570
9620
  }
9571
9621
  if (res.status === 401) {
9572
- deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
9573
- deps.log(` \u2192 ${LINK_BASE2}/dashboard
9574
- `);
9622
+ deps.log("\n Your linked web session expired.");
9623
+ deps.log(" Run `terminalhire link` to reconnect this terminal, then re-run.\n");
9575
9624
  deps.exit(1);
9576
9625
  return;
9577
9626
  }
@@ -9602,10 +9651,7 @@ async function runIntroDecision(args5, overrides) {
9602
9651
  const cookie = deps.sessionCookie();
9603
9652
  if (!cookie) {
9604
9653
  deps.log("\n No linked web session found on this machine.");
9605
- deps.log(" Sign in at your dashboard first, then re-run.");
9606
- deps.log(` \u2192 ${LINK_BASE2}/dashboard
9607
- `);
9608
- deps.openBrowser(`${LINK_BASE2}/dashboard`);
9654
+ deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
9609
9655
  deps.exit(0);
9610
9656
  return;
9611
9657
  }
@@ -9648,9 +9694,8 @@ async function runIntroDecision(args5, overrides) {
9648
9694
  return;
9649
9695
  }
9650
9696
  if (res.status === 401) {
9651
- deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
9652
- deps.log(` \u2192 ${LINK_BASE2}/dashboard
9653
- `);
9697
+ deps.log("\n Your linked web session expired.");
9698
+ deps.log(" Run `terminalhire link` to reconnect this terminal, then re-run.\n");
9654
9699
  deps.exit(1);
9655
9700
  return;
9656
9701
  }
@@ -9689,10 +9734,7 @@ async function runIntroList(overrides) {
9689
9734
  const cookie = deps.sessionCookie();
9690
9735
  if (!cookie) {
9691
9736
  deps.log("\n No linked web session found on this machine.");
9692
- deps.log(" Sign in at your dashboard first, then re-run.");
9693
- deps.log(` \u2192 ${LINK_BASE2}/dashboard
9694
- `);
9695
- deps.openBrowser(`${LINK_BASE2}/dashboard`);
9737
+ deps.log(" Run `terminalhire link` to connect this terminal to your account, then re-run.\n");
9696
9738
  deps.exit(0);
9697
9739
  return;
9698
9740
  }
@@ -9711,9 +9753,8 @@ async function runIntroList(overrides) {
9711
9753
  return;
9712
9754
  }
9713
9755
  if (res.status === 401) {
9714
- deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
9715
- deps.log(` \u2192 ${LINK_BASE2}/dashboard
9716
- `);
9756
+ deps.log("\n Your linked web session expired.");
9757
+ deps.log(" Run `terminalhire link` to reconnect this terminal, then re-run.\n");
9717
9758
  deps.exit(1);
9718
9759
  return;
9719
9760
  }
@@ -9751,6 +9792,7 @@ var init_intro2 = __esm({
9751
9792
  "src/intro.ts"() {
9752
9793
  "use strict";
9753
9794
  init_src();
9795
+ init_web_session();
9754
9796
  LINK_BASE2 = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
9755
9797
  GH_SESSION_COOKIE2 = "__jpi_gh_session";
9756
9798
  }
@@ -9807,19 +9849,19 @@ var init_jpi_intro = __esm({
9807
9849
  });
9808
9850
 
9809
9851
  // src/chat-keystore.ts
9810
- import { existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync10, rmSync as rmSync2 } from "fs";
9811
- import { homedir as homedir11 } from "os";
9812
- import { join as join12 } from "path";
9852
+ import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync11, rmSync as rmSync3 } from "fs";
9853
+ import { homedir as homedir12 } from "os";
9854
+ import { join as join13 } from "path";
9813
9855
  async function loadOrCreateIdentity() {
9814
9856
  const key = await loadKey();
9815
- if (existsSync8(IDENTITY_FILE)) {
9816
- const blob2 = JSON.parse(readFileSync12(IDENTITY_FILE, "utf8"));
9857
+ if (existsSync9(IDENTITY_FILE)) {
9858
+ const blob2 = JSON.parse(readFileSync13(IDENTITY_FILE, "utf8"));
9817
9859
  return JSON.parse(decrypt(blob2, key));
9818
9860
  }
9819
9861
  const keypair = generateIdentityKeypair();
9820
- mkdirSync10(TERMINALHIRE_DIR10, { recursive: true });
9862
+ mkdirSync11(TERMINALHIRE_DIR10, { recursive: true });
9821
9863
  const blob = encrypt(JSON.stringify(keypair), key);
9822
- writeFileSync10(IDENTITY_FILE, JSON.stringify(blob, null, 2), { mode: 384, encoding: "utf8" });
9864
+ writeFileSync11(IDENTITY_FILE, JSON.stringify(blob, null, 2), { mode: 384, encoding: "utf8" });
9823
9865
  return keypair;
9824
9866
  }
9825
9867
  var TERMINALHIRE_DIR10, IDENTITY_FILE;
@@ -9828,19 +9870,19 @@ var init_chat_keystore = __esm({
9828
9870
  "use strict";
9829
9871
  init_src();
9830
9872
  init_github_auth();
9831
- TERMINALHIRE_DIR10 = join12(homedir11(), ".terminalhire");
9832
- IDENTITY_FILE = join12(TERMINALHIRE_DIR10, "chat-identity.enc");
9873
+ TERMINALHIRE_DIR10 = join13(homedir12(), ".terminalhire");
9874
+ IDENTITY_FILE = join13(TERMINALHIRE_DIR10, "chat-identity.enc");
9833
9875
  }
9834
9876
  });
9835
9877
 
9836
9878
  // src/chat-client.ts
9837
- import { existsSync as existsSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
9838
- import { homedir as homedir12 } from "os";
9839
- import { join as join13 } from "path";
9879
+ import { existsSync as existsSync10, mkdirSync as mkdirSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
9880
+ import { homedir as homedir13 } from "os";
9881
+ import { join as join14 } from "path";
9840
9882
  function defaultReadPeerPins() {
9841
9883
  try {
9842
- if (!existsSync9(PEERS_FILE)) return {};
9843
- const parsed = JSON.parse(readFileSync13(PEERS_FILE, "utf8"));
9884
+ if (!existsSync10(PEERS_FILE)) return {};
9885
+ const parsed = JSON.parse(readFileSync14(PEERS_FILE, "utf8"));
9844
9886
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
9845
9887
  const out = {};
9846
9888
  for (const [login, key] of Object.entries(parsed)) {
@@ -9852,16 +9894,15 @@ function defaultReadPeerPins() {
9852
9894
  }
9853
9895
  }
9854
9896
  function defaultWritePeerPins(pins) {
9855
- mkdirSync11(TERMINALHIRE_DIR11, { recursive: true });
9856
- writeFileSync11(PEERS_FILE, JSON.stringify(pins, null, 2), { mode: 384, encoding: "utf8" });
9897
+ mkdirSync12(TERMINALHIRE_DIR11, { recursive: true });
9898
+ writeFileSync12(PEERS_FILE, JSON.stringify(pins, null, 2), { mode: 384, encoding: "utf8" });
9857
9899
  }
9858
9900
  function defaultChatClientDeps() {
9859
9901
  return {
9860
9902
  fetchImpl: (...args5) => globalThis.fetch(...args5),
9861
- sessionCookie: () => {
9862
- const v = process.env["TERMINALHIRE_WEB_SESSION"];
9863
- return typeof v === "string" && v.length > 0 ? v : null;
9864
- },
9903
+ // Session source priority: persisted file (`terminalhire link`) FIRST, then the
9904
+ // legacy TERMINALHIRE_WEB_SESSION env, then none.
9905
+ sessionCookie: () => readWebSessionCookie(),
9865
9906
  loadIdentity: () => loadOrCreateIdentity(),
9866
9907
  readPeerPins: defaultReadPeerPins,
9867
9908
  writePeerPins: defaultWritePeerPins
@@ -10014,15 +10055,16 @@ var init_chat_client = __esm({
10014
10055
  "use strict";
10015
10056
  init_src();
10016
10057
  init_chat_keystore();
10058
+ init_web_session();
10017
10059
  CHAT_BASE = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
10018
10060
  GH_SESSION_COOKIE3 = "__jpi_gh_session";
10019
- TERMINALHIRE_DIR11 = join13(homedir12(), ".terminalhire");
10020
- PEERS_FILE = join13(TERMINALHIRE_DIR11, "chat-peers.json");
10061
+ TERMINALHIRE_DIR11 = join14(homedir13(), ".terminalhire");
10062
+ PEERS_FILE = join14(TERMINALHIRE_DIR11, "chat-peers.json");
10021
10063
  REQUEST_TIMEOUT_MS = 1e4;
10022
10064
  ChatNotLinkedError = class extends Error {
10023
10065
  constructor() {
10024
10066
  super(
10025
- `No linked web session found on this machine. Sign in at ${CHAT_BASE}/dashboard, then re-run with a bridged session (TERMINALHIRE_WEB_SESSION).`
10067
+ "No linked web session found on this machine. Run `terminalhire link` to connect this terminal to your account, then re-run."
10026
10068
  );
10027
10069
  this.name = "ChatNotLinkedError";
10028
10070
  }
@@ -10030,7 +10072,7 @@ var init_chat_client = __esm({
10030
10072
  ChatSessionExpiredError = class extends Error {
10031
10073
  constructor() {
10032
10074
  super(
10033
- `Your web session expired \u2014 re-link it by signing in again at ${CHAT_BASE}/dashboard and re-bridging TERMINALHIRE_WEB_SESSION, then re-run.`
10075
+ "Your linked web session expired. Run `terminalhire link` to reconnect this terminal, then re-run."
10034
10076
  );
10035
10077
  this.name = "ChatSessionExpiredError";
10036
10078
  }
@@ -10071,13 +10113,13 @@ __export(jpi_chat_read_exports, {
10071
10113
  runSend: () => runSend,
10072
10114
  writeReadCursor: () => writeReadCursor
10073
10115
  });
10074
- import { existsSync as existsSync10, mkdirSync as mkdirSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
10075
- import { homedir as homedir13 } from "os";
10076
- import { join as join14 } from "path";
10116
+ import { existsSync as existsSync11, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
10117
+ import { homedir as homedir14 } from "os";
10118
+ import { join as join15 } from "path";
10077
10119
  function readReadCursors() {
10078
10120
  try {
10079
- if (!existsSync10(READS_FILE)) return {};
10080
- const parsed = JSON.parse(readFileSync14(READS_FILE, "utf8"));
10121
+ if (!existsSync11(READS_FILE)) return {};
10122
+ const parsed = JSON.parse(readFileSync15(READS_FILE, "utf8"));
10081
10123
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
10082
10124
  const out = {};
10083
10125
  for (const [login, iso] of Object.entries(parsed)) {
@@ -10092,8 +10134,8 @@ function writeReadCursor(login, iso, deps = {}) {
10092
10134
  const read = deps.readReadCursors ?? readReadCursors;
10093
10135
  const cursors = read();
10094
10136
  cursors[login] = iso;
10095
- mkdirSync12(TERMINALHIRE_DIR12, { recursive: true });
10096
- writeFileSync12(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
10137
+ mkdirSync13(TERMINALHIRE_DIR12, { recursive: true });
10138
+ writeFileSync13(READS_FILE, JSON.stringify(cursors, null, 2), { mode: 384, encoding: "utf8" });
10097
10139
  }
10098
10140
  function formatClock(iso) {
10099
10141
  const d = new Date(iso);
@@ -10167,11 +10209,7 @@ function writeProblem(output, result, target) {
10167
10209
  switch (result.status) {
10168
10210
  case "not-linked":
10169
10211
  output.write(
10170
- `
10171
- No linked web session found on this machine.
10172
- Sign in at ${CHAT_BASE2}/dashboard, then re-run.
10173
-
10174
- `
10212
+ "\n No linked web session found on this machine.\n Run `terminalhire link` to connect this terminal to your account, then re-run.\n\n"
10175
10213
  );
10176
10214
  return "not-linked";
10177
10215
  case "expired":
@@ -10377,8 +10415,8 @@ var init_jpi_chat_read = __esm({
10377
10415
  init_chat_client();
10378
10416
  init_jpi_chat();
10379
10417
  CHAT_BASE2 = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
10380
- TERMINALHIRE_DIR12 = join14(homedir13(), ".terminalhire");
10381
- READS_FILE = join14(TERMINALHIRE_DIR12, "chat-reads.json");
10418
+ TERMINALHIRE_DIR12 = join15(homedir14(), ".terminalhire");
10419
+ READS_FILE = join15(TERMINALHIRE_DIR12, "chat-reads.json");
10382
10420
  }
10383
10421
  });
10384
10422
 
@@ -10442,8 +10480,7 @@ async function ensureChatDisclosure(opts = {}) {
10442
10480
  return { shown: true, acknowledged: true };
10443
10481
  }
10444
10482
  function defaultSessionCookie() {
10445
- const v = process.env["TERMINALHIRE_WEB_SESSION"];
10446
- return typeof v === "string" && v.length > 0 ? v : null;
10483
+ return readWebSessionCookie();
10447
10484
  }
10448
10485
  async function fetchIntroList(deps = {}) {
10449
10486
  const fetchImpl = deps.fetchImpl ?? ((...a) => globalThis.fetch(...a));
@@ -10542,11 +10579,7 @@ async function runChatPane(opts = {}) {
10542
10579
  const resolved = await resolveConnection(target);
10543
10580
  if (resolved.status === "not-linked") {
10544
10581
  output.write(
10545
- `
10546
- No linked web session found on this machine.
10547
- Sign in at ${CHAT_BASE3}/dashboard, then re-run.
10548
-
10549
- `
10582
+ "\n No linked web session found on this machine.\n Run `terminalhire link` to connect this terminal to your account, then re-run.\n\n"
10550
10583
  );
10551
10584
  return { entered: false, reason: "not-linked" };
10552
10585
  }
@@ -11006,6 +11039,7 @@ var init_jpi_chat = __esm({
11006
11039
  "use strict";
11007
11040
  init_chat_client();
11008
11041
  init_config();
11042
+ init_web_session();
11009
11043
  CHAT_BASE3 = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
11010
11044
  GH_SESSION_COOKIE4 = "__jpi_gh_session";
11011
11045
  HIDE_CURSOR = "\x1B[?25l";
@@ -11030,10 +11064,210 @@ var init_jpi_chat = __esm({
11030
11064
  }
11031
11065
  });
11032
11066
 
11067
+ // src/link.ts
11068
+ var link_exports = {};
11069
+ __export(link_exports, {
11070
+ resolveLoopbackRequest: () => resolveLoopbackRequest,
11071
+ runLink: () => runLink,
11072
+ runLinkLogout: () => runLinkLogout
11073
+ });
11074
+ import { createServer } from "http";
11075
+ import { randomBytes as randomBytes5 } from "crypto";
11076
+ function resolveLoopbackRequest(rawUrl, expectedNonce) {
11077
+ let u;
11078
+ try {
11079
+ u = new URL(rawUrl, "http://127.0.0.1");
11080
+ } catch {
11081
+ return { ok: false, reason: "bad_url" };
11082
+ }
11083
+ const nonce = u.searchParams.get("nonce");
11084
+ if (!nonce || nonce !== expectedNonce) return { ok: false, reason: "nonce_mismatch" };
11085
+ const token = u.searchParams.get("token");
11086
+ if (!token) return { ok: false, reason: "missing_token" };
11087
+ return { ok: true, token };
11088
+ }
11089
+ function defaultStartLoopback(expectedNonce, timeoutMs) {
11090
+ return new Promise((resolveHandle) => {
11091
+ let settle;
11092
+ const result = new Promise((res) => {
11093
+ settle = res;
11094
+ });
11095
+ let done = false;
11096
+ const finish = (r) => {
11097
+ if (done) return;
11098
+ done = true;
11099
+ clearTimeout(timer);
11100
+ settle(r);
11101
+ setImmediate(() => {
11102
+ try {
11103
+ server.close();
11104
+ } catch {
11105
+ }
11106
+ });
11107
+ };
11108
+ const server = createServer((req, res) => {
11109
+ const outcome = resolveLoopbackRequest(req.url ?? "", expectedNonce);
11110
+ res.writeHead(outcome.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
11111
+ res.end(outcome.ok ? LINKED_HTML : FAILED_HTML);
11112
+ finish(outcome);
11113
+ });
11114
+ const timer = setTimeout(() => finish({ ok: false, reason: "timeout" }), timeoutMs);
11115
+ if (typeof timer.unref === "function") timer.unref();
11116
+ server.on("error", () => finish({ ok: false, reason: "listen_error" }));
11117
+ server.listen(0, "127.0.0.1", () => {
11118
+ const addr = server.address();
11119
+ const port = typeof addr === "object" && addr ? addr.port : 0;
11120
+ resolveHandle({
11121
+ port,
11122
+ result,
11123
+ close: () => {
11124
+ try {
11125
+ server.close();
11126
+ } catch {
11127
+ }
11128
+ }
11129
+ });
11130
+ });
11131
+ });
11132
+ }
11133
+ function defaultLinkDeps() {
11134
+ return {
11135
+ startLoopback: defaultStartLoopback,
11136
+ openBrowser: (url) => {
11137
+ void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
11138
+ });
11139
+ },
11140
+ generateNonce: () => randomBytes5(16).toString("hex"),
11141
+ persistToken: (token) => writeWebSessionFile(token),
11142
+ log: (msg) => console.log(msg),
11143
+ errorLog: (msg) => console.error(msg),
11144
+ exit: (code) => process.exit(code)
11145
+ };
11146
+ }
11147
+ async function runLink(overrides) {
11148
+ const deps = { ...defaultLinkDeps(), ...overrides };
11149
+ const nonce = deps.generateNonce();
11150
+ const handle = await deps.startLoopback(nonce, LINK_TIMEOUT_MS);
11151
+ const url = `${LINK_BASE3}/api/auth/link?port=${handle.port}&nonce=${encodeURIComponent(nonce)}`;
11152
+ deps.log("");
11153
+ deps.log(" terminalhire \u2014 link this terminal to your account");
11154
+ 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");
11155
+ deps.log(" Opening your browser to approve. If it does not open, paste this URL:");
11156
+ deps.log(` \u2192 ${url}`);
11157
+ deps.log(" Waiting for approval (this tab closes itself once you approve)\u2026");
11158
+ deps.openBrowser(url);
11159
+ let outcome;
11160
+ try {
11161
+ outcome = await handle.result;
11162
+ } finally {
11163
+ handle.close();
11164
+ }
11165
+ if (!outcome.ok || !outcome.token) {
11166
+ if (outcome.reason === "timeout") {
11167
+ deps.errorLog("\n Link timed out \u2014 run `terminalhire link` again.\n");
11168
+ } else if (outcome.reason === "nonce_mismatch") {
11169
+ deps.errorLog("\n Link rejected (nonce did not match) \u2014 run `terminalhire link` again.\n");
11170
+ } else {
11171
+ deps.errorLog("\n Link failed \u2014 run `terminalhire link` again.\n");
11172
+ }
11173
+ deps.exit(1);
11174
+ return;
11175
+ }
11176
+ deps.persistToken(outcome.token);
11177
+ deps.log("\n This terminal is now linked to your terminalhire account.");
11178
+ deps.log(" Try `terminalhire intro <login>`, `terminalhire chat`, or `terminalhire trajectory --push`.");
11179
+ deps.log(" Unlink any time with `terminalhire link --logout`.\n");
11180
+ deps.exit(0);
11181
+ }
11182
+ function defaultLinkLogoutDeps() {
11183
+ return {
11184
+ fetchImpl: (...args5) => globalThis.fetch(...args5),
11185
+ readSessionFile: () => readWebSessionFile(),
11186
+ clearSessionFile: () => clearWebSessionFile(),
11187
+ log: (msg) => console.log(msg),
11188
+ errorLog: (msg) => console.error(msg),
11189
+ exit: (code) => process.exit(code)
11190
+ };
11191
+ }
11192
+ async function runLinkLogout(overrides) {
11193
+ const deps = { ...defaultLinkLogoutDeps(), ...overrides };
11194
+ const token = deps.readSessionFile();
11195
+ if (!token) {
11196
+ deps.log("\n No linked web session on this machine \u2014 nothing to unlink.\n");
11197
+ deps.exit(0);
11198
+ return;
11199
+ }
11200
+ let revoked = false;
11201
+ try {
11202
+ const res = await deps.fetchImpl(`${LINK_BASE3}/api/auth/session`, {
11203
+ method: "DELETE",
11204
+ headers: { Cookie: `${GH_SESSION_COOKIE5}=${token}` },
11205
+ signal: AbortSignal.timeout(1e4)
11206
+ });
11207
+ revoked = res.ok;
11208
+ } catch {
11209
+ }
11210
+ deps.clearSessionFile();
11211
+ if (revoked) {
11212
+ deps.log("\n Unlinked \u2014 the session was revoked server-side and removed from this machine.\n");
11213
+ } else {
11214
+ deps.log("\n Removed the local session from this machine.");
11215
+ deps.log(" (Could not reach the server to revoke it \u2014 it expires on its own.)\n");
11216
+ }
11217
+ deps.exit(0);
11218
+ }
11219
+ var LINK_BASE3, GH_SESSION_COOKIE5, LINK_TIMEOUT_MS, LINKED_HTML, FAILED_HTML;
11220
+ var init_link = __esm({
11221
+ "src/link.ts"() {
11222
+ "use strict";
11223
+ init_web_session();
11224
+ LINK_BASE3 = "https://www.terminalhire.com";
11225
+ GH_SESSION_COOKIE5 = "__jpi_gh_session";
11226
+ LINK_TIMEOUT_MS = 12e4;
11227
+ LINKED_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>terminalhire</title></head>
11228
+ <body style="font-family:system-ui;padding:2rem;background:#0b0d10;color:#e6e6e6">
11229
+ <script>history.replaceState({},'','/');</script>
11230
+ <p>CLI linked \u2014 you can close this tab.</p>
11231
+ <script>setTimeout(function(){window.close();},400);</script>
11232
+ </body></html>`;
11233
+ FAILED_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>terminalhire</title></head>
11234
+ <body style="font-family:system-ui;padding:2rem;background:#0b0d10;color:#e6e6e6">
11235
+ <script>history.replaceState({},'','/');</script>
11236
+ <p>Link failed \u2014 return to your terminal and run <code>terminalhire link</code> again.</p>
11237
+ </body></html>`;
11238
+ }
11239
+ });
11240
+
11241
+ // bin/jpi-link.js
11242
+ var jpi_link_exports = {};
11243
+ __export(jpi_link_exports, {
11244
+ run: () => run10
11245
+ });
11246
+ async function run10() {
11247
+ try {
11248
+ const args5 = process.argv.slice(2);
11249
+ if (args5.includes("--logout")) {
11250
+ const { runLinkLogout: runLinkLogout2 } = await Promise.resolve().then(() => (init_link(), link_exports));
11251
+ await runLinkLogout2();
11252
+ return;
11253
+ }
11254
+ const { runLink: runLink2 } = await Promise.resolve().then(() => (init_link(), link_exports));
11255
+ await runLink2();
11256
+ } catch (err) {
11257
+ console.error("terminalhire link error:", err?.message ?? err);
11258
+ process.exit(1);
11259
+ }
11260
+ }
11261
+ var init_jpi_link = __esm({
11262
+ "bin/jpi-link.js"() {
11263
+ "use strict";
11264
+ }
11265
+ });
11266
+
11033
11267
  // bin/jpi-profile.js
11034
11268
  var jpi_profile_exports = {};
11035
11269
  __export(jpi_profile_exports, {
11036
- run: () => run10
11270
+ run: () => run11
11037
11271
  });
11038
11272
  import { createInterface as createInterface8 } from "readline";
11039
11273
  function prompt4(question) {
@@ -11045,7 +11279,7 @@ function prompt4(question) {
11045
11279
  });
11046
11280
  });
11047
11281
  }
11048
- async function run10() {
11282
+ async function run11() {
11049
11283
  const { readProfile: readProfile2, writeProfile: writeProfile2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
11050
11284
  const args5 = process.argv.slice(2);
11051
11285
  if (args5.includes("--show")) {
@@ -11118,9 +11352,9 @@ var signal_exports = {};
11118
11352
  __export(signal_exports, {
11119
11353
  extractFingerprint: () => extractFingerprint
11120
11354
  });
11121
- import { readFileSync as readFileSync15, readdirSync as readdirSync2 } from "fs";
11355
+ import { readFileSync as readFileSync16, readdirSync as readdirSync2 } from "fs";
11122
11356
  import { execFileSync } from "child_process";
11123
- import { join as join15 } from "path";
11357
+ import { join as join16 } from "path";
11124
11358
  function safeGit(args5, cwd) {
11125
11359
  try {
11126
11360
  return execFileSync("git", ["-C", cwd, ...args5], {
@@ -11148,20 +11382,20 @@ function isEmployerContext(cwd) {
11148
11382
  }
11149
11383
  function readJsonSafe(path) {
11150
11384
  try {
11151
- return JSON.parse(readFileSync15(path, "utf8"));
11385
+ return JSON.parse(readFileSync16(path, "utf8"));
11152
11386
  } catch {
11153
11387
  return null;
11154
11388
  }
11155
11389
  }
11156
11390
  function readFileSafe(path) {
11157
11391
  try {
11158
- return readFileSync15(path, "utf8");
11392
+ return readFileSync16(path, "utf8");
11159
11393
  } catch {
11160
11394
  return "";
11161
11395
  }
11162
11396
  }
11163
11397
  function tokensFromPackageJson(cwd) {
11164
- const pkg = readJsonSafe(join15(cwd, "package.json"));
11398
+ const pkg = readJsonSafe(join16(cwd, "package.json"));
11165
11399
  if (!pkg || typeof pkg !== "object") return [];
11166
11400
  const p = pkg;
11167
11401
  const deps = {
@@ -11175,9 +11409,9 @@ function workspaceMemberDirs(cwd) {
11175
11409
  const dirs = [cwd];
11176
11410
  for (const group of ["apps", "packages"]) {
11177
11411
  try {
11178
- const groupDir = join15(cwd, group);
11412
+ const groupDir = join16(cwd, group);
11179
11413
  for (const e of readdirSync2(groupDir, { withFileTypes: true })) {
11180
- if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join15(groupDir, e.name));
11414
+ if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join16(groupDir, e.name));
11181
11415
  }
11182
11416
  } catch {
11183
11417
  }
@@ -11185,18 +11419,18 @@ function workspaceMemberDirs(cwd) {
11185
11419
  return dirs;
11186
11420
  }
11187
11421
  function tokensFromRequirementsTxt(cwd) {
11188
- const content = readFileSafe(join15(cwd, "requirements.txt"));
11422
+ const content = readFileSafe(join16(cwd, "requirements.txt"));
11189
11423
  if (!content) return [];
11190
11424
  return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
11191
11425
  }
11192
11426
  function tokensFromGoMod(cwd) {
11193
- const content = readFileSafe(join15(cwd, "go.mod"));
11427
+ const content = readFileSafe(join16(cwd, "go.mod"));
11194
11428
  if (!content) return [];
11195
11429
  const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
11196
11430
  return ["go", ...requires];
11197
11431
  }
11198
11432
  function tokensFromCargoToml(cwd) {
11199
- const content = readFileSafe(join15(cwd, "Cargo.toml"));
11433
+ const content = readFileSafe(join16(cwd, "Cargo.toml"));
11200
11434
  if (!content) return [];
11201
11435
  const deps = [];
11202
11436
  let inDeps = false;
@@ -11217,7 +11451,7 @@ function tokensFromFileExtensions(cwd) {
11217
11451
  const tokens = [];
11218
11452
  const scanDirs = [cwd];
11219
11453
  try {
11220
- const srcDir = join15(cwd, "src");
11454
+ const srcDir = join16(cwd, "src");
11221
11455
  readdirSync2(srcDir);
11222
11456
  scanDirs.push(srcDir);
11223
11457
  } catch {
@@ -11378,9 +11612,9 @@ var init_signal = __esm({
11378
11612
  // bin/jpi-learn.js
11379
11613
  var jpi_learn_exports = {};
11380
11614
  __export(jpi_learn_exports, {
11381
- run: () => run11
11615
+ run: () => run12
11382
11616
  });
11383
- async function run11() {
11617
+ async function run12() {
11384
11618
  try {
11385
11619
  const args5 = process.argv.slice(2);
11386
11620
  const cwdIdx = args5.indexOf("--cwd");
@@ -11407,7 +11641,7 @@ var init_jpi_learn = __esm({
11407
11641
  "use strict";
11408
11642
  isMain = process.argv[1]?.endsWith("jpi-learn.js") || process.argv[1]?.endsWith("jpi-learn");
11409
11643
  if (isMain) {
11410
- run11();
11644
+ run12();
11411
11645
  }
11412
11646
  }
11413
11647
  });
@@ -11415,17 +11649,17 @@ var init_jpi_learn = __esm({
11415
11649
  // bin/jpi-config.js
11416
11650
  var jpi_config_exports = {};
11417
11651
  __export(jpi_config_exports, {
11418
- run: () => run12
11652
+ run: () => run13
11419
11653
  });
11420
- import { join as join16 } from "path";
11421
- import { homedir as homedir14 } from "os";
11654
+ import { join as join17 } from "path";
11655
+ import { homedir as homedir15 } from "os";
11422
11656
  function parseNudgeMode(raw) {
11423
11657
  if (raw === "session" || raw === "always") return raw;
11424
11658
  const m = /^every:(\d+)$/.exec(raw);
11425
11659
  if (m && parseInt(m[1], 10) >= 1) return raw;
11426
11660
  return null;
11427
11661
  }
11428
- async function run12() {
11662
+ async function run13() {
11429
11663
  const args5 = process.argv.slice(2);
11430
11664
  const filtered = args5[0] === "config" ? args5.slice(1) : args5;
11431
11665
  if (filtered.includes("--show") || filtered.length === 0) {
@@ -11491,8 +11725,8 @@ var init_jpi_config = __esm({
11491
11725
  "bin/jpi-config.js"() {
11492
11726
  "use strict";
11493
11727
  init_config();
11494
- TERMINALHIRE_DIR13 = join16(homedir14(), ".terminalhire");
11495
- CONFIG_FILE2 = join16(TERMINALHIRE_DIR13, "config.json");
11728
+ TERMINALHIRE_DIR13 = join17(homedir15(), ".terminalhire");
11729
+ CONFIG_FILE2 = join17(TERMINALHIRE_DIR13, "config.json");
11496
11730
  }
11497
11731
  });
11498
11732
 
@@ -11515,25 +11749,25 @@ __export(spinner_exports, {
11515
11749
  readSpinnerConfig: () => readSpinnerConfig
11516
11750
  });
11517
11751
  import {
11518
- readFileSync as readFileSync16,
11519
- writeFileSync as writeFileSync13,
11520
- existsSync as existsSync11,
11521
- mkdirSync as mkdirSync13,
11752
+ readFileSync as readFileSync17,
11753
+ writeFileSync as writeFileSync14,
11754
+ existsSync as existsSync12,
11755
+ mkdirSync as mkdirSync14,
11522
11756
  renameSync as renameSync2
11523
11757
  } from "fs";
11524
- import { join as join17, dirname } from "path";
11525
- import { homedir as homedir15 } from "os";
11758
+ import { join as join18, dirname } from "path";
11759
+ import { homedir as homedir16 } from "os";
11526
11760
  function readJson(path, fallback) {
11527
11761
  try {
11528
- return existsSync11(path) ? JSON.parse(readFileSync16(path, "utf8")) : fallback;
11762
+ return existsSync12(path) ? JSON.parse(readFileSync17(path, "utf8")) : fallback;
11529
11763
  } catch {
11530
11764
  return fallback;
11531
11765
  }
11532
11766
  }
11533
11767
  function atomicWriteJson(path, obj) {
11534
- mkdirSync13(dirname(path), { recursive: true });
11768
+ mkdirSync14(dirname(path), { recursive: true });
11535
11769
  const tmp = `${path}.tmp-${process.pid}`;
11536
- writeFileSync13(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
11770
+ writeFileSync14(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
11537
11771
  renameSync2(tmp, path);
11538
11772
  }
11539
11773
  function titleCase(s) {
@@ -11793,10 +12027,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE3, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
11793
12027
  var init_spinner = __esm({
11794
12028
  "bin/spinner.js"() {
11795
12029
  "use strict";
11796
- TH_DIR = process.env["TERMINALHIRE_DIR"] || join17(homedir15(), ".terminalhire");
11797
- CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join17(homedir15(), ".claude", "settings.json");
11798
- CONFIG_FILE3 = join17(TH_DIR, "config.json");
11799
- SPINNER_STATE_FILE = join17(TH_DIR, "spinner-state.json");
12030
+ TH_DIR = process.env["TERMINALHIRE_DIR"] || join18(homedir16(), ".terminalhire");
12031
+ CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join18(homedir16(), ".claude", "settings.json");
12032
+ CONFIG_FILE3 = join18(TH_DIR, "config.json");
12033
+ SPINNER_STATE_FILE = join18(TH_DIR, "spinner-state.json");
11800
12034
  SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
11801
12035
  VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
11802
12036
  }
@@ -11805,32 +12039,32 @@ var init_spinner = __esm({
11805
12039
  // bin/jpi-spinner.js
11806
12040
  var jpi_spinner_exports = {};
11807
12041
  __export(jpi_spinner_exports, {
11808
- run: () => run13
12042
+ run: () => run14
11809
12043
  });
11810
12044
  import {
11811
- readFileSync as readFileSync17,
11812
- writeFileSync as writeFileSync14,
12045
+ readFileSync as readFileSync18,
12046
+ writeFileSync as writeFileSync15,
11813
12047
  copyFileSync,
11814
- existsSync as existsSync12,
11815
- mkdirSync as mkdirSync14
12048
+ existsSync as existsSync13,
12049
+ mkdirSync as mkdirSync15
11816
12050
  } from "fs";
11817
- import { join as join18 } from "path";
11818
- import { homedir as homedir16 } from "os";
12051
+ import { join as join19 } from "path";
12052
+ import { homedir as homedir17 } from "os";
11819
12053
  import { createInterface as createInterface9 } from "readline";
11820
12054
  function readConfig2() {
11821
12055
  try {
11822
- return existsSync12(CONFIG_FILE4) ? JSON.parse(readFileSync17(CONFIG_FILE4, "utf8")) : {};
12056
+ return existsSync13(CONFIG_FILE4) ? JSON.parse(readFileSync18(CONFIG_FILE4, "utf8")) : {};
11823
12057
  } catch {
11824
12058
  return {};
11825
12059
  }
11826
12060
  }
11827
12061
  function writeConfig2(patch) {
11828
- mkdirSync14(TH_DIR2, { recursive: true });
12062
+ mkdirSync15(TH_DIR2, { recursive: true });
11829
12063
  const merged = { ...readConfig2(), ...patch };
11830
- writeFileSync14(CONFIG_FILE4, JSON.stringify(merged, null, 2) + "\n", "utf8");
12064
+ writeFileSync15(CONFIG_FILE4, JSON.stringify(merged, null, 2) + "\n", "utf8");
11831
12065
  }
11832
12066
  function backupSettings() {
11833
- if (!existsSync12(SETTINGS_PATH)) return null;
12067
+ if (!existsSync13(SETTINGS_PATH)) return null;
11834
12068
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11835
12069
  const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
11836
12070
  copyFileSync(SETTINGS_PATH, backupPath);
@@ -11847,13 +12081,13 @@ function ask(question) {
11847
12081
  }
11848
12082
  function readTopMatches() {
11849
12083
  try {
11850
- const c = JSON.parse(readFileSync17(CACHE_FILE, "utf8"));
12084
+ const c = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
11851
12085
  return Array.isArray(c.topMatches) ? c.topMatches : [];
11852
12086
  } catch {
11853
12087
  return [];
11854
12088
  }
11855
12089
  }
11856
- async function run13() {
12090
+ async function run14() {
11857
12091
  const args5 = process.argv.slice(2).filter((a) => a !== "spinner");
11858
12092
  const has = (f) => args5.includes(f);
11859
12093
  const val = (f) => {
@@ -11990,21 +12224,21 @@ var init_jpi_spinner = __esm({
11990
12224
  "bin/jpi-spinner.js"() {
11991
12225
  "use strict";
11992
12226
  init_spinner();
11993
- TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join18(homedir16(), ".terminalhire");
11994
- CONFIG_FILE4 = join18(TH_DIR2, "config.json");
11995
- SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join18(homedir16(), ".claude", "settings.json");
11996
- CACHE_FILE = join18(TH_DIR2, "index-cache.json");
12227
+ TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join19(homedir17(), ".terminalhire");
12228
+ CONFIG_FILE4 = join19(TH_DIR2, "config.json");
12229
+ SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join19(homedir17(), ".claude", "settings.json");
12230
+ CACHE_FILE = join19(TH_DIR2, "index-cache.json");
11997
12231
  }
11998
12232
  });
11999
12233
 
12000
12234
  // bin/jpi-sync.js
12001
12235
  var jpi_sync_exports = {};
12002
12236
  __export(jpi_sync_exports, {
12003
- run: () => run14
12237
+ run: () => run15
12004
12238
  });
12005
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync15, mkdirSync as mkdirSync15, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
12006
- import { join as join19 } from "path";
12007
- import { homedir as homedir17, hostname as osHostname } from "os";
12239
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync16, mkdirSync as mkdirSync16, existsSync as existsSync14, rmSync as rmSync4 } from "fs";
12240
+ import { join as join20 } from "path";
12241
+ import { homedir as homedir18, hostname as osHostname } from "os";
12008
12242
  import { createInterface as createInterface10 } from "readline";
12009
12243
  function ask2(question) {
12010
12244
  const rl = createInterface10({ input: process.stdin, output: process.stdout });
@@ -12017,18 +12251,18 @@ function ask2(question) {
12017
12251
  }
12018
12252
  function readMarker() {
12019
12253
  try {
12020
- return existsSync13(TIER1_MARKER) ? JSON.parse(readFileSync18(TIER1_MARKER, "utf8")) : null;
12254
+ return existsSync14(TIER1_MARKER) ? JSON.parse(readFileSync19(TIER1_MARKER, "utf8")) : null;
12021
12255
  } catch {
12022
12256
  return null;
12023
12257
  }
12024
12258
  }
12025
12259
  function writeMarker(marker) {
12026
- mkdirSync15(TH_DIR3, { recursive: true });
12027
- writeFileSync15(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
12260
+ mkdirSync16(TH_DIR3, { recursive: true });
12261
+ writeFileSync16(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
12028
12262
  }
12029
12263
  function clearMarker() {
12030
12264
  try {
12031
- rmSync3(TIER1_MARKER);
12265
+ rmSync4(TIER1_MARKER);
12032
12266
  } catch {
12033
12267
  }
12034
12268
  }
@@ -12312,7 +12546,7 @@ async function runDelete() {
12312
12546
  clearMarker();
12313
12547
  console.log("\n Synced profile deleted and local marker cleared.\n");
12314
12548
  }
12315
- async function run14() {
12549
+ async function run15() {
12316
12550
  const args5 = process.argv.slice(2).filter((a) => a !== "sync");
12317
12551
  const has = (f) => args5.includes(f);
12318
12552
  if (has("--push") || has("--enable")) {
@@ -12343,8 +12577,8 @@ var init_jpi_sync = __esm({
12343
12577
  "bin/jpi-sync.js"() {
12344
12578
  "use strict";
12345
12579
  init_open_url();
12346
- TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join19(homedir17(), ".terminalhire");
12347
- TIER1_MARKER = join19(TH_DIR3, "tier1.json");
12580
+ TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join20(homedir18(), ".terminalhire");
12581
+ TIER1_MARKER = join20(TH_DIR3, "tier1.json");
12348
12582
  API_URL5 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
12349
12583
  SYNC_BASE = "https://www.terminalhire.com";
12350
12584
  POLL_INTERVAL_MS = 2e3;
@@ -12356,14 +12590,14 @@ var init_jpi_sync = __esm({
12356
12590
  // bin/jpi-init.js
12357
12591
  var jpi_init_exports = {};
12358
12592
  __export(jpi_init_exports, {
12359
- run: () => run15
12593
+ run: () => run16
12360
12594
  });
12361
- import { existsSync as existsSync14 } from "fs";
12362
- import { join as join20, resolve } from "path";
12595
+ import { existsSync as existsSync15 } from "fs";
12596
+ import { join as join21, resolve } from "path";
12363
12597
  import { fileURLToPath as fileURLToPath3 } from "url";
12364
12598
  import { createInterface as createInterface11 } from "readline";
12365
12599
  import { spawnSync, spawn as spawn2 } from "child_process";
12366
- import { homedir as homedir18 } from "os";
12600
+ import { homedir as homedir19 } from "os";
12367
12601
  function ask3(question) {
12368
12602
  const rl = createInterface11({ input: process.stdin, output: process.stdout });
12369
12603
  return new Promise((resolve2) => {
@@ -12374,18 +12608,18 @@ function ask3(question) {
12374
12608
  });
12375
12609
  }
12376
12610
  function resolveScript(name) {
12377
- const distPath = resolve(join20(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
12378
- const legacyPath = resolve(join20(__dirname2, `${name}.js`));
12379
- return existsSync14(distPath) ? distPath : legacyPath;
12611
+ const distPath = resolve(join21(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
12612
+ const legacyPath = resolve(join21(__dirname2, `${name}.js`));
12613
+ return existsSync15(distPath) ? distPath : legacyPath;
12380
12614
  }
12381
12615
  function resolveInstallJs() {
12382
- const fromDist = resolve(join20(__dirname2, "..", "..", "install.js"));
12383
- const fromBin = resolve(join20(__dirname2, "..", "install.js"));
12384
- if (existsSync14(fromDist)) return fromDist;
12385
- if (existsSync14(fromBin)) return fromBin;
12616
+ const fromDist = resolve(join21(__dirname2, "..", "..", "install.js"));
12617
+ const fromBin = resolve(join21(__dirname2, "..", "install.js"));
12618
+ if (existsSync15(fromDist)) return fromDist;
12619
+ if (existsSync15(fromBin)) return fromBin;
12386
12620
  return fromBin;
12387
12621
  }
12388
- async function run15() {
12622
+ async function run16() {
12389
12623
  console.log("");
12390
12624
  console.log("\u250C\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\u2510");
12391
12625
  console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
@@ -12488,13 +12722,13 @@ var init_jpi_init = __esm({
12488
12722
  // bin/jpi-refresh.js
12489
12723
  var jpi_refresh_exports = {};
12490
12724
  __export(jpi_refresh_exports, {
12491
- run: () => run16
12725
+ run: () => run17
12492
12726
  });
12493
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync16, existsSync as existsSync15, mkdirSync as mkdirSync16 } from "fs";
12494
- import { join as join21 } from "path";
12495
- import { homedir as homedir19 } from "os";
12727
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync17, existsSync as existsSync16, mkdirSync as mkdirSync17 } from "fs";
12728
+ import { join as join22 } from "path";
12729
+ import { homedir as homedir20 } from "os";
12496
12730
  import { fileURLToPath as fileURLToPath4 } from "url";
12497
- async function run16() {
12731
+ async function run17() {
12498
12732
  try {
12499
12733
  let index;
12500
12734
  try {
@@ -12584,7 +12818,7 @@ async function run16() {
12584
12818
  }
12585
12819
  } catch {
12586
12820
  }
12587
- mkdirSync16(TERMINALHIRE_DIR14, { recursive: true });
12821
+ mkdirSync17(TERMINALHIRE_DIR14, { recursive: true });
12588
12822
  const cacheEntry = {
12589
12823
  ts: Date.now(),
12590
12824
  index,
@@ -12592,7 +12826,7 @@ async function run16() {
12592
12826
  topMatches,
12593
12827
  topPeers
12594
12828
  };
12595
- writeFileSync16(INDEX_CACHE_FILE4, JSON.stringify(cacheEntry), "utf8");
12829
+ writeFileSync17(INDEX_CACHE_FILE4, JSON.stringify(cacheEntry), "utf8");
12596
12830
  try {
12597
12831
  const {
12598
12832
  readSpinnerConfig: readSpinnerConfig2,
@@ -12643,8 +12877,8 @@ var init_jpi_refresh = __esm({
12643
12877
  init_directory2();
12644
12878
  init_config();
12645
12879
  __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
12646
- TERMINALHIRE_DIR14 = join21(homedir19(), ".terminalhire");
12647
- INDEX_CACHE_FILE4 = join21(TERMINALHIRE_DIR14, "index-cache.json");
12880
+ TERMINALHIRE_DIR14 = join22(homedir20(), ".terminalhire");
12881
+ INDEX_CACHE_FILE4 = join22(TERMINALHIRE_DIR14, "index-cache.json");
12648
12882
  API_URL6 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
12649
12883
  }
12650
12884
  });
@@ -12652,16 +12886,16 @@ var init_jpi_refresh = __esm({
12652
12886
  // bin/jpi-save.js
12653
12887
  var jpi_save_exports = {};
12654
12888
  __export(jpi_save_exports, {
12655
- run: () => run17
12889
+ run: () => run18
12656
12890
  });
12657
- import { readFileSync as readFileSync20, existsSync as existsSync16 } from "fs";
12658
- import { join as join22 } from "path";
12659
- import { homedir as homedir20 } from "os";
12891
+ import { readFileSync as readFileSync21, existsSync as existsSync17 } from "fs";
12892
+ import { join as join23 } from "path";
12893
+ import { homedir as homedir21 } from "os";
12660
12894
  import { fileURLToPath as fileURLToPath5 } from "url";
12661
12895
  function findJobInCache(jobId) {
12662
12896
  try {
12663
- if (!existsSync16(INDEX_CACHE_FILE5)) return null;
12664
- const raw = readFileSync20(INDEX_CACHE_FILE5, "utf8");
12897
+ if (!existsSync17(INDEX_CACHE_FILE5)) return null;
12898
+ const raw = readFileSync21(INDEX_CACHE_FILE5, "utf8");
12665
12899
  const entry = JSON.parse(raw);
12666
12900
  const jobs = entry?.index?.jobs ?? [];
12667
12901
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -12730,7 +12964,7 @@ async function cmdUnsave(jobId) {
12730
12964
  process.exit(1);
12731
12965
  }
12732
12966
  }
12733
- async function run17() {
12967
+ async function run18() {
12734
12968
  const verb = process.argv[2];
12735
12969
  const jobId = process.argv[3];
12736
12970
  try {
@@ -12754,26 +12988,26 @@ var init_jpi_save = __esm({
12754
12988
  "bin/jpi-save.js"() {
12755
12989
  "use strict";
12756
12990
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
12757
- TERMINALHIRE_DIR15 = join22(homedir20(), ".terminalhire");
12758
- INDEX_CACHE_FILE5 = join22(TERMINALHIRE_DIR15, "index-cache.json");
12991
+ TERMINALHIRE_DIR15 = join23(homedir21(), ".terminalhire");
12992
+ INDEX_CACHE_FILE5 = join23(TERMINALHIRE_DIR15, "index-cache.json");
12759
12993
  }
12760
12994
  });
12761
12995
 
12762
12996
  // bin/jpi-dispatch.js
12763
12997
  import { fileURLToPath as fileURLToPath6 } from "url";
12764
- import { join as join23, dirname as dirname2 } from "path";
12765
- import { existsSync as existsSync17, readFileSync as readFileSync21 } from "fs";
12998
+ import { join as join24, dirname as dirname2 } from "path";
12999
+ import { existsSync as existsSync18, readFileSync as readFileSync22 } from "fs";
12766
13000
  import { createRequire } from "module";
12767
13001
  var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
12768
13002
  function readPackageVersion() {
12769
13003
  try {
12770
13004
  const candidates = [
12771
- join23(__dirname5, "..", "..", "package.json"),
12772
- join23(__dirname5, "..", "package.json")
13005
+ join24(__dirname5, "..", "..", "package.json"),
13006
+ join24(__dirname5, "..", "package.json")
12773
13007
  ];
12774
13008
  for (const p of candidates) {
12775
- if (existsSync17(p)) {
12776
- const pkg = JSON.parse(readFileSync21(p, "utf8"));
13009
+ if (existsSync18(p)) {
13010
+ const pkg = JSON.parse(readFileSync22(p, "utf8"));
12777
13011
  if (pkg.version) return pkg.version;
12778
13012
  }
12779
13013
  }
@@ -12784,7 +13018,7 @@ function readPackageVersion() {
12784
13018
  var firstArg = process.argv[2];
12785
13019
  if (!firstArg && !process.stdin.isTTY) {
12786
13020
  const { default: childProcess } = await import("child_process");
12787
- const nudgeScript = join23(__dirname5, "jpi.js");
13021
+ const nudgeScript = join24(__dirname5, "jpi.js");
12788
13022
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
12789
13023
  stdio: ["inherit", "inherit", "inherit"]
12790
13024
  });
@@ -12820,6 +13054,8 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
12820
13054
  console.log(" terminalhire chat <github-login> --read Read a thread inline (last 8; -n N / --all for depth)");
12821
13055
  console.log(' terminalhire chat <github-login> --send "\u2026" Send one line to a connection (E2E encrypted)');
12822
13056
  console.log(" terminalhire chat <github-login> Open the live E2E chat pane with an accepted connection");
13057
+ console.log(" terminalhire link Connect this terminal to your terminalhire account");
13058
+ console.log(" terminalhire link --logout Revoke + remove this terminal's linked session");
12823
13059
  console.log(" terminalhire profile --show Display your encrypted local profile");
12824
13060
  console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
12825
13061
  console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
@@ -12909,6 +13145,12 @@ if (firstArg === "chat") {
12909
13145
  await mod2.run();
12910
13146
  process.exit(0);
12911
13147
  }
13148
+ if (firstArg === "link") {
13149
+ process.argv.splice(2, 1);
13150
+ const mod2 = await Promise.resolve().then(() => (init_jpi_link(), jpi_link_exports));
13151
+ await mod2.run();
13152
+ process.exit(0);
13153
+ }
12912
13154
  if (firstArg === "profile") {
12913
13155
  const mod2 = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
12914
13156
  await mod2.run();