@hasna/experts 0.0.4 → 0.0.6

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.
package/dist/cli/index.js CHANGED
@@ -983,13 +983,14 @@ function defaultRunner(bin = "connectors") {
983
983
  ]);
984
984
  const blob = `${out}
985
985
  ${err}`;
986
- const rateLimited = /429|rate limit|too many requests/i.test(blob);
986
+ const rateLimited = /429|rate.?limit|too many requests/i.test(blob);
987
+ const quotaExhausted = /credit|quota|usage ?cap|usagecap|monthly cap|capexceeded/i.test(blob);
987
988
  if (code !== 0) {
988
- return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited };
989
+ return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited, quotaExhausted };
989
990
  }
990
991
  const parsed = extractJson(out);
991
992
  if (parsed === undefined) {
992
- return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited };
993
+ return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited, quotaExhausted };
993
994
  }
994
995
  return { success: true, data: parsed };
995
996
  };
@@ -1119,11 +1120,13 @@ async function enrichExpert(db, e, opts) {
1119
1120
  const handle = handleFromSocial(e.socials.twitter || "");
1120
1121
  const now = new Date().toISOString();
1121
1122
  if (!handle)
1122
- return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
1123
+ return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
1123
1124
  const { user, result } = await client.xUser(handle);
1124
1125
  if (!user) {
1126
+ if (result.quotaExhausted)
1127
+ return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: true };
1125
1128
  if (result.rateLimited)
1126
- return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true };
1129
+ return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true, quotaExhausted: false };
1127
1130
  db.upsertXProfile({
1128
1131
  source: e.source,
1129
1132
  sourceId: e.sourceId,
@@ -1140,7 +1143,7 @@ async function enrichExpert(db, e, opts) {
1140
1143
  worksOn: "",
1141
1144
  enrichedAt: now
1142
1145
  });
1143
- return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
1146
+ return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
1144
1147
  }
1145
1148
  const profile = {
1146
1149
  source: e.source,
@@ -1175,8 +1178,11 @@ async function enrichExpert(db, e, opts) {
1175
1178
  max: opts.tweetMax ?? 100,
1176
1179
  replies: false
1177
1180
  });
1181
+ if (tlResult.quotaExhausted && tweets.length === 0) {
1182
+ return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: false, quotaExhausted: true };
1183
+ }
1178
1184
  if (tlResult.rateLimited && tweets.length === 0) {
1179
- return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true };
1185
+ return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true, quotaExhausted: false };
1180
1186
  }
1181
1187
  const cutoff = Date.now() - (opts.sinceDays ?? 30) * 86400000;
1182
1188
  const recent = tweets.filter((t) => {
@@ -1200,7 +1206,35 @@ async function enrichExpert(db, e, opts) {
1200
1206
  db.replaceTweets(e.source, e.sourceId, rows);
1201
1207
  tweetCount = rows.length;
1202
1208
  }
1203
- return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false };
1209
+ return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false, quotaExhausted: false };
1210
+ }
1211
+ async function backfillAvatars(db, opts = {}) {
1212
+ const log = opts.onLog ?? (() => {});
1213
+ const fetchFn = opts.fetchFn ?? fetch;
1214
+ const delayMs = opts.delayMs ?? 150;
1215
+ const experts = db.list({ source: opts.source });
1216
+ const res = { downloaded: 0, skipped: 0, failed: 0 };
1217
+ for (const e of experts) {
1218
+ if (e.avatarLocal || !e.avatar) {
1219
+ res.skipped++;
1220
+ continue;
1221
+ }
1222
+ try {
1223
+ const path = await downloadAvatar(e.avatar, e, fetchFn);
1224
+ if (path) {
1225
+ db.setAvatarLocal(e.source, e.sourceId, path);
1226
+ res.downloaded++;
1227
+ if (res.downloaded % 100 === 0)
1228
+ log(` avatars: ${res.downloaded} downloaded`);
1229
+ } else {
1230
+ res.failed++;
1231
+ }
1232
+ } catch {
1233
+ res.failed++;
1234
+ }
1235
+ await sleep2(delayMs);
1236
+ }
1237
+ return res;
1204
1238
  }
1205
1239
  async function enrichX(db, opts = {}) {
1206
1240
  const log = opts.onLog ?? (() => {});
@@ -1226,6 +1260,13 @@ async function enrichX(db, opts = {}) {
1226
1260
  await sleep2(delayMs);
1227
1261
  continue;
1228
1262
  }
1263
+ if (outcome.quotaExhausted) {
1264
+ res.stoppedEarly = true;
1265
+ res.reason = "X API credits/quota exhausted \u2014 top up or wait for the monthly reset, then re-run the same command to finish the rest";
1266
+ res.attempted--;
1267
+ log(`X API quota exhausted; stopping at ${i}/${targets.length}. ${res.enriched} enriched so far.`);
1268
+ break;
1269
+ }
1229
1270
  if (outcome.rateLimited) {
1230
1271
  consecutiveRateLimits++;
1231
1272
  if (consecutiveRateLimits >= 3) {
@@ -1256,7 +1297,7 @@ async function enrichX(db, opts = {}) {
1256
1297
  }
1257
1298
 
1258
1299
  // src/cli/index.ts
1259
- var VERSION = "0.0.4";
1300
+ var VERSION = "0.0.6";
1260
1301
  function openDb() {
1261
1302
  const opts = program.opts();
1262
1303
  return new ExpertsDB(opts.db || defaultDbPath());
@@ -1320,6 +1361,21 @@ program.command("enrich [source]").description("Enrich experts via X/Twitter: pr
1320
1361
  console.log(chalk2.dim(`progress: ${after.enriched}/${after.withHandle} enriched`));
1321
1362
  db.close();
1322
1363
  });
1364
+ program.command("avatars [source]").description("Download + properly name profile pictures for experts missing one").option("--delay <ms>", "delay between downloads", (v) => parseInt(v, 10), 150).action(async (source, cmdOpts) => {
1365
+ const db = openDb();
1366
+ requireData(db);
1367
+ console.error(chalk2.dim("Backfilling profile pictures from source media\u2026"));
1368
+ const res = await backfillAvatars(db, {
1369
+ source,
1370
+ delayMs: cmdOpts.delay,
1371
+ onLog: (m) => process.stderr.write(chalk2.dim(m + `
1372
+ `))
1373
+ });
1374
+ console.log(chalk2.green(`\u2713 ${res.downloaded} avatars downloaded`) + chalk2.dim(` (${res.skipped} already had one or no URL, ${res.failed} failed)`));
1375
+ const total = db.enrichmentStats(source).avatars;
1376
+ console.log(chalk2.dim(`total experts with a named avatar: ${total}`));
1377
+ db.close();
1378
+ });
1323
1379
  program.command("tweets <idOrSlug>").description("Show an expert's stored recent tweets").option("-s, --source <name>", "disambiguate by source").option("-n, --limit <n>", "max tweets", (v) => parseInt(v, 10), 10).action((idOrSlug, cmdOpts) => {
1324
1380
  const db = openDb();
1325
1381
  const e = db.get(idOrSlug, cmdOpts.source);
@@ -10,7 +10,10 @@ export interface ConnectorResult {
10
10
  success: boolean;
11
11
  data?: any;
12
12
  error?: string;
13
+ /** Transient 429 / rate-window limit — back off and retry. */
13
14
  rateLimited?: boolean;
15
+ /** Hard quota/credit exhaustion — retrying won't help until reset/top-up. */
16
+ quotaExhausted?: boolean;
14
17
  }
15
18
  /** Runs `connectors run <connector> <args…> --format json`. */
16
19
  export type ConnectorRunner = (connector: string, args: string[]) => Promise<ConnectorResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["../src/connectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,+DAA+D;AAC/D,MAAM,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAE9F,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,GAAG,SAAe,GAAG,eAAe,CAuBjE;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAqB7C;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAAkB;gBAEjB,IAAI,GAAE,uBAA4B;IAI9C,gEAAgE;IAC1D,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC;IAuBvF,oEAAoE;IAC9D,SAAS,CACb,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO,GACjE,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC;CAwB1D"}
1
+ {"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["../src/connectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,+DAA+D;AAC/D,MAAM,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAE9F,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,GAAG,SAAe,GAAG,eAAe,CA0BjE;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAqB7C;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,GAAG,CAAkB;gBAEjB,IAAI,GAAE,uBAA4B;IAI9C,gEAAgE;IAC1D,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC;IAuBvF,oEAAoE;IAC9D,SAAS,CACb,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAO,GACjE,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC;CAwB1D"}
package/dist/enrich.d.ts CHANGED
@@ -39,7 +39,25 @@ export declare function enrichExpert(db: ExpertsDB, e: Expert, opts: EnrichOptio
39
39
  tweets: number;
40
40
  avatar: boolean;
41
41
  rateLimited: boolean;
42
+ quotaExhausted: boolean;
42
43
  }>;
44
+ export interface AvatarBackfillResult {
45
+ downloaded: number;
46
+ skipped: number;
47
+ failed: number;
48
+ }
49
+ /**
50
+ * Download + properly name profile pictures for every expert that doesn't have
51
+ * a local one yet, using the source's own avatar URL (e.g. intro.co media). This
52
+ * needs no third-party API, so it completes "a named profile picture for every
53
+ * expert" even when X enrichment is unavailable.
54
+ */
55
+ export declare function backfillAvatars(db: ExpertsDB, opts?: {
56
+ source?: string;
57
+ delayMs?: number;
58
+ fetchFn?: typeof fetch;
59
+ onLog?: (m: string) => void;
60
+ }): Promise<AvatarBackfillResult>;
43
61
  /** Resumable, throttled enrichment over all experts with a Twitter handle. */
44
62
  export declare function enrichX(db: ExpertsDB, opts?: EnrichOptions): Promise<EnrichResult>;
45
63
  //# sourceMappingURL=enrich.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"enrich.d.ts","sourceRoot":"","sources":["../src/enrich.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,SAAS,CAAC;AAIvD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,GAAG,MAAM,CAQxF;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,mFAAmF;AACnF,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,EAC5D,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uEAAuE;AACvE,wBAAsB,YAAY,CAChC,EAAE,EAAE,SAAS,EACb,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAoEpG;AAED,8EAA8E;AAC9E,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4D5F"}
1
+ {"version":3,"file":"enrich.d.ts","sourceRoot":"","sources":["../src/enrich.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,SAAS,CAAC;AAIvD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,GAAG,MAAM,CAQxF;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,mFAAmF;AACnF,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,UAAU,CAAC,EAC5D,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uEAAuE;AACvE,wBAAsB,YAAY,CAChC,EAAE,EAAE,SAAS,EACb,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,CAAC,CAyE7H;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,SAAS,EACb,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GACpG,OAAO,CAAC,oBAAoB,CAAC,CA0B/B;AAED,8EAA8E;AAC9E,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAqE5F"}
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { crawlSource, type CrawlResult } from "./crawl";
10
10
  export { getSource, listSources, registerSource, IntroSource } from "./sources";
11
11
  export { IntroClient } from "./sources/intro-api";
12
12
  export { normalizeIntroExpert, slugFromUrl } from "./sources/intro";
13
- export { enrichX, enrichExpert, downloadAvatar, avatarBasename, handleFromSocial, type EnrichOptions, type EnrichResult } from "./enrich";
13
+ export { enrichX, enrichExpert, backfillAvatars, downloadAvatar, avatarBasename, handleFromSocial, type EnrichOptions, type EnrichResult, type AvatarBackfillResult } from "./enrich";
14
14
  export { ConnectorsClient, defaultRunner, extractJson, type ConnectorRunner, type ConnectorResult } from "./connectors";
15
15
  export { inferTags, expertText } from "./graph";
16
16
  export * as format from "./format";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAC1I,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACxH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACtL,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACxH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -855,13 +855,14 @@ function defaultRunner(bin = "connectors") {
855
855
  ]);
856
856
  const blob = `${out}
857
857
  ${err}`;
858
- const rateLimited = /429|rate limit|too many requests/i.test(blob);
858
+ const rateLimited = /429|rate.?limit|too many requests/i.test(blob);
859
+ const quotaExhausted = /credit|quota|usage ?cap|usagecap|monthly cap|capexceeded/i.test(blob);
859
860
  if (code !== 0) {
860
- return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited };
861
+ return { success: false, error: (err || out || `exit ${code}`).trim(), rateLimited, quotaExhausted };
861
862
  }
862
863
  const parsed = extractJson(out);
863
864
  if (parsed === undefined) {
864
- return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited };
865
+ return { success: false, error: `unparseable output: ${out.slice(0, 200)}`, rateLimited, quotaExhausted };
865
866
  }
866
867
  return { success: true, data: parsed };
867
868
  };
@@ -991,11 +992,13 @@ async function enrichExpert(db, e, opts) {
991
992
  const handle = handleFromSocial(e.socials.twitter || "");
992
993
  const now = new Date().toISOString();
993
994
  if (!handle)
994
- return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
995
+ return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
995
996
  const { user, result } = await client.xUser(handle);
996
997
  if (!user) {
998
+ if (result.quotaExhausted)
999
+ return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: true };
997
1000
  if (result.rateLimited)
998
- return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true };
1001
+ return { ok: false, notFound: false, tweets: 0, avatar: false, rateLimited: true, quotaExhausted: false };
999
1002
  db.upsertXProfile({
1000
1003
  source: e.source,
1001
1004
  sourceId: e.sourceId,
@@ -1012,7 +1015,7 @@ async function enrichExpert(db, e, opts) {
1012
1015
  worksOn: "",
1013
1016
  enrichedAt: now
1014
1017
  });
1015
- return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false };
1018
+ return { ok: false, notFound: true, tweets: 0, avatar: false, rateLimited: false, quotaExhausted: false };
1016
1019
  }
1017
1020
  const profile = {
1018
1021
  source: e.source,
@@ -1047,8 +1050,11 @@ async function enrichExpert(db, e, opts) {
1047
1050
  max: opts.tweetMax ?? 100,
1048
1051
  replies: false
1049
1052
  });
1053
+ if (tlResult.quotaExhausted && tweets.length === 0) {
1054
+ return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: false, quotaExhausted: true };
1055
+ }
1050
1056
  if (tlResult.rateLimited && tweets.length === 0) {
1051
- return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true };
1057
+ return { ok: true, notFound: false, tweets: 0, avatar, rateLimited: true, quotaExhausted: false };
1052
1058
  }
1053
1059
  const cutoff = Date.now() - (opts.sinceDays ?? 30) * 86400000;
1054
1060
  const recent = tweets.filter((t) => {
@@ -1072,7 +1078,35 @@ async function enrichExpert(db, e, opts) {
1072
1078
  db.replaceTweets(e.source, e.sourceId, rows);
1073
1079
  tweetCount = rows.length;
1074
1080
  }
1075
- return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false };
1081
+ return { ok: true, notFound: false, tweets: tweetCount, avatar, rateLimited: false, quotaExhausted: false };
1082
+ }
1083
+ async function backfillAvatars(db, opts = {}) {
1084
+ const log = opts.onLog ?? (() => {});
1085
+ const fetchFn = opts.fetchFn ?? fetch;
1086
+ const delayMs = opts.delayMs ?? 150;
1087
+ const experts = db.list({ source: opts.source });
1088
+ const res = { downloaded: 0, skipped: 0, failed: 0 };
1089
+ for (const e of experts) {
1090
+ if (e.avatarLocal || !e.avatar) {
1091
+ res.skipped++;
1092
+ continue;
1093
+ }
1094
+ try {
1095
+ const path = await downloadAvatar(e.avatar, e, fetchFn);
1096
+ if (path) {
1097
+ db.setAvatarLocal(e.source, e.sourceId, path);
1098
+ res.downloaded++;
1099
+ if (res.downloaded % 100 === 0)
1100
+ log(` avatars: ${res.downloaded} downloaded`);
1101
+ } else {
1102
+ res.failed++;
1103
+ }
1104
+ } catch {
1105
+ res.failed++;
1106
+ }
1107
+ await sleep2(delayMs);
1108
+ }
1109
+ return res;
1076
1110
  }
1077
1111
  async function enrichX(db, opts = {}) {
1078
1112
  const log = opts.onLog ?? (() => {});
@@ -1098,6 +1132,13 @@ async function enrichX(db, opts = {}) {
1098
1132
  await sleep2(delayMs);
1099
1133
  continue;
1100
1134
  }
1135
+ if (outcome.quotaExhausted) {
1136
+ res.stoppedEarly = true;
1137
+ res.reason = "X API credits/quota exhausted \u2014 top up or wait for the monthly reset, then re-run the same command to finish the rest";
1138
+ res.attempted--;
1139
+ log(`X API quota exhausted; stopping at ${i}/${targets.length}. ${res.enriched} enriched so far.`);
1140
+ break;
1141
+ }
1101
1142
  if (outcome.rateLimited) {
1102
1143
  consecutiveRateLimits++;
1103
1144
  if (consecutiveRateLimits >= 3) {
@@ -1768,6 +1809,7 @@ export {
1768
1809
  defaultRunner,
1769
1810
  defaultDbPath,
1770
1811
  crawlSource,
1812
+ backfillAvatars,
1771
1813
  avatarBasename,
1772
1814
  IntroSource,
1773
1815
  IntroClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/experts",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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",