@hasna/experts 0.0.7 → 0.0.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,SAAS,EAAmC,MAAM,OAAO,CAAC;AA2BnE,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,QAAQ,CA6I5D;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuDhF;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAS,wBAUrD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,SAAS,EAAmC,MAAM,OAAO,CAAC;AA4BnE,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,QAAQ,CA6I5D;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqDhF;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAS,wBAUrD"}
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __require = import.meta.require;
3
4
 
4
5
  // src/db.ts
5
6
  import { Database } from "bun:sqlite";
@@ -49,6 +50,36 @@ function authorityScore(e, inputs = {}, weights = DEFAULT_WEIGHTS) {
49
50
  const raw = weights.rating * rating + weights.reviews * reviews + weights.followers * followers + weights.featured * featured + weights.verified * verified + weights.recency * recency;
50
51
  return Math.round(raw * 1000) / 10;
51
52
  }
53
+ function pricePerHour(price, priceUnit) {
54
+ if (!price || price <= 0)
55
+ return /free/i.test(priceUnit) ? 0 : null;
56
+ const u = (priceUnit || "").toLowerCase();
57
+ const minMatch = u.match(/(\d+)\s*min/);
58
+ if (minMatch)
59
+ return Math.round(price * 60 / Number(minMatch[1]));
60
+ if (/per\s*min|\/\s*min|minute/.test(u))
61
+ return price * 60;
62
+ if (/hour|\/\s*hr|per\s*hr/.test(u))
63
+ return price;
64
+ if (/free/.test(u))
65
+ return 0;
66
+ return null;
67
+ }
68
+ var DEFAULT_BLEND = { semantic: 0.8, authority: 0.2 };
69
+ function blendScore(semantic, authority, w = DEFAULT_BLEND) {
70
+ const a = Math.max(0, Math.min(1, (authority || 0) / 100));
71
+ const s = Math.max(0, Math.min(1, semantic));
72
+ return w.semantic * s + w.authority * a;
73
+ }
74
+ function explainMatch(query, e) {
75
+ const q = ` ${(query || "").toLowerCase()} `;
76
+ const hit = (label) => {
77
+ const l = label.toLowerCase();
78
+ return q.includes(` ${l} `) || q.includes(`${l},`) || q.includes(`${l}.`) || new RegExp(`\\b${l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(q);
79
+ };
80
+ const matched = [...e.topics, ...e.tags].filter(hit);
81
+ return [...new Set(matched)].slice(0, 6);
82
+ }
52
83
 
53
84
  // src/embed.ts
54
85
  var STOPWORDS = new Set([
@@ -139,11 +170,40 @@ class OpenAIEmbedder {
139
170
  return data.data.map((d) => d.embedding);
140
171
  }
141
172
  }
142
- function getEmbedder() {
143
- if (process.env.EXPERTS_EMBEDDER === "openai" && process.env.OPENAI_API_KEY) {
173
+
174
+ class TransformersEmbedder {
175
+ id = "minilm-l6-v2";
176
+ dim = 384;
177
+ model = process.env.EXPERTS_EMBED_MODEL || "Xenova/all-MiniLM-L6-v2";
178
+ extractor = null;
179
+ async ensure() {
180
+ if (this.extractor)
181
+ return;
182
+ const { pipeline } = await import("@huggingface/transformers");
183
+ this.extractor = await pipeline("feature-extraction", this.model);
184
+ }
185
+ async embed(texts) {
186
+ await this.ensure();
187
+ const out = [];
188
+ for (const t of texts) {
189
+ const r = await this.extractor(t || " ", { pooling: "mean", normalize: true });
190
+ out.push(Array.from(r.data));
191
+ }
192
+ return out;
193
+ }
194
+ }
195
+ async function getEmbedder() {
196
+ const choice = process.env.EXPERTS_EMBEDDER;
197
+ if (choice === "openai" && process.env.OPENAI_API_KEY)
144
198
  return new OpenAIEmbedder;
199
+ if (choice === "hash")
200
+ return new HashingEmbedder;
201
+ try {
202
+ await import("@huggingface/transformers");
203
+ return new TransformersEmbedder;
204
+ } catch {
205
+ return new HashingEmbedder;
145
206
  }
146
- return new HashingEmbedder;
147
207
  }
148
208
  function cosine(a, b) {
149
209
  let dot = 0;
@@ -228,6 +288,48 @@ function clusterPersons(experts) {
228
288
  return out;
229
289
  }
230
290
 
291
+ // src/crypto.ts
292
+ import { createCipheriv, createDecipheriv, createHmac, scryptSync } from "crypto";
293
+ var PREFIX = "enc1:";
294
+ var cachedKey = null;
295
+ var cachedFrom = null;
296
+ function key2() {
297
+ const secret = process.env.OPEN_EXPERTS_KEY;
298
+ if (!secret)
299
+ return null;
300
+ if (cachedKey && cachedFrom === secret)
301
+ return cachedKey;
302
+ cachedKey = scryptSync(secret, "open-experts/contacts/v1", 32);
303
+ cachedFrom = secret;
304
+ return cachedKey;
305
+ }
306
+ function maybeEncrypt(plaintext) {
307
+ const k = key2();
308
+ if (!k || plaintext == null)
309
+ return plaintext;
310
+ if (plaintext.startsWith(PREFIX))
311
+ return plaintext;
312
+ const iv = createHmac("sha256", k).update(plaintext).digest().subarray(0, 12);
313
+ const cipher = createCipheriv("aes-256-gcm", k, iv);
314
+ const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
315
+ const tag = cipher.getAuthTag();
316
+ return PREFIX + Buffer.concat([iv, tag, enc]).toString("base64");
317
+ }
318
+ function maybeDecrypt(stored) {
319
+ if (stored == null || !stored.startsWith(PREFIX))
320
+ return stored;
321
+ const k = key2();
322
+ if (!k)
323
+ return stored;
324
+ const raw = Buffer.from(stored.slice(PREFIX.length), "base64");
325
+ const iv = raw.subarray(0, 12);
326
+ const tag = raw.subarray(12, 28);
327
+ const enc = raw.subarray(28);
328
+ const decipher = createDecipheriv("aes-256-gcm", k, iv);
329
+ decipher.setAuthTag(tag);
330
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
331
+ }
332
+
231
333
  // src/db.ts
232
334
  function defaultDbPath() {
233
335
  return process.env.OPEN_EXPERTS_DB || join(homedir(), ".hasna", "experts", "experts.db");
@@ -339,10 +441,11 @@ class ExpertsDB {
339
441
  CREATE INDEX IF NOT EXISTS idx_contacts_expert ON contacts(source, source_id);
340
442
  CREATE INDEX IF NOT EXISTS idx_contacts_status ON contacts(status);
341
443
 
342
- -- Semantic search: one embedding vector per expert.
444
+ -- Semantic search: one embedding vector per expert (text_hash enables
445
+ -- incremental re-embedding \u2014 skip unchanged experts).
343
446
  CREATE TABLE IF NOT EXISTS vectors (
344
447
  source TEXT NOT NULL, source_id TEXT NOT NULL,
345
- embedder TEXT NOT NULL, dim INTEGER, vec BLOB,
448
+ embedder TEXT NOT NULL, dim INTEGER, vec BLOB, text_hash TEXT,
346
449
  PRIMARY KEY (source, source_id)
347
450
  );
348
451
 
@@ -364,6 +467,7 @@ class ExpertsDB {
364
467
  `);
365
468
  this.addColumnIfMissing("experts", "avatar_local", "TEXT");
366
469
  this.addColumnIfMissing("experts", "authority", "REAL DEFAULT 0");
470
+ this.addColumnIfMissing("vectors", "text_hash", "TEXT");
367
471
  }
368
472
  addColumnIfMissing(table, column, type) {
369
473
  const cols = this.db.query(`PRAGMA table_info(${table})`).all();
@@ -450,6 +554,7 @@ class ExpertsDB {
450
554
  extra: JSON.parse(r.extra || "{}"),
451
555
  avatarLocal: r.avatar_local || undefined,
452
556
  authority: r.authority ?? 0,
557
+ pricePerHour: pricePerHour(r.price ?? 0, r.price_unit ?? ""),
453
558
  crawledAt: r.crawled_at
454
559
  };
455
560
  }
@@ -573,11 +678,11 @@ class ExpertsDB {
573
678
  sql += " ORDER BY name";
574
679
  return this.db.query(sql).all(...params);
575
680
  }
576
- setMeta(key2, value) {
577
- this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(key2, value);
681
+ setMeta(key3, value) {
682
+ this.db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(key3, value);
578
683
  }
579
- getMeta(key2) {
580
- const row = this.db.query("SELECT value FROM meta WHERE key = ?").get(key2);
684
+ getMeta(key3) {
685
+ const row = this.db.query("SELECT value FROM meta WHERE key = ?").get(key3);
581
686
  return row ? row.value : null;
582
687
  }
583
688
  stats(source) {
@@ -601,12 +706,12 @@ class ExpertsDB {
601
706
  const nodeIds = new Map;
602
707
  const insertNode = this.db.query("INSERT INTO kg_nodes (type, key, label) VALUES (?, ?, ?) ON CONFLICT(type, key) DO UPDATE SET label=excluded.label RETURNING id");
603
708
  const insertEdge = this.db.query("INSERT OR REPLACE INTO kg_edges (src, dst, rel, weight) VALUES (?, ?, ?, ?)");
604
- const node = (type, key2, label) => {
605
- const ck = `${type}\x00${key2.toLowerCase()}`;
709
+ const node = (type, key3, label) => {
710
+ const ck = `${type}\x00${key3.toLowerCase()}`;
606
711
  const cached = nodeIds.get(ck);
607
712
  if (cached != null)
608
713
  return cached;
609
- const id = insertNode.get(type, key2.toLowerCase(), label).id;
714
+ const id = insertNode.get(type, key3.toLowerCase(), label).id;
610
715
  nodeIds.set(ck, id);
611
716
  return id;
612
717
  };
@@ -616,7 +721,7 @@ class ExpertsDB {
616
721
  for (const topic of e.topics) {
617
722
  insertEdge.run(eId, node("topic", topic, topic), "IN_TOPIC", 1);
618
723
  }
619
- const tweetText = this.recentTweets(e.source, e.sourceId, 30).map((t) => t.text).join(". ");
724
+ const tweetText = this.recentTweets(e.source, e.sourceId, 30).filter((t) => !t.isRetweet).map((t) => t.text).join(". ");
620
725
  const tags = inferTags(expertText(e) + ". " + tweetText, vocabulary);
621
726
  for (const tag of tags) {
622
727
  insertEdge.run(eId, node("tag", tag, tag), "HAS_TAG", 1);
@@ -670,11 +775,11 @@ class ExpertsDB {
670
775
  lastSeen: r.last_seen || ""
671
776
  }));
672
777
  }
673
- expertFromNodeKey(key2) {
674
- const idx = key2.indexOf(":");
778
+ expertFromNodeKey(key3) {
779
+ const idx = key3.indexOf(":");
675
780
  if (idx < 0)
676
781
  return null;
677
- return this.get(key2.slice(idx + 1), key2.slice(0, idx));
782
+ return this.get(key3.slice(idx + 1), key3.slice(0, idx));
678
783
  }
679
784
  findByNeeds(needs, opts = {}) {
680
785
  const cleaned = needs.map((n) => n.trim().toLowerCase()).filter(Boolean);
@@ -801,6 +906,15 @@ class ExpertsDB {
801
906
  };
802
907
  }
803
908
  replaceTweets(source, sourceId, tweets) {
909
+ const norm = (t) => (t || "").toLowerCase().replace(/^rt @\w+:\s*/, "").replace(/https?:\/\/\S+/g, "").replace(/[^a-z0-9 ]/g, "").replace(/\s+/g, " ").trim();
910
+ const seen = new Set;
911
+ const deduped = tweets.filter((t) => {
912
+ const k = norm(t.text);
913
+ if (!k || seen.has(k))
914
+ return false;
915
+ seen.add(k);
916
+ return true;
917
+ });
804
918
  const tx = this.db.transaction((rows) => {
805
919
  this.db.query("DELETE FROM tweets WHERE source = ? AND source_id = ?").run(source, sourceId);
806
920
  const stmt = this.db.query(`
@@ -813,7 +927,7 @@ class ExpertsDB {
813
927
  stmt.run(t.source, t.sourceId, t.tweetId, t.text, t.createdAt, t.retweetCount, t.replyCount, t.likeCount, t.quoteCount, t.impressionCount, t.isRetweet ? 1 : 0, t.isReply ? 1 : 0);
814
928
  }
815
929
  });
816
- tx(tweets);
930
+ tx(deduped);
817
931
  }
818
932
  recentTweets(source, sourceId, limit = 10) {
819
933
  const rows = this.db.query("SELECT * FROM tweets WHERE source = ? AND source_id = ? ORDER BY created_at DESC LIMIT ?").all(source, sourceId, limit);
@@ -931,17 +1045,24 @@ class ExpertsDB {
931
1045
  const log = opts.onLog ?? (() => {});
932
1046
  const experts = this.list({ source: opts.source });
933
1047
  const batch = opts.batch ?? 64;
934
- const stmt = this.db.query("INSERT OR REPLACE INTO vectors (source, source_id, embedder, dim, vec) VALUES (?, ?, ?, ?, ?)");
1048
+ const stmt = this.db.query("INSERT OR REPLACE INTO vectors (source, source_id, embedder, dim, vec, text_hash) VALUES (?, ?, ?, ?, ?, ?)");
1049
+ const existing = new Map(this.db.query("SELECT source, source_id, embedder, text_hash FROM vectors").all().map((r) => [`${r.source}:${r.source_id}`, { embedder: r.embedder, hash: r.text_hash || "" }]));
1050
+ const work = experts.map((e) => ({ e, text: expertEmbedText(e), hash: String(Bun.hash(expertEmbedText(e))) })).filter(({ e, hash }) => {
1051
+ if (opts.force)
1052
+ return true;
1053
+ const prev = existing.get(`${e.source}:${e.sourceId}`);
1054
+ return !prev || prev.embedder !== embedder.id || prev.hash !== hash;
1055
+ });
935
1056
  let done = 0;
936
- for (let i = 0;i < experts.length; i += batch) {
937
- const slice = experts.slice(i, i + batch);
938
- const vecs = await embedder.embed(slice.map((e) => expertEmbedText(e)));
1057
+ for (let i = 0;i < work.length; i += batch) {
1058
+ const slice = work.slice(i, i + batch);
1059
+ const vecs = await embedder.embed(slice.map((w) => w.text));
939
1060
  const tx = this.db.transaction(() => {
940
- slice.forEach((e, j) => stmt.run(e.source, e.sourceId, embedder.id, embedder.dim, packVector(vecs[j])));
1061
+ slice.forEach((w, j) => stmt.run(w.e.source, w.e.sourceId, embedder.id, embedder.dim, packVector(vecs[j]), w.hash));
941
1062
  });
942
1063
  tx();
943
1064
  done += slice.length;
944
- log(` embedded ${done}/${experts.length}`);
1065
+ log(` embedded ${done}/${work.length} (${experts.length - work.length} unchanged)`);
945
1066
  }
946
1067
  this.setMeta("embedder", embedder.id);
947
1068
  this.setMeta("embedded_at", new Date().toISOString());
@@ -954,7 +1075,13 @@ class ExpertsDB {
954
1075
  const where = opts.source ? "WHERE v.source = ?" : "";
955
1076
  const params = opts.source ? [opts.source] : [];
956
1077
  const rows = this.db.query(`SELECT e.*, v.vec AS _vec FROM vectors v JOIN experts e ON e.source=v.source AND e.source_id=v.source_id ${where}`).all(...params);
957
- const scored = rows.map((r) => ({ expert: this.rowToExpert(r), score: cosine(queryVec, unpackVector(r._vec)) }));
1078
+ const blend = opts.blend !== false;
1079
+ const scored = rows.map((r) => {
1080
+ const expert = this.rowToExpert(r);
1081
+ const semantic = cosine(queryVec, unpackVector(r._vec));
1082
+ const score = blend ? blendScore(semantic, expert.authority ?? 0) : semantic;
1083
+ return { expert, score, semantic };
1084
+ });
958
1085
  scored.sort((a, b) => b.score - a.score);
959
1086
  return scored.slice(0, opts.limit ?? 25);
960
1087
  }
@@ -1044,7 +1171,7 @@ class ExpertsDB {
1044
1171
  $source: c.source,
1045
1172
  $source_id: c.sourceId,
1046
1173
  $type: c.type,
1047
- $value: c.value,
1174
+ $value: maybeEncrypt(c.value),
1048
1175
  $label: c.label,
1049
1176
  $provider: c.provider,
1050
1177
  $confidence: c.confidence,
@@ -1054,7 +1181,7 @@ class ExpertsDB {
1054
1181
  });
1055
1182
  }
1056
1183
  setContactStatus(source, sourceId, type, value, status) {
1057
- this.db.query("UPDATE contacts SET status = ?, verified_at = ? WHERE source = ? AND source_id = ? AND type = ? AND value = ?").run(status, new Date().toISOString(), source, sourceId, type, value);
1184
+ this.db.query("UPDATE contacts SET status = ?, verified_at = ? WHERE source = ? AND source_id = ? AND type = ? AND value = ?").run(status, new Date().toISOString(), source, sourceId, type, maybeEncrypt(value));
1058
1185
  }
1059
1186
  contacts(source, sourceId) {
1060
1187
  const rows = this.db.query("SELECT * FROM contacts WHERE source = ? AND source_id = ? ORDER BY type, confidence DESC").all(source, sourceId);
@@ -1062,7 +1189,7 @@ class ExpertsDB {
1062
1189
  source: r.source,
1063
1190
  sourceId: r.source_id,
1064
1191
  type: r.type,
1065
- value: r.value,
1192
+ value: maybeDecrypt(r.value),
1066
1193
  label: r.label || "",
1067
1194
  provider: r.provider || "",
1068
1195
  confidence: r.confidence ?? 0,
@@ -1087,7 +1214,7 @@ class ExpertsDB {
1087
1214
  source: r.source,
1088
1215
  sourceId: r.source_id,
1089
1216
  type: r.type,
1090
- value: r.value,
1217
+ value: maybeDecrypt(r.value),
1091
1218
  label: r.label || "",
1092
1219
  provider: r.provider || "",
1093
1220
  confidence: r.confidence ?? 0,
@@ -1397,34 +1524,37 @@ async function fetchJson(url, fetchFn, init = {}) {
1397
1524
  }
1398
1525
 
1399
1526
  // src/sources/mentorcruise.ts
1527
+ function stripHtml(s) {
1528
+ return (s || "").replace(/<[^>]+>/g, " ").replace(/&[a-z#0-9]+;/gi, " ").replace(/\s+/g, " ").trim();
1529
+ }
1400
1530
  function normalizeMentor(m, crawledAt) {
1401
- const slug = m.slug || slugify(m.name || String(m.id ?? ""));
1531
+ const path = m.get_absolute_url || "";
1532
+ const slug = path.match(/\/mentor\/([^/]+)/)?.[1] || slugify(m.get_full_name || String(m.objectID ?? ""));
1402
1533
  const socials = {};
1403
1534
  if (m.twitter)
1404
1535
  socials.twitter = m.twitter.startsWith("http") ? m.twitter : `https://x.com/${m.twitter}`;
1405
1536
  if (m.linkedin)
1406
1537
  socials.linkedin = m.linkedin;
1538
+ const price = m.all_prices?.length ? Math.min(...m.all_prices) : Math.round(m.avg_price_per_call ?? 0);
1407
1539
  return makeExpert({
1408
1540
  source: "mentorcruise",
1409
- sourceId: String(m.id ?? slug),
1541
+ sourceId: String(m.objectID ?? slug),
1410
1542
  slug,
1411
- url: `https://mentorcruise.com/mentor/${slug}/`,
1412
- fullName: m.name ?? [m.first_name, m.last_name].filter(Boolean).join(" "),
1413
- firstName: m.first_name ?? "",
1414
- lastName: m.last_name ?? "",
1415
- title: m.job_title ?? "",
1416
- bio: m.bio ?? "",
1417
- avatar: m.avatar ?? m.photo ?? "",
1418
- price: m.price ?? 0,
1419
- priceCurrency: m.currency ?? "USD",
1420
- priceUnit: m.price ? "per month" : "",
1421
- rating: m.rating ?? 0,
1422
- ratingCount: m.reviews_count ?? 0,
1423
- verified: Boolean(m.verified),
1543
+ url: path ? `https://mentorcruise.com${path}` : `https://mentorcruise.com/mentor/${slug}/`,
1544
+ fullName: m.get_full_name ?? "",
1545
+ title: (m.cleaned_job_title ?? []).join(", "),
1546
+ bio: stripHtml(m.bio_formatted ?? ""),
1547
+ avatar: m.get_profile_picture ?? "",
1548
+ price,
1549
+ priceCurrency: "USD",
1550
+ priceUnit: price ? "per month" : "",
1551
+ rating: m.avg_rating_float_one_decimal ?? 0,
1552
+ ratingCount: m.number_of_reviews ?? 0,
1424
1553
  featured: Boolean(m.is_top_mentor),
1425
- topics: m.categories ?? [],
1426
- tags: m.skills ?? [],
1554
+ topics: m.get_industries ?? [],
1555
+ tags: m.get_skills ?? [],
1427
1556
  socials,
1557
+ extra: { company: m.company ?? "", location: m.get_location_display ?? "", avgPricePerCall: m.avg_price_per_call ?? 0 },
1428
1558
  crawledAt
1429
1559
  });
1430
1560
  }
@@ -1434,44 +1564,61 @@ class MentorCruiseSource {
1434
1564
  description = "MentorCruise \u2014 long-term mentorship from vetted mentors";
1435
1565
  website = "https://mentorcruise.com";
1436
1566
  fetchFn;
1437
- apiBase;
1567
+ appId;
1568
+ apiKey;
1569
+ index;
1438
1570
  pageSize;
1439
1571
  constructor(opts = {}) {
1440
1572
  this.fetchFn = opts.fetchFn ?? fetch;
1441
- this.apiBase = opts.apiBase ?? process.env.MENTORCRUISE_API_BASE ?? "https://mentorcruise.com/api";
1442
- this.pageSize = opts.pageSize ?? 50;
1573
+ this.appId = opts.appId ?? process.env.MENTORCRUISE_ALGOLIA_APP_ID ?? "YD3XA4V91L";
1574
+ this.apiKey = opts.apiKey ?? process.env.MENTORCRUISE_ALGOLIA_API_KEY ?? "454b55a2e50bc884225318d99b0dad1a";
1575
+ this.index = opts.index ?? process.env.MENTORCRUISE_ALGOLIA_INDEX ?? "MentorProfile_prod";
1576
+ this.pageSize = opts.pageSize ?? 200;
1443
1577
  }
1444
1578
  async crawl(opts = {}) {
1445
1579
  const log = opts.onLog ?? (() => {});
1446
1580
  const crawledAt = new Date().toISOString();
1581
+ const url = `https://${this.appId}-dsn.algolia.net/1/indexes/${this.index}/query`;
1447
1582
  const experts = [];
1448
1583
  const tags = new Set;
1449
- let offset = 0;
1450
- for (;; ) {
1451
- const data = await fetchJson(`${this.apiBase}/mentors/?limit=${this.pageSize}&offset=${offset}`, this.fetchFn);
1452
- const items = data?.results ?? data?.data ?? (Array.isArray(data) ? data : []);
1453
- if (!items.length)
1584
+ let page = 0;
1585
+ let pages = 1;
1586
+ while (page < pages) {
1587
+ let data;
1588
+ try {
1589
+ const res = await this.fetchFn(url, {
1590
+ method: "POST",
1591
+ headers: {
1592
+ "X-Algolia-Application-Id": this.appId,
1593
+ "X-Algolia-API-Key": this.apiKey,
1594
+ "Content-Type": "application/json"
1595
+ },
1596
+ body: JSON.stringify({ params: `hitsPerPage=${this.pageSize}&page=${page}` })
1597
+ });
1598
+ if (!res.ok)
1599
+ break;
1600
+ data = await res.json();
1601
+ } catch {
1454
1602
  break;
1455
- for (const m of items) {
1456
- const e = normalizeMentor(m, crawledAt);
1603
+ }
1604
+ pages = data.nbPages ?? 1;
1605
+ for (const hit of data.hits ?? []) {
1606
+ const e = normalizeMentor(hit, crawledAt);
1457
1607
  experts.push(e);
1458
1608
  for (const t of e.tags)
1459
1609
  tags.add(t);
1460
1610
  }
1461
- offset += items.length;
1462
- log(` mentorcruise: ${experts.length}`);
1611
+ log(` mentorcruise: ${experts.length}/${data.nbHits ?? "?"}`);
1612
+ page++;
1463
1613
  if (opts.max && experts.length >= opts.max)
1464
1614
  break;
1465
- if (items.length < this.pageSize)
1466
- break;
1467
1615
  }
1468
1616
  if (experts.length === 0) {
1469
- log("mentorcruise: no public listing reachable (set MENTORCRUISE_API_BASE or provide a fetchFn).");
1617
+ log("mentorcruise: Algolia returned nothing (set MENTORCRUISE_ALGOLIA_* or inject fetchFn).");
1470
1618
  }
1471
- const topics = [];
1472
1619
  return {
1473
1620
  experts: opts.max ? experts.slice(0, opts.max) : experts,
1474
- topics,
1621
+ topics: [],
1475
1622
  tags: [...tags].map((name) => ({ name, topic: "" })),
1476
1623
  total: experts.length
1477
1624
  };
@@ -1692,6 +1839,9 @@ async function crawlSource(db, sourceName, opts = {}) {
1692
1839
  throw new Error(`Unknown source "${sourceName}". Run \`experts sources\` to list options.`);
1693
1840
  }
1694
1841
  const data = await source.crawl(opts);
1842
+ if (data.experts.length === 0 && db.count(source.name) > 0) {
1843
+ opts.onLog?.(`\u26A0 ${source.name} returned 0 experts but ${db.count(source.name)} are stored \u2014 possible API drift; not overwriting.`);
1844
+ }
1695
1845
  const changes = db.recordChanges(source.name, data.experts);
1696
1846
  db.upsertExperts(data.experts);
1697
1847
  if (data.topics.length)
@@ -1861,11 +2011,12 @@ async function handleAsync(db, req) {
1861
2011
  return json({ error: "missing q" }, 400);
1862
2012
  if (db.vectorCount() === 0)
1863
2013
  return json({ error: "no semantic index; run `experts embed`" }, 409);
1864
- const [qv] = await getEmbedder().embed([q]);
1865
- return json(db.semanticSearch(qv, {
2014
+ const [qv] = await (await getEmbedder()).embed([q]);
2015
+ const results = db.semanticSearch(qv, {
1866
2016
  source: url.searchParams.get("source") || undefined,
1867
2017
  limit: num(url.searchParams.get("limit"))
1868
- }));
2018
+ }).map((r) => ({ ...r, why: explainMatch(q, r.expert) }));
2019
+ return json(results);
1869
2020
  }
1870
2021
  if (url.pathname.replace(/\/+$/, "") === "/brief") {
1871
2022
  const q = url.searchParams.get("q") || "";
@@ -1874,17 +2025,16 @@ async function handleAsync(db, req) {
1874
2025
  if (db.vectorCount() === 0)
1875
2026
  return json({ error: "no semantic index; run `experts embed`" }, 409);
1876
2027
  const limit = num(url.searchParams.get("limit")) ?? 10;
1877
- const [qv] = await getEmbedder().embed([q]);
2028
+ const [qv] = await (await getEmbedder()).embed([q]);
1878
2029
  const raw = db.semanticSearch(qv, { source: url.searchParams.get("source") || undefined, limit: (limit + 5) * 4 });
1879
2030
  const seen = new Set;
1880
- const briefLc = q.toLowerCase();
1881
2031
  const out = [];
1882
2032
  for (const r of raw) {
1883
2033
  const pid = db.personIdOf(r.expert.source, r.expert.sourceId);
1884
2034
  if (seen.has(pid))
1885
2035
  continue;
1886
2036
  seen.add(pid);
1887
- out.push({ ...r, why: r.expert.tags.filter((t) => briefLc.includes(t.toLowerCase())).slice(0, 4) });
2037
+ out.push({ ...r, why: explainMatch(q, r.expert) });
1888
2038
  if (out.length >= limit)
1889
2039
  break;
1890
2040
  }
@@ -1,45 +1,48 @@
1
1
  /**
2
2
  * MentorCruise source adapter (mentorship marketplace).
3
3
  *
4
- * MentorCruise serves its mentor directory from a JSON API. The exact listing
5
- * endpoint/params can change; `apiBase`/`fetchFn` are injectable so the adapter
6
- * is testable and re-pointable. Normalization to the common Expert model is the
7
- * stable, tested part.
4
+ * MentorCruise's directory is backed by an Algolia index (`MentorProfile_prod`).
5
+ * The public app id + search key are client-side (as Algolia search keys are),
6
+ * so we query the index directly and paginate. All of it is injectable for
7
+ * tests; normalization to the common Expert model is the stable, tested part.
8
8
  */
9
9
  import type { CrawlData, Expert, Source, SourceCrawlOptions } from "../types";
10
+ /** A MentorCruise mentor as stored in Algolia. */
10
11
  export interface RawMentor {
11
- id?: number | string;
12
- slug?: string;
13
- name?: string;
14
- first_name?: string;
15
- last_name?: string;
16
- job_title?: string;
17
- bio?: string;
18
- avatar?: string;
19
- photo?: string;
20
- price?: number;
21
- currency?: string;
22
- rating?: number;
23
- reviews_count?: number;
12
+ objectID?: string | number;
13
+ get_full_name?: string;
14
+ cleaned_job_title?: string[];
15
+ company?: string;
16
+ bio_formatted?: string;
17
+ avg_price_per_call?: number;
18
+ all_prices?: number[];
19
+ avg_rating_float_one_decimal?: number;
20
+ number_of_reviews?: number;
21
+ get_industries?: string[];
22
+ get_skills?: string[];
23
+ get_location_display?: string;
24
+ get_absolute_url?: string;
25
+ get_profile_picture?: string;
24
26
  is_top_mentor?: boolean;
25
- verified?: boolean;
26
- skills?: string[];
27
- categories?: string[];
28
27
  twitter?: string;
29
28
  linkedin?: string;
30
29
  }
31
- /** Map a MentorCruise mentor to the common Expert shape. */
30
+ /** Map a MentorCruise Algolia record to the common Expert shape. */
32
31
  export declare function normalizeMentor(m: RawMentor, crawledAt?: string): Expert;
33
32
  export declare class MentorCruiseSource implements Source {
34
33
  readonly name = "mentorcruise";
35
34
  readonly description = "MentorCruise \u2014 long-term mentorship from vetted mentors";
36
35
  readonly website = "https://mentorcruise.com";
37
36
  private fetchFn;
38
- private apiBase;
37
+ private appId;
38
+ private apiKey;
39
+ private index;
39
40
  private pageSize;
40
41
  constructor(opts?: {
41
42
  fetchFn?: typeof fetch;
42
- apiBase?: string;
43
+ appId?: string;
44
+ apiKey?: string;
45
+ index?: string;
43
46
  pageSize?: number;
44
47
  });
45
48
  crawl(opts?: SourceCrawlOptions): Promise<CrawlData>;
@@ -1 +1 @@
1
- {"version":3,"file":"mentorcruise.d.ts","sourceRoot":"","sources":["../../src/sources/mentorcruise.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAS,MAAM,UAAU,CAAC;AAGrF,MAAM,WAAW,SAAS;IACxB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CA4BxE;AAED,qBAAa,kBAAmB,YAAW,MAAM;IAC/C,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,WAAW,kEAA6D;IACjF,QAAQ,CAAC,OAAO,8BAA8B;IAC9C,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;gBAEb,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO;IAMhF,KAAK,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;CA+B/D"}
1
+ {"version":3,"file":"mentorcruise.d.ts","sourceRoot":"","sources":["../../src/sources/mentorcruise.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAG9E,kDAAkD;AAClD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CA4BxE;AAED,qBAAa,kBAAmB,YAAW,MAAM;IAC/C,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,WAAW,kEAA6D;IACjF,QAAQ,CAAC,OAAO,8BAA8B;IAC9C,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAS;gBAEb,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO;IAQ/G,KAAK,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;CA6C/D"}
package/dist/types.d.ts CHANGED
@@ -48,6 +48,8 @@ export interface Expert {
48
48
  avatarLocal?: string;
49
49
  /** Composite authority/relevance score (0..100), set by rescore(). */
50
50
  authority?: number;
51
+ /** Comparable USD/hour rate derived from price + priceUnit (null if not hourly-comparable). */
52
+ pricePerHour?: number | null;
51
53
  crawledAt: string;
52
54
  }
53
55
  /** X/Twitter profile snapshot captured during enrichment. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/experts",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Crawl expert marketplaces (intro.co and more) into a local store, then query them via CLI or a remote HTTP API.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "README.md"
27
27
  ],
28
28
  "scripts": {
29
- "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external commander --external chalk && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/index.ts src/sdk.ts --outdir dist --target bun && tsc --emitDeclarationOnly --outDir dist",
29
+ "build": "rm -rf dist && bun build src/cli/index.ts --outdir dist/cli --target bun --external commander --external chalk --external @huggingface/transformers && bun build src/server/index.ts --outdir dist/server --target bun --external @huggingface/transformers && bun build src/index.ts src/sdk.ts --outdir dist --target bun --external @huggingface/transformers && tsc --emitDeclarationOnly --outDir dist",
30
30
  "typecheck": "tsc --noEmit",
31
31
  "test": "bun test",
32
32
  "dev": "bun run src/cli/index.ts",
@@ -64,6 +64,9 @@
64
64
  "chalk": "5.4.1",
65
65
  "commander": "13.1.0"
66
66
  },
67
+ "optionalDependencies": {
68
+ "@huggingface/transformers": "4.2.0"
69
+ },
67
70
  "devDependencies": {
68
71
  "@types/bun": "1.2.4",
69
72
  "typescript": "5.7.3"