terminalhire 0.10.7 → 0.10.9

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.
@@ -4269,6 +4269,31 @@ var init_jpi_chat = __esm({
4269
4269
  import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4270
4270
  import { homedir as homedir6 } from "os";
4271
4271
  import { join as join7 } from "path";
4272
+ async function syncUnreadBadge(deps = {}) {
4273
+ const readCookie = deps.readCookie ?? readWebSessionCookie;
4274
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
4275
+ const cacheFile = deps.cacheFile ?? INDEX_CACHE_FILE;
4276
+ try {
4277
+ const cookie = readCookie();
4278
+ if (!cookie || !existsSync6(cacheFile)) return;
4279
+ const res = await fetchImpl(`${CHAT_BASE3}/api/chat/inbox`, {
4280
+ method: "GET",
4281
+ headers: { Cookie: `${GH_SESSION_COOKIE3}=${cookie}` },
4282
+ signal: AbortSignal.timeout(2500)
4283
+ });
4284
+ if (!res.ok) return;
4285
+ const body = await res.json();
4286
+ const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
4287
+ const total = inbox.reduce(
4288
+ (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
4289
+ 0
4290
+ );
4291
+ const entry = JSON.parse(readFileSync7(cacheFile, "utf8"));
4292
+ entry.unreadChat = { count: total };
4293
+ writeFileSync6(cacheFile, JSON.stringify(entry), "utf8");
4294
+ } catch {
4295
+ }
4296
+ }
4272
4297
  function readReadCursors() {
4273
4298
  try {
4274
4299
  if (!existsSync6(READS_FILE)) return {};
@@ -4486,6 +4511,7 @@ async function runReadThread(opts = {}) {
4486
4511
  resolveConnection = defaultResolveConnection,
4487
4512
  writeCursor = writeReadCursor,
4488
4513
  syncCursor = postReadCursor,
4514
+ syncBadge = syncUnreadBadge,
4489
4515
  ensureDisclosure = ensureChatDisclosure
4490
4516
  } = opts;
4491
4517
  const target = String(login ?? "").replace(/^@/, "").trim();
@@ -4532,6 +4558,7 @@ async function runReadThread(opts = {}) {
4532
4558
  } catch {
4533
4559
  }
4534
4560
  await syncCursor(peerLogin, newest.createdAt);
4561
+ await syncBadge();
4535
4562
  }
4536
4563
  }
4537
4564
  return { ok: true, shown: shownMessages.length, total };
@@ -4579,7 +4606,7 @@ async function runSend(opts = {}) {
4579
4606
  );
4580
4607
  return { ok: true };
4581
4608
  }
4582
- var CHAT_BASE3, GH_SESSION_COOKIE3, TERMINALHIRE_DIR5, READS_FILE;
4609
+ var CHAT_BASE3, GH_SESSION_COOKIE3, TERMINALHIRE_DIR5, READS_FILE, INDEX_CACHE_FILE;
4583
4610
  var init_jpi_chat_read = __esm({
4584
4611
  "bin/jpi-chat-read.js"() {
4585
4612
  init_chat_client();
@@ -4589,6 +4616,7 @@ var init_jpi_chat_read = __esm({
4589
4616
  GH_SESSION_COOKIE3 = "__jpi_gh_session";
4590
4617
  TERMINALHIRE_DIR5 = join7(homedir6(), ".terminalhire");
4591
4618
  READS_FILE = join7(TERMINALHIRE_DIR5, "chat-reads.json");
4619
+ INDEX_CACHE_FILE = join7(TERMINALHIRE_DIR5, "index-cache.json");
4592
4620
  }
4593
4621
  });
4594
4622
  init_jpi_chat_read();
@@ -4602,6 +4630,7 @@ export {
4602
4630
  runInbox,
4603
4631
  runReadThread,
4604
4632
  runSend,
4633
+ syncUnreadBadge,
4605
4634
  writeReadCursor
4606
4635
  };
4607
4636
  /*! Bundled license information:
@@ -4174,11 +4174,37 @@ __export(jpi_chat_read_exports, {
4174
4174
  runInbox: () => runInbox,
4175
4175
  runReadThread: () => runReadThread,
4176
4176
  runSend: () => runSend,
4177
+ syncUnreadBadge: () => syncUnreadBadge,
4177
4178
  writeReadCursor: () => writeReadCursor
4178
4179
  });
4179
4180
  import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4180
4181
  import { homedir as homedir6 } from "os";
4181
4182
  import { join as join7 } from "path";
4183
+ async function syncUnreadBadge(deps = {}) {
4184
+ const readCookie = deps.readCookie ?? readWebSessionCookie;
4185
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
4186
+ const cacheFile = deps.cacheFile ?? INDEX_CACHE_FILE;
4187
+ try {
4188
+ const cookie = readCookie();
4189
+ if (!cookie || !existsSync6(cacheFile)) return;
4190
+ const res = await fetchImpl(`${CHAT_BASE2}/api/chat/inbox`, {
4191
+ method: "GET",
4192
+ headers: { Cookie: `${GH_SESSION_COOKIE2}=${cookie}` },
4193
+ signal: AbortSignal.timeout(2500)
4194
+ });
4195
+ if (!res.ok) return;
4196
+ const body = await res.json();
4197
+ const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
4198
+ const total = inbox.reduce(
4199
+ (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
4200
+ 0
4201
+ );
4202
+ const entry = JSON.parse(readFileSync7(cacheFile, "utf8"));
4203
+ entry.unreadChat = { count: total };
4204
+ writeFileSync6(cacheFile, JSON.stringify(entry), "utf8");
4205
+ } catch {
4206
+ }
4207
+ }
4182
4208
  function readReadCursors() {
4183
4209
  try {
4184
4210
  if (!existsSync6(READS_FILE)) return {};
@@ -4396,6 +4422,7 @@ async function runReadThread(opts = {}) {
4396
4422
  resolveConnection = defaultResolveConnection,
4397
4423
  writeCursor = writeReadCursor,
4398
4424
  syncCursor = postReadCursor,
4425
+ syncBadge = syncUnreadBadge,
4399
4426
  ensureDisclosure = ensureChatDisclosure
4400
4427
  } = opts;
4401
4428
  const target = String(login ?? "").replace(/^@/, "").trim();
@@ -4442,6 +4469,7 @@ async function runReadThread(opts = {}) {
4442
4469
  } catch {
4443
4470
  }
4444
4471
  await syncCursor(peerLogin, newest.createdAt);
4472
+ await syncBadge();
4445
4473
  }
4446
4474
  }
4447
4475
  return { ok: true, shown: shownMessages.length, total };
@@ -4489,7 +4517,7 @@ async function runSend(opts = {}) {
4489
4517
  );
4490
4518
  return { ok: true };
4491
4519
  }
4492
- var CHAT_BASE2, GH_SESSION_COOKIE2, TERMINALHIRE_DIR5, READS_FILE;
4520
+ var CHAT_BASE2, GH_SESSION_COOKIE2, TERMINALHIRE_DIR5, READS_FILE, INDEX_CACHE_FILE;
4493
4521
  var init_jpi_chat_read = __esm({
4494
4522
  "bin/jpi-chat-read.js"() {
4495
4523
  "use strict";
@@ -4500,6 +4528,7 @@ var init_jpi_chat_read = __esm({
4500
4528
  GH_SESSION_COOKIE2 = "__jpi_gh_session";
4501
4529
  TERMINALHIRE_DIR5 = join7(homedir6(), ".terminalhire");
4502
4530
  READS_FILE = join7(TERMINALHIRE_DIR5, "chat-reads.json");
4531
+ INDEX_CACHE_FILE = join7(TERMINALHIRE_DIR5, "index-cache.json");
4503
4532
  }
4504
4533
  });
4505
4534
 
@@ -10296,11 +10296,37 @@ __export(jpi_chat_read_exports, {
10296
10296
  runInbox: () => runInbox,
10297
10297
  runReadThread: () => runReadThread,
10298
10298
  runSend: () => runSend,
10299
+ syncUnreadBadge: () => syncUnreadBadge,
10299
10300
  writeReadCursor: () => writeReadCursor
10300
10301
  });
10301
10302
  import { existsSync as existsSync12, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
10302
10303
  import { homedir as homedir15 } from "os";
10303
10304
  import { join as join16 } from "path";
10305
+ async function syncUnreadBadge(deps = {}) {
10306
+ const readCookie = deps.readCookie ?? readWebSessionCookie;
10307
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
10308
+ const cacheFile = deps.cacheFile ?? INDEX_CACHE_FILE5;
10309
+ try {
10310
+ const cookie = readCookie();
10311
+ if (!cookie || !existsSync12(cacheFile)) return;
10312
+ const res = await fetchImpl(`${CHAT_BASE2}/api/chat/inbox`, {
10313
+ method: "GET",
10314
+ headers: { Cookie: `${GH_SESSION_COOKIE4}=${cookie}` },
10315
+ signal: AbortSignal.timeout(2500)
10316
+ });
10317
+ if (!res.ok) return;
10318
+ const body = await res.json();
10319
+ const inbox = Array.isArray(body?.inbox) ? body.inbox : [];
10320
+ const total = inbox.reduce(
10321
+ (sum, it) => sum + (it && typeof it.unreadCount === "number" && it.unreadCount > 0 ? it.unreadCount : 0),
10322
+ 0
10323
+ );
10324
+ const entry = JSON.parse(readFileSync16(cacheFile, "utf8"));
10325
+ entry.unreadChat = { count: total };
10326
+ writeFileSync13(cacheFile, JSON.stringify(entry), "utf8");
10327
+ } catch {
10328
+ }
10329
+ }
10304
10330
  function readReadCursors() {
10305
10331
  try {
10306
10332
  if (!existsSync12(READS_FILE)) return {};
@@ -10518,6 +10544,7 @@ async function runReadThread(opts = {}) {
10518
10544
  resolveConnection = defaultResolveConnection,
10519
10545
  writeCursor = writeReadCursor,
10520
10546
  syncCursor = postReadCursor,
10547
+ syncBadge = syncUnreadBadge,
10521
10548
  ensureDisclosure = ensureChatDisclosure
10522
10549
  } = opts;
10523
10550
  const target = String(login ?? "").replace(/^@/, "").trim();
@@ -10564,6 +10591,7 @@ async function runReadThread(opts = {}) {
10564
10591
  } catch {
10565
10592
  }
10566
10593
  await syncCursor(peerLogin, newest.createdAt);
10594
+ await syncBadge();
10567
10595
  }
10568
10596
  }
10569
10597
  return { ok: true, shown: shownMessages.length, total };
@@ -10611,7 +10639,7 @@ async function runSend(opts = {}) {
10611
10639
  );
10612
10640
  return { ok: true };
10613
10641
  }
10614
- var CHAT_BASE2, GH_SESSION_COOKIE4, TERMINALHIRE_DIR12, READS_FILE;
10642
+ var CHAT_BASE2, GH_SESSION_COOKIE4, TERMINALHIRE_DIR12, READS_FILE, INDEX_CACHE_FILE5;
10615
10643
  var init_jpi_chat_read = __esm({
10616
10644
  "bin/jpi-chat-read.js"() {
10617
10645
  "use strict";
@@ -10622,6 +10650,7 @@ var init_jpi_chat_read = __esm({
10622
10650
  GH_SESSION_COOKIE4 = "__jpi_gh_session";
10623
10651
  TERMINALHIRE_DIR12 = join16(homedir15(), ".terminalhire");
10624
10652
  READS_FILE = join16(TERMINALHIRE_DIR12, "chat-reads.json");
10653
+ INDEX_CACHE_FILE5 = join16(TERMINALHIRE_DIR12, "index-cache.json");
10625
10654
  }
10626
10655
  });
10627
10656
 
@@ -13090,6 +13119,8 @@ async function run18() {
13090
13119
  } catch {
13091
13120
  }
13092
13121
  let incomingPending = { count: 0 };
13122
+ let sessionStale = false;
13123
+ const sessionExpired = (res) => res.status === 401;
13093
13124
  const sessionCookie = readWebSessionFile();
13094
13125
  if (sessionCookie && !isInboundNudgeMuted()) try {
13095
13126
  const res = await fetch(`${API_URL6}/api/intro/list`, {
@@ -13102,6 +13133,8 @@ async function run18() {
13102
13133
  const intros = Array.isArray(body?.intros) ? body.intros : [];
13103
13134
  const incoming = intros.filter((it) => it && it.role === "incoming" && it.status === "pending");
13104
13135
  incomingPending = { count: incoming.length };
13136
+ } else if (sessionExpired(res)) {
13137
+ sessionStale = true;
13105
13138
  }
13106
13139
  } catch {
13107
13140
  }
@@ -13120,6 +13153,8 @@ async function run18() {
13120
13153
  0
13121
13154
  );
13122
13155
  unreadChat = { count: total };
13156
+ } else if (sessionExpired(res)) {
13157
+ sessionStale = true;
13123
13158
  }
13124
13159
  } catch {
13125
13160
  }
@@ -13131,9 +13166,10 @@ async function run18() {
13131
13166
  topMatches,
13132
13167
  topPeers,
13133
13168
  incomingPending,
13134
- unreadChat
13169
+ unreadChat,
13170
+ sessionStale
13135
13171
  };
13136
- writeFileSync17(INDEX_CACHE_FILE5, JSON.stringify(cacheEntry), "utf8");
13172
+ writeFileSync17(INDEX_CACHE_FILE6, JSON.stringify(cacheEntry), "utf8");
13137
13173
  try {
13138
13174
  const {
13139
13175
  readSpinnerConfig: readSpinnerConfig2,
@@ -13184,7 +13220,7 @@ async function run18() {
13184
13220
  process.exit(1);
13185
13221
  }
13186
13222
  }
13187
- var GH_SESSION_COOKIE7, __dirname4, TERMINALHIRE_DIR14, INDEX_CACHE_FILE5, API_URL6;
13223
+ var GH_SESSION_COOKIE7, __dirname4, TERMINALHIRE_DIR14, INDEX_CACHE_FILE6, API_URL6;
13188
13224
  var init_jpi_refresh = __esm({
13189
13225
  "bin/jpi-refresh.js"() {
13190
13226
  "use strict";
@@ -13194,8 +13230,8 @@ var init_jpi_refresh = __esm({
13194
13230
  GH_SESSION_COOKIE7 = "__jpi_gh_session";
13195
13231
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
13196
13232
  TERMINALHIRE_DIR14 = join23(homedir21(), ".terminalhire");
13197
- INDEX_CACHE_FILE5 = join23(TERMINALHIRE_DIR14, "index-cache.json");
13198
- API_URL6 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
13233
+ INDEX_CACHE_FILE6 = join23(TERMINALHIRE_DIR14, "index-cache.json");
13234
+ API_URL6 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://www.terminalhire.com";
13199
13235
  }
13200
13236
  });
13201
13237
 
@@ -13210,8 +13246,8 @@ import { homedir as homedir22 } from "os";
13210
13246
  import { fileURLToPath as fileURLToPath6 } from "url";
13211
13247
  function findJobInCache(jobId) {
13212
13248
  try {
13213
- if (!existsSync18(INDEX_CACHE_FILE6)) return null;
13214
- const raw = readFileSync22(INDEX_CACHE_FILE6, "utf8");
13249
+ if (!existsSync18(INDEX_CACHE_FILE7)) return null;
13250
+ const raw = readFileSync22(INDEX_CACHE_FILE7, "utf8");
13215
13251
  const entry = JSON.parse(raw);
13216
13252
  const jobs = entry?.index?.jobs ?? [];
13217
13253
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -13299,13 +13335,13 @@ async function run19() {
13299
13335
  process.exit(1);
13300
13336
  }
13301
13337
  }
13302
- var __dirname5, TERMINALHIRE_DIR15, INDEX_CACHE_FILE6;
13338
+ var __dirname5, TERMINALHIRE_DIR15, INDEX_CACHE_FILE7;
13303
13339
  var init_jpi_save = __esm({
13304
13340
  "bin/jpi-save.js"() {
13305
13341
  "use strict";
13306
13342
  __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
13307
13343
  TERMINALHIRE_DIR15 = join24(homedir22(), ".terminalhire");
13308
- INDEX_CACHE_FILE6 = join24(TERMINALHIRE_DIR15, "index-cache.json");
13344
+ INDEX_CACHE_FILE7 = join24(TERMINALHIRE_DIR15, "index-cache.json");
13309
13345
  }
13310
13346
  });
13311
13347
 
@@ -6980,7 +6980,7 @@ var GH_SESSION_COOKIE = "__jpi_gh_session";
6980
6980
  var __dirname2 = fileURLToPath3(new URL(".", import.meta.url));
6981
6981
  var TERMINALHIRE_DIR4 = join9(homedir7(), ".terminalhire");
6982
6982
  var INDEX_CACHE_FILE2 = join9(TERMINALHIRE_DIR4, "index-cache.json");
6983
- var API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
6983
+ var API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://www.terminalhire.com";
6984
6984
  async function run() {
6985
6985
  try {
6986
6986
  let index;
@@ -7079,6 +7079,8 @@ async function run() {
7079
7079
  } catch {
7080
7080
  }
7081
7081
  let incomingPending = { count: 0 };
7082
+ let sessionStale = false;
7083
+ const sessionExpired = (res) => res.status === 401;
7082
7084
  const sessionCookie = readWebSessionFile();
7083
7085
  if (sessionCookie && !isInboundNudgeMuted()) try {
7084
7086
  const res = await fetch(`${API_URL2}/api/intro/list`, {
@@ -7091,6 +7093,8 @@ async function run() {
7091
7093
  const intros = Array.isArray(body?.intros) ? body.intros : [];
7092
7094
  const incoming = intros.filter((it) => it && it.role === "incoming" && it.status === "pending");
7093
7095
  incomingPending = { count: incoming.length };
7096
+ } else if (sessionExpired(res)) {
7097
+ sessionStale = true;
7094
7098
  }
7095
7099
  } catch {
7096
7100
  }
@@ -7109,6 +7113,8 @@ async function run() {
7109
7113
  0
7110
7114
  );
7111
7115
  unreadChat = { count: total };
7116
+ } else if (sessionExpired(res)) {
7117
+ sessionStale = true;
7112
7118
  }
7113
7119
  } catch {
7114
7120
  }
@@ -7120,7 +7126,8 @@ async function run() {
7120
7126
  topMatches,
7121
7127
  topPeers,
7122
7128
  incomingPending,
7123
- unreadChat
7129
+ unreadChat,
7130
+ sessionStale
7124
7131
  };
7125
7132
  writeFileSync6(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
7126
7133
  try {
package/dist/bin/jpi.js CHANGED
@@ -105,6 +105,16 @@ function getCachedUnreadChatCount() {
105
105
  return 0;
106
106
  }
107
107
  }
108
+ function getCachedSessionStale() {
109
+ try {
110
+ const raw = readFileSync(INDEX_CACHE_FILE, "utf8");
111
+ const entry = JSON.parse(raw);
112
+ if (Date.now() - entry.ts > INDEX_CACHE_TTL_MS) return false;
113
+ return entry.sessionStale === true;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
108
118
  function getNudgeMode() {
109
119
  const envVal = process.env["TERMINALHIRE_NUDGE"];
110
120
  if (envVal) {
@@ -178,9 +188,10 @@ try {
178
188
  const matchCount = getCachedMatchCount();
179
189
  const incomingCount = getCachedIncomingCount();
180
190
  const unreadChatCount = getCachedUnreadChatCount();
191
+ const sessionStale = getCachedSessionStale() && incomingCount === 0 && unreadChatCount === 0;
181
192
  const haveRoles = matchCount !== null && matchCount > 0;
182
- if (!haveRoles && incomingCount === 0 && unreadChatCount === 0) process.exit(0);
183
- const hasConnectionSignal = incomingCount > 0 || unreadChatCount > 0;
193
+ if (!haveRoles && incomingCount === 0 && unreadChatCount === 0 && !sessionStale) process.exit(0);
194
+ const hasConnectionSignal = incomingCount > 0 || unreadChatCount > 0 || sessionStale;
184
195
  const nudgeMode = getNudgeMode();
185
196
  if (!hasConnectionSignal && !shouldNudge(nudgeMode, sessionId)) process.exit(0);
186
197
  let line;
@@ -189,11 +200,14 @@ try {
189
200
  line = `\u2726 ${matchCount} ${plural} match your current work \u2014 run: terminalhire jobs`;
190
201
  if (incomingCount > 0) line += ` \xB7 \u2709 ${incomingCount} waiting to connect`;
191
202
  if (unreadChatCount > 0) line += ` \xB7 \u{1F4AC} ${unreadChatCount} unread`;
203
+ if (sessionStale) line += ` \xB7 \u26A0 session expired \u2014 run: terminalhire link`;
192
204
  } else if (incomingCount > 0) {
193
205
  line = `\u2709 ${incomingCount} waiting to connect \u2014 run: terminalhire intro --list`;
194
206
  if (unreadChatCount > 0) line += ` \xB7 \u{1F4AC} ${unreadChatCount} unread`;
195
- } else {
207
+ } else if (unreadChatCount > 0) {
196
208
  line = `\u{1F4AC} ${unreadChatCount} unread \u2014 run: terminalhire chat`;
209
+ } else {
210
+ line = `\u26A0 terminalhire session expired \u2014 run: terminalhire link to restore your connection signals`;
197
211
  }
198
212
  process.stdout.write(line + "\n");
199
213
  if (haveRoles && nudgeMode === "session") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.10.7",
3
+ "version": "0.10.9",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",