lurqrun 0.0.1 → 0.0.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.
package/dist/index.js CHANGED
@@ -70,14 +70,16 @@ var init_types = __esm({
70
70
  });
71
71
 
72
72
  // src/core/constants.ts
73
- var SERVER_NAME, PACKAGE_NAME, VERSION, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
73
+ var SERVER_NAME, PACKAGE_NAME, VERSION, DEFAULT_ENDPOINT, API_KEY_PREFIX, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
74
74
  var init_constants = __esm({
75
75
  "src/core/constants.ts"() {
76
76
  "use strict";
77
77
  init_esm_shims();
78
78
  SERVER_NAME = "lurq";
79
79
  PACKAGE_NAME = "lurqrun";
80
- VERSION = "0.0.1";
80
+ VERSION = "0.0.2";
81
+ DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
82
+ API_KEY_PREFIX = "lurq_live_";
81
83
  EMBEDDING_DIM = 1536;
82
84
  STALENESS_DAYS = 7;
83
85
  HOUR = 60 * 60 * 1e3;
@@ -93,6 +95,14 @@ var init_constants = __esm({
93
95
  });
94
96
 
95
97
  // src/core/config.ts
98
+ var config_exports = {};
99
+ __export(config_exports, {
100
+ ConfigError: () => ConfigError,
101
+ getConfig: () => getConfig,
102
+ loadEnv: () => loadEnv,
103
+ requireConfig: () => requireConfig,
104
+ resetConfigCache: () => resetConfigCache
105
+ });
96
106
  import { config as dotenvConfig } from "dotenv";
97
107
  import { z } from "zod";
98
108
  function loadEnv() {
@@ -112,6 +122,9 @@ ${issues}`);
112
122
  cached = parsed.data;
113
123
  return cached;
114
124
  }
125
+ function resetConfigCache() {
126
+ cached = void 0;
127
+ }
115
128
  function requireConfig(keys) {
116
129
  const config = getConfig();
117
130
  const missing = keys.filter((k) => config[k] === void 0 || config[k] === "");
@@ -139,7 +152,18 @@ var init_config = __esm({
139
152
  SUMMARY_API_KEY: z.string().min(1).optional(),
140
153
  SUMMARY_MODEL: z.string().min(1).default("gpt-4o-mini"),
141
154
  LURQ_SYNC_CONCURRENCY: z.coerce.number().int().positive().max(50).default(5),
142
- LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info")
155
+ LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
156
+ // Hosted HTTP service (`serve-http`). Server-side only.
157
+ PORT: z.coerce.number().int().positive().default(8080),
158
+ /** Per-API-key rate limit: max requests per window. */
159
+ LURQ_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(120),
160
+ /** Coarser per-IP rate limit (blunts unauthenticated floods before auth). */
161
+ LURQ_IP_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(240),
162
+ /** Rate-limit window, milliseconds (applies to both limiters). */
163
+ LURQ_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(6e4),
164
+ // Client-side (install wizard / CLI talking to a remote endpoint).
165
+ LURQ_ENDPOINT: z.string().url().optional(),
166
+ LURQ_API_KEY: z.string().min(1).optional()
143
167
  });
144
168
  ConfigError = class extends Error {
145
169
  constructor(message) {
@@ -188,13 +212,17 @@ var init_logger = __esm({
188
212
  // src/db/schema.ts
189
213
  var schema_exports = {};
190
214
  __export(schema_exports, {
215
+ apiKeys: () => apiKeys,
216
+ discoveryQueue: () => discoveryQueue,
191
217
  packages: () => packages,
192
218
  seedPackages: () => seedPackages,
193
219
  syncRuns: () => syncRuns
194
220
  });
221
+ import { sql } from "drizzle-orm";
195
222
  import {
196
223
  bigint,
197
224
  boolean,
225
+ customType,
198
226
  index,
199
227
  integer,
200
228
  jsonb,
@@ -205,12 +233,17 @@ import {
205
233
  timestamp,
206
234
  vector
207
235
  } from "drizzle-orm/pg-core";
208
- var ts, packages, syncRuns, seedPackages;
236
+ var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys;
209
237
  var init_schema = __esm({
210
238
  "src/db/schema.ts"() {
211
239
  "use strict";
212
240
  init_esm_shims();
213
241
  init_constants();
242
+ tsvector = customType({
243
+ dataType() {
244
+ return "tsvector";
245
+ }
246
+ });
214
247
  ts = (name) => timestamp(name, { withTimezone: true, mode: "date" });
215
248
  packages = pgTable(
216
249
  "packages",
@@ -220,6 +253,8 @@ var init_schema = __esm({
220
253
  ecosystem: text("ecosystem").notNull().default("npm"),
221
254
  // Classification + descriptive text
222
255
  category: text("category").$type(),
256
+ /** Whether `category` was hand-curated or inferred at ingest (§2A). */
257
+ categorySource: text("category_source").$type(),
223
258
  description: text("description"),
224
259
  summary: text("summary"),
225
260
  repoUrl: text("repo_url"),
@@ -245,10 +280,19 @@ var init_schema = __esm({
245
280
  advisories: jsonb("advisories").$type(),
246
281
  // Computed outputs
247
282
  healthScore: integer("health_score"),
283
+ /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
284
+ * ranking time (composite); never folded into health_score itself. */
285
+ qualityScore: integer("quality_score"),
248
286
  confidence: text("confidence").$type(),
249
287
  scoreBreakdown: jsonb("score_breakdown").$type(),
250
288
  usageGuide: jsonb("usage_guide").$type(),
251
289
  embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
290
+ // Lexical search vector (§3): name weighted highest (A), then category (B),
291
+ // then summary/description (C). Generated + STORED so it stays in sync with
292
+ // the row automatically; indexed with GIN for fast `@@` matching.
293
+ searchVector: tsvector("search_vector").generatedAlwaysAs(
294
+ sql`setweight(to_tsvector('english', coalesce(name, '')), 'A') || setweight(to_tsvector('english', coalesce(category, '')), 'B') || setweight(to_tsvector('english', coalesce(summary, description, '')), 'C')`
295
+ ),
252
296
  // Freshness + bookkeeping
253
297
  dataAsOf: timestamp("data_as_of", { withTimezone: true, mode: "date" }),
254
298
  createdAt: ts("created_at").notNull().defaultNow(),
@@ -258,7 +302,9 @@ var init_schema = __esm({
258
302
  index("packages_category_idx").on(table2.category),
259
303
  index("packages_health_score_idx").on(table2.healthScore),
260
304
  // pgvector HNSW index for cosine similarity search (§11).
261
- index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops"))
305
+ index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops")),
306
+ // GIN index for lexical full-text search (§3).
307
+ index("packages_search_vector_idx").using("gin", table2.searchVector)
262
308
  ]
263
309
  );
264
310
  syncRuns = pgTable("sync_runs", {
@@ -275,6 +321,38 @@ var init_schema = __esm({
275
321
  category: text("category").$type(),
276
322
  addedAt: ts("added_at").notNull().defaultNow()
277
323
  });
324
+ discoveryQueue = pgTable(
325
+ "discovery_queue",
326
+ {
327
+ id: serial("id").primaryKey(),
328
+ name: text("name").notNull().unique(),
329
+ discoveredVia: text("discovered_via").$type().notNull(),
330
+ /** Lightweight quality-only pre-score (§2B). Null until the gate runs. */
331
+ preScore: integer("pre_score"),
332
+ status: text("status").$type().notNull().default("pending"),
333
+ discoveredAt: ts("discovered_at").notNull().defaultNow()
334
+ },
335
+ (table2) => [index("discovery_queue_status_idx").on(table2.status)]
336
+ );
337
+ apiKeys = pgTable(
338
+ "api_keys",
339
+ {
340
+ id: serial("id").primaryKey(),
341
+ /** sha256 hex of the full key — the only form persisted. */
342
+ keyHash: text("key_hash").notNull().unique(),
343
+ /** Identifiable display prefix, e.g. `lurq_live_ab12cd` (safe to show/log). */
344
+ prefix: text("prefix").notNull(),
345
+ /** Free-text label (owner / org / purpose). */
346
+ label: text("label"),
347
+ /** Reserved for self-serve issuance: maps to a Clerk user id. */
348
+ ownerId: text("owner_id"),
349
+ tier: text("tier").notNull().default("free"),
350
+ createdAt: ts("created_at").notNull().defaultNow(),
351
+ lastUsedAt: ts("last_used_at"),
352
+ revokedAt: ts("revoked_at")
353
+ },
354
+ (table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
355
+ );
278
356
  }
279
357
  });
280
358
 
@@ -283,10 +361,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
283
361
  import postgres from "postgres";
284
362
  function createDb(opts = {}) {
285
363
  const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
286
- const sql6 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
364
+ const sql8 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
287
365
  } });
288
- const db = drizzle(sql6, { schema: schema_exports });
289
- return { db, sql: sql6, close: () => sql6.end() };
366
+ const db = drizzle(sql8, { schema: schema_exports });
367
+ return { db, sql: sql8, close: () => sql8.end() };
290
368
  }
291
369
  var init_client = __esm({
292
370
  "src/db/client.ts"() {
@@ -530,8 +608,9 @@ function parseNpmRegistry(json2) {
530
608
  const { repoUrl, repo } = normalizeRepoUrl(repository, homepage);
531
609
  const deprecated = Boolean(latestManifest?.deprecated ?? json2?.deprecated);
532
610
  const maintainers = Array.isArray(json2?.maintainers) ? json2.maintainers.length : null;
611
+ const name = json2?.name ?? "";
533
612
  return {
534
- name: json2?.name ?? "",
613
+ name,
535
614
  description: json2?.description ?? latestManifest?.description ?? null,
536
615
  latestVersion: latest,
537
616
  license: pickLicense(json2?.license ?? latestManifest?.license),
@@ -542,9 +621,40 @@ function parseNpmRegistry(json2) {
542
621
  lastReleaseAt: toDate(latest ? time?.[latest] : null) ?? toDate(time?.modified),
543
622
  deprecated,
544
623
  maintainersCount: maintainers,
545
- readme: typeof json2?.readme === "string" ? json2.readme : null
624
+ readme: typeof json2?.readme === "string" ? json2.readme : null,
625
+ keywords: parseKeywords(json2?.keywords ?? latestManifest?.keywords),
626
+ hasTypes: detectTypes(name, latestManifest),
627
+ hasTestScript: detectTestScript(latestManifest),
628
+ directDependenciesCount: countDeps(latestManifest?.dependencies),
629
+ hasProvenance: detectProvenance(latestManifest)
546
630
  };
547
631
  }
632
+ function parseKeywords(value) {
633
+ if (!Array.isArray(value)) return [];
634
+ return value.filter((k) => typeof k === "string");
635
+ }
636
+ function detectTypes(name, manifest) {
637
+ if (typeof manifest?.types === "string" || typeof manifest?.typings === "string") return true;
638
+ if (name.startsWith("@types/")) return true;
639
+ const exportsField = manifest?.exports;
640
+ if (exportsField && typeof exportsField === "object") {
641
+ const serialized = JSON.stringify(exportsField);
642
+ if (serialized.includes('"types"')) return true;
643
+ }
644
+ return false;
645
+ }
646
+ function detectTestScript(manifest) {
647
+ const test = manifest?.scripts?.test;
648
+ if (typeof test !== "string" || test.trim() === "") return false;
649
+ return !/no test specified/i.test(test);
650
+ }
651
+ function countDeps(deps) {
652
+ if (!deps || typeof deps !== "object") return 0;
653
+ return Object.keys(deps).length;
654
+ }
655
+ function detectProvenance(manifest) {
656
+ return Boolean(manifest?.dist?.attestations);
657
+ }
548
658
  async function fetchNpmRegistry(name, fetchImpl) {
549
659
  const url = `https://${HOST}/${encodeNpmName(name)}`;
550
660
  const { data } = await httpGetJson(url, {
@@ -582,6 +692,32 @@ var init_npmRegistry = __esm({
582
692
  }
583
693
  });
584
694
 
695
+ // src/ingestion/sources/npmSearch.ts
696
+ async function searchNpm(query, size = 20, fetchImpl) {
697
+ const url = `https://${HOST2}/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`;
698
+ try {
699
+ const { data } = await httpGetJson(url, {
700
+ host: HOST2,
701
+ ttlMs: CACHE_TTL.npmRegistry,
702
+ fetchImpl
703
+ });
704
+ const objects = Array.isArray(data?.objects) ? data.objects : [];
705
+ return objects.map((o) => ({ name: o?.package?.name, date: o?.package?.date ?? null })).filter((h) => typeof h.name === "string");
706
+ } catch {
707
+ return [];
708
+ }
709
+ }
710
+ var HOST2;
711
+ var init_npmSearch = __esm({
712
+ "src/ingestion/sources/npmSearch.ts"() {
713
+ "use strict";
714
+ init_esm_shims();
715
+ init_constants();
716
+ init_http();
717
+ HOST2 = "registry.npmjs.org";
718
+ }
719
+ });
720
+
585
721
  // src/ingestion/sources/npmDownloads.ts
586
722
  function parseWeeklyDownloads(json2) {
587
723
  const value = json2?.downloads;
@@ -627,8 +763,8 @@ function chunk(items, size) {
627
763
  async function fetchWeeklyDownloads(name, fetchImpl) {
628
764
  try {
629
765
  const { data } = await httpGetJson(
630
- `https://${HOST2}/downloads/point/last-week/${name}`,
631
- { host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
766
+ `https://${HOST3}/downloads/point/last-week/${name}`,
767
+ { host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
632
768
  );
633
769
  return parseWeeklyDownloads(data);
634
770
  } catch (err) {
@@ -639,8 +775,8 @@ async function fetchWeeklyDownloads(name, fetchImpl) {
639
775
  async function fetchDownloadGrowth(name, fetchImpl) {
640
776
  try {
641
777
  const { data } = await httpGetJson(
642
- `https://${HOST2}/downloads/range/${last90DayRange()}/${name}`,
643
- { host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
778
+ `https://${HOST3}/downloads/range/${last90DayRange()}/${name}`,
779
+ { host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
644
780
  );
645
781
  return parseDownloadGrowth(data);
646
782
  } catch {
@@ -654,8 +790,8 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
654
790
  for (const batch of chunk(unscoped, BULK_CHUNK)) {
655
791
  try {
656
792
  const { data } = await httpGetJson(
657
- `https://${HOST2}/downloads/point/last-week/${batch.join(",")}`,
658
- { host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
793
+ `https://${HOST3}/downloads/point/last-week/${batch.join(",")}`,
794
+ { host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
659
795
  );
660
796
  for (const [k, v] of parseBulkWeekly(data, batch)) result.set(k, v);
661
797
  } catch {
@@ -669,14 +805,14 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
669
805
  }
670
806
  return result;
671
807
  }
672
- var HOST2, BULK_CHUNK;
808
+ var HOST3, BULK_CHUNK;
673
809
  var init_npmDownloads = __esm({
674
810
  "src/ingestion/sources/npmDownloads.ts"() {
675
811
  "use strict";
676
812
  init_esm_shims();
677
813
  init_constants();
678
814
  init_http();
679
- HOST2 = "api.npmjs.org";
815
+ HOST3 = "api.npmjs.org";
680
816
  BULK_CHUNK = 128;
681
817
  }
682
818
  });
@@ -700,7 +836,7 @@ function parseGithub(json2, now = /* @__PURE__ */ new Date()) {
700
836
  async function fetchGithubRepo(owner, repo, token, fetchImpl) {
701
837
  const body = JSON.stringify({ query: QUERY, variables: { owner, name: repo } });
702
838
  const { data } = await httpRequest(ENDPOINT, {
703
- host: HOST3,
839
+ host: HOST4,
704
840
  ttlMs: CACHE_TTL.github,
705
841
  method: "POST",
706
842
  headers: {
@@ -715,15 +851,15 @@ async function fetchGithubRepo(owner, repo, token, fetchImpl) {
715
851
  });
716
852
  return parseGithub(data);
717
853
  }
718
- var HOST3, ENDPOINT, QUERY, ONE_YEAR_MS;
854
+ var HOST4, ENDPOINT, QUERY, ONE_YEAR_MS;
719
855
  var init_github = __esm({
720
856
  "src/ingestion/sources/github.ts"() {
721
857
  "use strict";
722
858
  init_esm_shims();
723
859
  init_constants();
724
860
  init_http();
725
- HOST3 = "api.github.com";
726
- ENDPOINT = `https://${HOST3}/graphql`;
861
+ HOST4 = "api.github.com";
862
+ ENDPOINT = `https://${HOST4}/graphql`;
727
863
  QUERY = `query($owner:String!, $name:String!) {
728
864
  repository(owner:$owner, name:$name) {
729
865
  stargazerCount
@@ -769,10 +905,10 @@ function parseAdvisoryDetail(json2) {
769
905
  }
770
906
  async function fetchScorecard(owner, repo, fetchImpl) {
771
907
  const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
772
- const url = `https://${HOST4}/v3/projects/${projectKey}`;
908
+ const url = `https://${HOST5}/v3/projects/${projectKey}`;
773
909
  try {
774
910
  const { data } = await httpGetJson(url, {
775
- host: HOST4,
911
+ host: HOST5,
776
912
  ttlMs: CACHE_TTL.depsDev,
777
913
  fetchImpl
778
914
  });
@@ -782,11 +918,11 @@ async function fetchScorecard(owner, repo, fetchImpl) {
782
918
  }
783
919
  }
784
920
  async function fetchAdvisories(name, version, fetchImpl) {
785
- const versionUrl = `https://${HOST4}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
921
+ const versionUrl = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
786
922
  let keys = [];
787
923
  try {
788
924
  const { data } = await httpGetJson(versionUrl, {
789
- host: HOST4,
925
+ host: HOST5,
790
926
  ttlMs: CACHE_TTL.depsDev,
791
927
  fetchImpl
792
928
  });
@@ -796,8 +932,8 @@ async function fetchAdvisories(name, version, fetchImpl) {
796
932
  }
797
933
  const details = await Promise.allSettled(
798
934
  keys.map(
799
- (key) => httpGetJson(`https://${HOST4}/v3/advisories/${encodeURIComponent(key)}`, {
800
- host: HOST4,
935
+ (key) => httpGetJson(`https://${HOST5}/v3/advisories/${encodeURIComponent(key)}`, {
936
+ host: HOST5,
801
937
  ttlMs: CACHE_TTL.depsDev,
802
938
  fetchImpl
803
939
  })
@@ -807,6 +943,21 @@ async function fetchAdvisories(name, version, fetchImpl) {
807
943
  (d) => d.status === "fulfilled"
808
944
  ).map((d) => parseAdvisoryDetail(d.value.data));
809
945
  }
946
+ async function fetchDependencyNames(name, version, fetchImpl) {
947
+ const url = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}:dependencies`;
948
+ try {
949
+ const { data } = await httpGetJson(url, {
950
+ host: HOST5,
951
+ ttlMs: CACHE_TTL.depsDev,
952
+ fetchImpl
953
+ });
954
+ const nodes = Array.isArray(data?.nodes) ? data.nodes : [];
955
+ const names = nodes.map((n) => n?.versionKey?.name).filter((n) => typeof n === "string" && n !== name);
956
+ return [...new Set(names)];
957
+ } catch {
958
+ return [];
959
+ }
960
+ }
810
961
  async function fetchDepsDev(name, version, repo, fetchImpl) {
811
962
  const [scorecard, advisories] = await Promise.all([
812
963
  repo ? fetchScorecard(repo.owner, repo.repo, fetchImpl) : Promise.resolve(null),
@@ -814,14 +965,14 @@ async function fetchDepsDev(name, version, repo, fetchImpl) {
814
965
  ]);
815
966
  return { scorecard, advisories };
816
967
  }
817
- var HOST4, MAX_ADVISORIES;
968
+ var HOST5, MAX_ADVISORIES;
818
969
  var init_depsDev = __esm({
819
970
  "src/ingestion/sources/depsDev.ts"() {
820
971
  "use strict";
821
972
  init_esm_shims();
822
973
  init_constants();
823
974
  init_http();
824
- HOST4 = "api.deps.dev";
975
+ HOST5 = "api.deps.dev";
825
976
  MAX_ADVISORIES = 10;
826
977
  }
827
978
  });
@@ -834,10 +985,10 @@ function parseBundleSize(json2) {
834
985
  }
835
986
  async function fetchBundlephobia(name, category, fetchImpl) {
836
987
  if (!isFrontendCategory(category)) return { bundleMinGzipKb: null };
837
- const url = `https://${HOST5}/api/size?package=${encodeURIComponent(name)}`;
988
+ const url = `https://${HOST6}/api/size?package=${encodeURIComponent(name)}`;
838
989
  try {
839
990
  const { data } = await httpGetJson(url, {
840
- host: HOST5,
991
+ host: HOST6,
841
992
  ttlMs: CACHE_TTL.bundlephobia,
842
993
  timeoutMs: 12e3,
843
994
  retries: 1,
@@ -848,7 +999,7 @@ async function fetchBundlephobia(name, category, fetchImpl) {
848
999
  return { bundleMinGzipKb: null };
849
1000
  }
850
1001
  }
851
- var HOST5;
1002
+ var HOST6;
852
1003
  var init_bundlephobia = __esm({
853
1004
  "src/ingestion/sources/bundlephobia.ts"() {
854
1005
  "use strict";
@@ -856,7 +1007,7 @@ var init_bundlephobia = __esm({
856
1007
  init_constants();
857
1008
  init_http();
858
1009
  init_types();
859
- HOST5 = "bundlephobia.com";
1010
+ HOST6 = "bundlephobia.com";
860
1011
  }
861
1012
  });
862
1013
 
@@ -866,6 +1017,7 @@ var init_sources = __esm({
866
1017
  "use strict";
867
1018
  init_esm_shims();
868
1019
  init_npmRegistry();
1020
+ init_npmSearch();
869
1021
  init_npmDownloads();
870
1022
  init_github();
871
1023
  init_depsDev();
@@ -876,10 +1028,10 @@ var init_sources = __esm({
876
1028
  // src/ingestion/sources/githubReadme.ts
877
1029
  async function fetchGithubReadme(owner, repo, fetchImpl) {
878
1030
  for (const file of CANDIDATES) {
879
- const url = `https://${HOST6}/${owner}/${repo}/HEAD/${file}`;
1031
+ const url = `https://${HOST7}/${owner}/${repo}/HEAD/${file}`;
880
1032
  try {
881
1033
  const { data } = await httpRequest(url, {
882
- host: HOST6,
1034
+ host: HOST7,
883
1035
  ttlMs: CACHE_TTL.github,
884
1036
  accept: "text",
885
1037
  retries: 0,
@@ -891,14 +1043,14 @@ async function fetchGithubReadme(owner, repo, fetchImpl) {
891
1043
  }
892
1044
  return null;
893
1045
  }
894
- var HOST6, CANDIDATES;
1046
+ var HOST7, CANDIDATES;
895
1047
  var init_githubReadme = __esm({
896
1048
  "src/ingestion/sources/githubReadme.ts"() {
897
1049
  "use strict";
898
1050
  init_esm_shims();
899
1051
  init_constants();
900
1052
  init_http();
901
- HOST6 = "raw.githubusercontent.com";
1053
+ HOST7 = "raw.githubusercontent.com";
902
1054
  CANDIDATES = ["README.md", "readme.md", "README.markdown", "README"];
903
1055
  }
904
1056
  });
@@ -915,6 +1067,7 @@ function truncateSentences(text2, max = 3) {
915
1067
  }
916
1068
  function buildUserPrompt(input) {
917
1069
  const readme = (input.readme ?? "").slice(0, 4e3);
1070
+ const needsCategory = input.category == null;
918
1071
  return [
919
1072
  `Package: ${input.name}`,
920
1073
  `Category: ${input.category ?? "unknown"}`,
@@ -928,7 +1081,8 @@ ${readme || "(none)"}`,
928
1081
  '- "whenToUse": one sentence on the ideal use case.',
929
1082
  '- "whenNotToUse": one sentence on when to pick something else (or "").',
930
1083
  '- "whereItFits": one sentence on its architectural role.',
931
- '- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".'
1084
+ '- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".',
1085
+ ...needsCategory ? [`- "category": the single best fit from this list: ${CATEGORIES.join(", ")}.`] : []
932
1086
  ].join("\n");
933
1087
  }
934
1088
  function str(value) {
@@ -962,6 +1116,7 @@ var init_summarize = __esm({
962
1116
  "src/ingestion/summarize.ts"() {
963
1117
  "use strict";
964
1118
  init_esm_shims();
1119
+ init_types();
965
1120
  init_config();
966
1121
  init_http();
967
1122
  init_logger();
@@ -1003,7 +1158,7 @@ var init_summarize = __esm({
1003
1158
  whereItFits: `Sits at ${role}.`,
1004
1159
  context7Hint: context7Hint(input.name)
1005
1160
  };
1006
- return { summary, usageGuide: guide };
1161
+ return { summary, usageGuide: guide, inferredCategory: null };
1007
1162
  }
1008
1163
  };
1009
1164
  SYSTEM_PROMPT = "You summarize npm packages factually for an AI coding agent choosing dependencies. Use ONLY the provided README/description. Never invent capabilities or APIs. Be concise and concrete. Respond with a JSON object.";
@@ -1051,7 +1206,13 @@ var init_summarize = __esm({
1051
1206
  howToWireIn: str(parsed.howToWireIn) || void 0,
1052
1207
  context7Hint: context7Hint(input.name)
1053
1208
  };
1054
- return { summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null, usageGuide: guide };
1209
+ const rawCategory = str(parsed.category);
1210
+ const inferredCategory = rawCategory && isCategory(rawCategory) ? rawCategory : null;
1211
+ return {
1212
+ summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null,
1213
+ usageGuide: guide,
1214
+ inferredCategory
1215
+ };
1055
1216
  } catch (err) {
1056
1217
  logger.warn(`summary LLM failed for ${input.name}, using fallback: ${err.message}`);
1057
1218
  return new FallbackSummaryProvider().generate(input);
@@ -1103,18 +1264,227 @@ var init_collect = __esm({
1103
1264
  }
1104
1265
  });
1105
1266
 
1267
+ // src/search/categoryInference.ts
1268
+ function inferCategory(need) {
1269
+ const text2 = need.toLowerCase();
1270
+ for (const rule of RULES) {
1271
+ if (rule.patterns.test(text2)) return rule.category;
1272
+ }
1273
+ return null;
1274
+ }
1275
+ function inferCategoryFromSignals(signals) {
1276
+ const r = signals.registry;
1277
+ const keywords = (r?.keywords ?? []).join(" ");
1278
+ const text2 = [
1279
+ signals.name,
1280
+ keywords,
1281
+ keywords,
1282
+ // weight keywords higher via repetition
1283
+ r?.description ?? "",
1284
+ (r?.readme ?? "").slice(0, 600)
1285
+ ].filter(Boolean).join(" ");
1286
+ return inferCategory(text2);
1287
+ }
1288
+ var RULES;
1289
+ var init_categoryInference = __esm({
1290
+ "src/search/categoryInference.ts"() {
1291
+ "use strict";
1292
+ init_esm_shims();
1293
+ RULES = [
1294
+ { category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
1295
+ { category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
1296
+ { category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
1297
+ { category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
1298
+ { category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
1299
+ { category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
1300
+ { category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
1301
+ { category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
1302
+ { category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
1303
+ { category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
1304
+ { category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
1305
+ { category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
1306
+ { category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
1307
+ { category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
1308
+ { category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
1309
+ // date-time precedes linting: "date formatting" must not be captured by linting's
1310
+ // bare "formatting" token before the date rule is reached (first-match-wins).
1311
+ { category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
1312
+ { category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
1313
+ { category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
1314
+ { category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
1315
+ { category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
1316
+ { category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
1317
+ { category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
1318
+ ];
1319
+ }
1320
+ });
1321
+
1322
+ // src/core/paths.ts
1323
+ import { existsSync as existsSync2 } from "fs";
1324
+ import { homedir as homedir2 } from "os";
1325
+ import { dirname as dirname2, join as join2 } from "path";
1326
+ import { fileURLToPath as fileURLToPath2 } from "url";
1327
+ function packageRoot() {
1328
+ if (cachedRoot) return cachedRoot;
1329
+ let dir = dirname2(fileURLToPath2(import.meta.url));
1330
+ for (; ; ) {
1331
+ if (existsSync2(join2(dir, "package.json"))) {
1332
+ cachedRoot = dir;
1333
+ return dir;
1334
+ }
1335
+ const parent = dirname2(dir);
1336
+ if (parent === dir) break;
1337
+ dir = parent;
1338
+ }
1339
+ cachedRoot = process.cwd();
1340
+ return cachedRoot;
1341
+ }
1342
+ function migrationsDir() {
1343
+ return join2(packageRoot(), "drizzle");
1344
+ }
1345
+ function seedJsonPath() {
1346
+ return join2(packageRoot(), "src", "data", "seed.json");
1347
+ }
1348
+ function userWeightsPath() {
1349
+ const base = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
1350
+ return join2(base, "lurq", "weights.json");
1351
+ }
1352
+ function projectWeightsPath() {
1353
+ return join2(process.cwd(), ".lurq", "weights.json");
1354
+ }
1355
+ var cachedRoot;
1356
+ var init_paths = __esm({
1357
+ "src/core/paths.ts"() {
1358
+ "use strict";
1359
+ init_esm_shims();
1360
+ }
1361
+ });
1362
+
1106
1363
  // src/scoring/weights.ts
1107
- var HEALTH_WEIGHTS, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, CONFIDENCE;
1364
+ import { existsSync as existsSync3, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
1365
+ import { dirname as dirname3 } from "path";
1366
+ function activeWeightsPath() {
1367
+ if (existsSync3(projectWeightsPath())) return { path: projectWeightsPath(), source: "project-config" };
1368
+ if (existsSync3(userWeightsPath())) return { path: userWeightsPath(), source: "user-config" };
1369
+ return null;
1370
+ }
1371
+ function loadWeights() {
1372
+ if (cachedWeights) return cachedWeights;
1373
+ let merged = structuredClone(DEFAULT_WEIGHTS);
1374
+ const active = activeWeightsPath();
1375
+ if (active) {
1376
+ try {
1377
+ const fromFile = JSON.parse(readFileSync(active.path, "utf8"));
1378
+ merged = mergeWeights(merged, fromFile);
1379
+ } catch {
1380
+ }
1381
+ }
1382
+ const envLambda = process.env.LURQ_COMPOSITE_LAMBDA;
1383
+ if (envLambda !== void 0 && envLambda !== "") {
1384
+ const n = Number(envLambda);
1385
+ if (Number.isFinite(n)) merged.composite.lambda = n;
1386
+ }
1387
+ cachedWeights = validateWeights(merged).weights;
1388
+ return cachedWeights;
1389
+ }
1390
+ function resetWeightsCache() {
1391
+ cachedWeights = void 0;
1392
+ }
1393
+ function mergeWeights(base, over) {
1394
+ return {
1395
+ health: { ...base.health, ...over.health ?? {} },
1396
+ composite: { ...base.composite, ...over.composite ?? {} }
1397
+ };
1398
+ }
1399
+ function validateWeights(w) {
1400
+ const lambda = Math.max(0, Math.min(1, w.composite.lambda));
1401
+ const h = w.health;
1402
+ const sum = h.maintenance + h.adoption + h.reliability + h.efficiency;
1403
+ let normalized = false;
1404
+ let health = h;
1405
+ if (sum <= 0) {
1406
+ health = { ...DEFAULT_WEIGHTS.health };
1407
+ normalized = true;
1408
+ } else if (Math.abs(sum - 1) > 1e-6) {
1409
+ health = {
1410
+ maintenance: h.maintenance / sum,
1411
+ adoption: h.adoption / sum,
1412
+ reliability: h.reliability / sum,
1413
+ efficiency: h.efficiency / sum
1414
+ };
1415
+ normalized = true;
1416
+ }
1417
+ return { weights: { health, composite: { lambda } }, normalized };
1418
+ }
1419
+ function settableKeys() {
1420
+ return Object.keys(SETTABLE);
1421
+ }
1422
+ function applyOverrides(base, sets) {
1423
+ const next = structuredClone(base);
1424
+ for (const entry of sets) {
1425
+ const eq7 = entry.indexOf("=");
1426
+ if (eq7 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
1427
+ const key = entry.slice(0, eq7).trim();
1428
+ const value = Number(entry.slice(eq7 + 1).trim());
1429
+ const apply = SETTABLE[key];
1430
+ if (!apply) {
1431
+ throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
1432
+ }
1433
+ if (!Number.isFinite(value)) throw new Error(`Value for "${key}" must be a number.`);
1434
+ apply(next, value);
1435
+ }
1436
+ return next;
1437
+ }
1438
+ function saveWeights(w, scope = "user") {
1439
+ const path2 = scope === "project" ? projectWeightsPath() : userWeightsPath();
1440
+ mkdirSync(dirname3(path2), { recursive: true });
1441
+ writeFileSync(path2, JSON.stringify(w, null, 2) + "\n", "utf8");
1442
+ resetWeightsCache();
1443
+ return path2;
1444
+ }
1445
+ function resetWeights() {
1446
+ const removed = [];
1447
+ for (const path2 of [projectWeightsPath(), userWeightsPath()]) {
1448
+ if (existsSync3(path2)) {
1449
+ rmSync(path2);
1450
+ removed.push(path2);
1451
+ }
1452
+ }
1453
+ resetWeightsCache();
1454
+ return removed;
1455
+ }
1456
+ var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DISCOVERY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
1108
1457
  var init_weights = __esm({
1109
1458
  "src/scoring/weights.ts"() {
1110
1459
  "use strict";
1111
1460
  init_esm_shims();
1461
+ init_paths();
1112
1462
  HEALTH_WEIGHTS = {
1113
1463
  maintenance: 0.35,
1114
1464
  adoption: 0.3,
1115
1465
  reliability: 0.25,
1116
1466
  efficiency: 0.1
1117
1467
  };
1468
+ COMPOSITE = {
1469
+ lambda: 0.35
1470
+ };
1471
+ QUALITY_WEIGHTS = {
1472
+ types: 0.25,
1473
+ tests: 0.2,
1474
+ docs: 0.2,
1475
+ changelog: 0.1,
1476
+ deps: 0.1,
1477
+ license: 0.1,
1478
+ provenance: 0.05
1479
+ };
1480
+ QUALITY = {
1481
+ /** README at/above this many characters scores full marks on doc length. */
1482
+ docsFullLengthChars: 1200,
1483
+ /** Direct-dependency count at/above this scores 0 on the deps component (log-scaled). */
1484
+ depsLogMax: 30,
1485
+ /** A license string present but not OSI-recognized scores this (vs 100 / 0). */
1486
+ licenseUnrecognizedScore: 60
1487
+ };
1118
1488
  MAINTENANCE_WEIGHTS = {
1119
1489
  recency: 0.5,
1120
1490
  cadence: 0.25,
@@ -1162,6 +1532,35 @@ var init_weights = __esm({
1162
1532
  /** Bundle size at the category median maps to this score; smaller → higher. */
1163
1533
  medianScore: 50
1164
1534
  };
1535
+ DISCOVERY = {
1536
+ /** A queued candidate must clear this quality pre-score to graduate to ingest. */
1537
+ minPreScore: 45,
1538
+ /** Max candidates fully ingested per crawler run (cost bound). */
1539
+ perRunCap: 25,
1540
+ /** npm-search hits to pull per category keyword. */
1541
+ searchSizePerCategory: 10,
1542
+ /** Max dependency-graph neighbors to enqueue per tracked seed. */
1543
+ graphNeighborsPerSeed: 20
1544
+ };
1545
+ DEFAULT_WEIGHTS = {
1546
+ health: { ...HEALTH_WEIGHTS },
1547
+ composite: { ...COMPOSITE }
1548
+ };
1549
+ SETTABLE = {
1550
+ "health.maintenance": (w, v) => w.health.maintenance = v,
1551
+ "health.adoption": (w, v) => w.health.adoption = v,
1552
+ "health.reliability": (w, v) => w.health.reliability = v,
1553
+ "health.efficiency": (w, v) => w.health.efficiency = v,
1554
+ "composite.lambda": (w, v) => w.composite.lambda = v
1555
+ };
1556
+ WEIGHT_EXPLANATIONS = {
1557
+ maintenance: "release recency, cadence, and issue close-ratio (weights.ts MAINTENANCE_WEIGHTS).",
1558
+ adoption: "weekly downloads (log-scaled), stars, and 90-day growth (weights.ts ADOPTION).",
1559
+ reliability: "OpenSSF Scorecard scaled 0\u2013100, minus advisory penalties (weights.ts RELIABILITY).",
1560
+ efficiency: "bundle size vs the category median; frontend categories only (weights.ts EFFICIENCY).",
1561
+ quality: "intrinsic, adoption-independent: types, tests, docs, changelog, deps, license, provenance (weights.ts QUALITY_WEIGHTS). A standalone axis \u2014 it does NOT feed health.",
1562
+ lambda: "how much the default sort composite favors quality over health: (1\u2212\u03BB)\xB7health + \u03BB\xB7quality."
1563
+ };
1165
1564
  CONFIDENCE = {
1166
1565
  proven: {
1167
1566
  minWeeklyDownloads: 1e5,
@@ -1172,6 +1571,11 @@ var init_weights = __esm({
1172
1571
  minWeeklyDownloads: 5e3,
1173
1572
  strongGrowth: 0.5,
1174
1573
  maxLastReleaseMonths: 9
1574
+ },
1575
+ /** `promising` (§1): new but intrinsically high-quality, regardless of adoption. */
1576
+ promising: {
1577
+ minQuality: 70,
1578
+ maxLastReleaseMonths: 12
1175
1579
  }
1176
1580
  };
1177
1581
  }
@@ -1180,6 +1584,7 @@ var init_weights = __esm({
1180
1584
  // src/scoring/score.ts
1181
1585
  function toScoringInput(signals, category) {
1182
1586
  const { registry, downloads, github, depsDev, bundle } = signals;
1587
+ const readme = registry?.readme ?? null;
1183
1588
  return {
1184
1589
  weeklyDownloads: downloads?.weeklyDownloads ?? null,
1185
1590
  downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
@@ -1196,7 +1601,18 @@ function toScoringInput(signals, category) {
1196
1601
  deprecated: registry?.deprecated ?? false,
1197
1602
  archived: github?.archived ?? false,
1198
1603
  bundleMinGzipKb: bundle?.bundleMinGzipKb ?? null,
1199
- category
1604
+ category,
1605
+ // Quality signals — null (not false) when the manifest itself is missing, so
1606
+ // weightedAverage drops the component rather than scoring it 0.
1607
+ hasTypes: registry ? registry.hasTypes : null,
1608
+ hasTestScript: registry ? registry.hasTestScript : null,
1609
+ readmeLength: readme !== null ? readme.length : null,
1610
+ hasExamples: readme !== null ? /```/.test(readme) : null,
1611
+ hasHomepage: registry ? registry.homepage !== null : null,
1612
+ hasReleaseNotes: github?.releasesLast12mo != null ? github.releasesLast12mo > 0 : null,
1613
+ directDependenciesCount: registry?.directDependenciesCount ?? null,
1614
+ license: registry?.license ?? null,
1615
+ hasProvenance: registry ? registry.hasProvenance : null
1200
1616
  };
1201
1617
  }
1202
1618
  function computeMaintenance(input, now) {
@@ -1254,23 +1670,68 @@ function computeEfficiency(bundleMinGzipKb, category, categoryMedian) {
1254
1670
  const ratio = bundleMinGzipKb / categoryMedian;
1255
1671
  return clamp(100 - ratio * EFFICIENCY.medianScore);
1256
1672
  }
1257
- function computeHealthScore(breakdown) {
1673
+ function isRecognizedLicense(license) {
1674
+ if (!license) return false;
1675
+ const id = license.trim().toLowerCase();
1676
+ return RECOGNIZED_LICENSES.some((l) => id === l || id.startsWith(l));
1677
+ }
1678
+ function computeQuality(input) {
1679
+ const components = [];
1680
+ if (input.hasTypes !== null) {
1681
+ components.push({ value: input.hasTypes ? 100 : 0, weight: QUALITY_WEIGHTS.types });
1682
+ }
1683
+ if (input.hasTestScript !== null) {
1684
+ components.push({ value: input.hasTestScript ? 100 : 0, weight: QUALITY_WEIGHTS.tests });
1685
+ }
1686
+ if (input.readmeLength !== null || input.hasHomepage !== null) {
1687
+ const lenScore = Math.min((input.readmeLength ?? 0) / QUALITY.docsFullLengthChars, 1) * 60;
1688
+ const examples = input.hasExamples ? 25 : 0;
1689
+ const homepage = input.hasHomepage ? 15 : 0;
1690
+ components.push({ value: clamp(lenScore + examples + homepage), weight: QUALITY_WEIGHTS.docs });
1691
+ }
1692
+ if (input.hasReleaseNotes !== null) {
1693
+ components.push({ value: input.hasReleaseNotes ? 100 : 0, weight: QUALITY_WEIGHTS.changelog });
1694
+ }
1695
+ if (input.directDependenciesCount !== null) {
1696
+ const deps = input.directDependenciesCount;
1697
+ const depsScore = clamp(
1698
+ 100 - Math.log10(deps + 1) / Math.log10(QUALITY.depsLogMax + 1) * 100
1699
+ );
1700
+ components.push({ value: depsScore, weight: QUALITY_WEIGHTS.deps });
1701
+ }
1702
+ if (input.license !== null) {
1703
+ components.push({
1704
+ value: isRecognizedLicense(input.license) ? 100 : QUALITY.licenseUnrecognizedScore,
1705
+ weight: QUALITY_WEIGHTS.license
1706
+ });
1707
+ }
1708
+ if (input.hasProvenance) {
1709
+ components.push({ value: 100, weight: QUALITY_WEIGHTS.provenance });
1710
+ }
1711
+ if (components.length === 0) return null;
1712
+ return weightedAverage(components);
1713
+ }
1714
+ function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
1258
1715
  const { maintenance, adoption, reliability, efficiency } = breakdown;
1716
+ const w = weights;
1259
1717
  if (efficiency === null) {
1260
- const w = HEALTH_WEIGHTS;
1261
1718
  const denom = w.maintenance + w.adoption + w.reliability;
1262
1719
  return Math.round(
1263
1720
  (w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
1264
1721
  );
1265
1722
  }
1266
1723
  return Math.round(
1267
- HEALTH_WEIGHTS.maintenance * maintenance + HEALTH_WEIGHTS.adoption * adoption + HEALTH_WEIGHTS.reliability * reliability + HEALTH_WEIGHTS.efficiency * efficiency
1724
+ w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability + w.efficiency * efficiency
1268
1725
  );
1269
1726
  }
1727
+ function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
1728
+ if (qualityScore === null) return healthScore;
1729
+ return Math.round((1 - lambda) * healthScore + lambda * qualityScore);
1730
+ }
1270
1731
  function hasCriticalOrHighAdvisory(advisories) {
1271
1732
  return advisories.some((a) => a.severity === "critical" || a.severity === "high");
1272
1733
  }
1273
- function computeConfidence(input, now) {
1734
+ function computeConfidence(input, now, qualityScore = null) {
1274
1735
  const dl = input.weeklyDownloads ?? 0;
1275
1736
  const ageMonths = monthsSince(input.firstPublishedAt, now);
1276
1737
  const lastReleaseMonths = monthsSince(input.lastReleaseAt, now);
@@ -1285,6 +1746,10 @@ function computeConfidence(input, now) {
1285
1746
  if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
1286
1747
  return "emerging";
1287
1748
  }
1749
+ const promisingReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
1750
+ if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
1751
+ return "promising";
1752
+ }
1288
1753
  return "unproven";
1289
1754
  }
1290
1755
  function weightedAverage(components) {
@@ -1299,7 +1764,7 @@ function median(values) {
1299
1764
  const mid = Math.floor(sorted.length / 2);
1300
1765
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
1301
1766
  }
1302
- var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince;
1767
+ var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince, RECOGNIZED_LICENSES;
1303
1768
  var init_score = __esm({
1304
1769
  "src/scoring/score.ts"() {
1305
1770
  "use strict";
@@ -1311,6 +1776,20 @@ var init_score = __esm({
1311
1776
  clamp = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
1312
1777
  daysSince = (date, now) => date ? (now.getTime() - date.getTime()) / DAY_MS : null;
1313
1778
  monthsSince = (date, now) => date ? (now.getTime() - date.getTime()) / MONTH_MS : null;
1779
+ RECOGNIZED_LICENSES = [
1780
+ "mit",
1781
+ "isc",
1782
+ "apache-2.0",
1783
+ "bsd-2-clause",
1784
+ "bsd-3-clause",
1785
+ "mpl-2.0",
1786
+ "lgpl",
1787
+ "gpl",
1788
+ "agpl",
1789
+ "unlicense",
1790
+ "0bsd",
1791
+ "cc0-1.0"
1792
+ ];
1314
1793
  }
1315
1794
  });
1316
1795
 
@@ -1422,7 +1901,7 @@ var init_embeddings = __esm({
1422
1901
  });
1423
1902
 
1424
1903
  // src/db/packages.ts
1425
- import { eq, sql } from "drizzle-orm";
1904
+ import { eq, sql as sql2 } from "drizzle-orm";
1426
1905
  async function getSeedTargets(db) {
1427
1906
  const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
1428
1907
  return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
@@ -1508,18 +1987,28 @@ async function runSync(opts = {}) {
1508
1987
  prefetchedWeekly: weeklyMap.has(target.name) ? weeklyMap.get(target.name) : void 0
1509
1988
  });
1510
1989
  for (const e of signals.errors) allErrors.push({ package: target.name, ...e });
1511
- const input = toScoringInput(signals, target.category);
1512
1990
  const summaryInput = await buildSummaryInput(signals, target.category);
1513
- const { summary, usageGuide } = await provider.generate(summaryInput);
1991
+ const { summary, usageGuide, inferredCategory } = await provider.generate(summaryInput);
1992
+ let category = target.category;
1993
+ let categorySource = target.category ? "curated" : null;
1994
+ if (!category) {
1995
+ category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
1996
+ categorySource = category ? "inferred" : null;
1997
+ }
1998
+ const input = toScoringInput(signals, category);
1999
+ const quality = computeQuality(input);
1514
2000
  if (++done % 25 === 0) logger.info(` \u2026${done}/${targets.length}`);
1515
2001
  return {
1516
2002
  target,
2003
+ category,
2004
+ categorySource,
1517
2005
  signals,
1518
2006
  input,
1519
2007
  maintenance: computeMaintenance(input, now),
1520
2008
  adoption: computeAdoption(input),
1521
2009
  reliability: computeReliability(input),
1522
- confidence: computeConfidence(input, now),
2010
+ quality,
2011
+ confidence: computeConfidence(input, now, quality),
1523
2012
  summary,
1524
2013
  usageGuide
1525
2014
  };
@@ -1542,7 +2031,7 @@ async function runSync(opts = {}) {
1542
2031
  ok.map(
1543
2032
  (c) => buildEmbeddingText({
1544
2033
  name: c.target.name,
1545
- category: c.target.category,
2034
+ category: c.category,
1546
2035
  summary: c.summary,
1547
2036
  description: c.signals.registry?.description ?? null
1548
2037
  })
@@ -1553,21 +2042,23 @@ async function runSync(opts = {}) {
1553
2042
  const c = ok[i];
1554
2043
  const efficiency = computeEfficiency(
1555
2044
  c.input.bundleMinGzipKb,
1556
- c.target.category,
1557
- c.target.category ? medians.get(c.target.category) ?? null : null
2045
+ c.category,
2046
+ c.category ? medians.get(c.category) ?? null : null
1558
2047
  );
1559
2048
  const breakdown = {
1560
2049
  maintenance: c.maintenance,
1561
2050
  adoption: c.adoption,
1562
2051
  reliability: c.reliability,
1563
- efficiency
2052
+ efficiency,
2053
+ quality: c.quality
1564
2054
  };
1565
2055
  const healthScore = computeHealthScore(breakdown);
1566
2056
  await upsertPackage(
1567
2057
  handle.db,
1568
2058
  assemblePackageRow({
1569
2059
  name: c.target.name,
1570
- category: c.target.category,
2060
+ category: c.category,
2061
+ categorySource: c.categorySource,
1571
2062
  signals: c.signals,
1572
2063
  input: c.input,
1573
2064
  summary: c.summary,
@@ -1575,6 +2066,7 @@ async function runSync(opts = {}) {
1575
2066
  confidence: c.confidence,
1576
2067
  breakdown,
1577
2068
  healthScore,
2069
+ qualityScore: c.quality,
1578
2070
  embedding: embeddings[i] ?? null,
1579
2071
  now
1580
2072
  })
@@ -1614,7 +2106,7 @@ async function resolveTargets(db, opts) {
1614
2106
  function computeCategoryMedians(computed) {
1615
2107
  const byCategory = /* @__PURE__ */ new Map();
1616
2108
  for (const c of computed) {
1617
- const cat = c.target.category;
2109
+ const cat = c.category;
1618
2110
  const kb = c.input.bundleMinGzipKb;
1619
2111
  if (cat && isFrontendCategory(cat) && kb !== null) {
1620
2112
  const list = byCategory.get(cat) ?? [];
@@ -1635,6 +2127,7 @@ function assemblePackageRow(p) {
1635
2127
  name: p.name,
1636
2128
  ecosystem: "npm",
1637
2129
  category: p.category,
2130
+ categorySource: p.categorySource,
1638
2131
  description: r?.description ?? null,
1639
2132
  summary: p.summary,
1640
2133
  repoUrl: r?.repoUrl ?? null,
@@ -1655,6 +2148,7 @@ function assemblePackageRow(p) {
1655
2148
  bundleMinGzipKb: p.input.bundleMinGzipKb,
1656
2149
  advisories: p.input.advisories,
1657
2150
  healthScore: p.healthScore,
2151
+ qualityScore: p.qualityScore,
1658
2152
  confidence: p.confidence,
1659
2153
  scoreBreakdown: p.breakdown,
1660
2154
  usageGuide: p.usageGuide,
@@ -1673,6 +2167,7 @@ var init_sync = __esm({
1673
2167
  init_collect();
1674
2168
  init_npmDownloads();
1675
2169
  init_summarize();
2170
+ init_categoryInference();
1676
2171
  init_embeddings();
1677
2172
  init_scoring();
1678
2173
  init_client();
@@ -1682,14 +2177,14 @@ var init_sync = __esm({
1682
2177
  });
1683
2178
 
1684
2179
  // src/pipeline/single.ts
1685
- import { and, eq as eq2, isNotNull, sql as sql2 } from "drizzle-orm";
2180
+ import { and, eq as eq2, isNotNull, sql as sql3 } from "drizzle-orm";
1686
2181
  async function getSeedCategory(db, name) {
1687
2182
  const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq2(seedPackages.name, name)).limit(1);
1688
2183
  return row?.category ?? null;
1689
2184
  }
1690
2185
  async function getCategoryMedianBundle(db, category) {
1691
2186
  const [row] = await db.select({
1692
- m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
2187
+ m: sql3`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
1693
2188
  }).from(packages).where(and(eq2(packages.category, category), isNotNull(packages.bundleMinGzipKb)));
1694
2189
  return row?.m ?? null;
1695
2190
  }
@@ -1697,24 +2192,39 @@ async function syncOnePackage(db, name, opts = {}) {
1697
2192
  const config = getConfig();
1698
2193
  const now = /* @__PURE__ */ new Date();
1699
2194
  const existing = await getPackageByName(db, name);
1700
- const category = opts.category ?? existing?.category ?? await getSeedCategory(db, name) ?? null;
2195
+ const curatedCategory = opts.category ?? await getSeedCategory(db, name) ?? null;
2196
+ const initialCategory = curatedCategory ?? existing?.category ?? null;
1701
2197
  const prefetchedWeekly = await fetchWeeklyDownloads(name).catch(() => null);
1702
- const signals = await collectSignals(name, category, {
2198
+ const signals = await collectSignals(name, initialCategory, {
1703
2199
  githubToken: config.GITHUB_TOKEN,
1704
2200
  prefetchedWeekly
1705
2201
  });
2202
+ const summaryInput = await buildSummaryInput(signals, initialCategory);
2203
+ const { summary, usageGuide, inferredCategory } = await createSummaryProvider().generate(summaryInput);
2204
+ let category;
2205
+ let categorySource;
2206
+ if (curatedCategory) {
2207
+ category = curatedCategory;
2208
+ categorySource = "curated";
2209
+ } else if (existing?.category) {
2210
+ category = existing.category;
2211
+ categorySource = existing.categorySource ?? "inferred";
2212
+ } else {
2213
+ category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
2214
+ categorySource = category ? "inferred" : null;
2215
+ }
1706
2216
  const input = toScoringInput(signals, category);
1707
- const summaryInput = await buildSummaryInput(signals, category);
1708
- const { summary, usageGuide } = await createSummaryProvider().generate(summaryInput);
1709
2217
  const median2 = category ? await getCategoryMedianBundle(db, category) : null;
2218
+ const quality = computeQuality(input);
1710
2219
  const breakdown = {
1711
2220
  maintenance: computeMaintenance(input, now),
1712
2221
  adoption: computeAdoption(input),
1713
2222
  reliability: computeReliability(input),
1714
- efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2)
2223
+ efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
2224
+ quality
1715
2225
  };
1716
2226
  const healthScore = computeHealthScore(breakdown);
1717
- const confidence = computeConfidence(input, now);
2227
+ const confidence = computeConfidence(input, now, quality);
1718
2228
  const [embedding] = await createEmbeddingProvider().embed([
1719
2229
  buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
1720
2230
  ]);
@@ -1723,6 +2233,7 @@ async function syncOnePackage(db, name, opts = {}) {
1723
2233
  assemblePackageRow({
1724
2234
  name,
1725
2235
  category,
2236
+ categorySource,
1726
2237
  signals,
1727
2238
  input,
1728
2239
  summary,
@@ -1730,6 +2241,7 @@ async function syncOnePackage(db, name, opts = {}) {
1730
2241
  confidence,
1731
2242
  breakdown,
1732
2243
  healthScore,
2244
+ qualityScore: quality,
1733
2245
  embedding: embedding ?? null,
1734
2246
  now
1735
2247
  })
@@ -1753,6 +2265,7 @@ var init_single = __esm({
1753
2265
  init_collect();
1754
2266
  init_sources();
1755
2267
  init_summarize();
2268
+ init_categoryInference();
1756
2269
  init_scoring();
1757
2270
  init_embeddings();
1758
2271
  init_packages();
@@ -1761,103 +2274,83 @@ var init_single = __esm({
1761
2274
  }
1762
2275
  });
1763
2276
 
1764
- // src/search/categoryInference.ts
1765
- function inferCategory(need) {
1766
- const text2 = need.toLowerCase();
1767
- for (const rule of RULES) {
1768
- if (rule.patterns.test(text2)) return rule.category;
1769
- }
1770
- return null;
1771
- }
1772
- var RULES;
1773
- var init_categoryInference = __esm({
1774
- "src/search/categoryInference.ts"() {
1775
- "use strict";
1776
- init_esm_shims();
1777
- RULES = [
1778
- { category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
1779
- { category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
1780
- { category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
1781
- { category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
1782
- { category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
1783
- { category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
1784
- { category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
1785
- { category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
1786
- { category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
1787
- { category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
1788
- { category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
1789
- { category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
1790
- { category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
1791
- { category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
1792
- { category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
1793
- // date-time precedes linting: "date formatting" must not be captured by linting's
1794
- // bare "formatting" token before the date rule is reached (first-match-wins).
1795
- { category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
1796
- { category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
1797
- { category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
1798
- { category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
1799
- { category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
1800
- { category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
1801
- { category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
1802
- ];
1803
- }
1804
- });
1805
-
1806
2277
  // src/search/recommend.ts
1807
- import { and as and2, cosineDistance, desc, eq as eq3, isNotNull as isNotNull2, lte, sql as sql3 } from "drizzle-orm";
2278
+ import { and as and2, cosineDistance, eq as eq3, isNotNull as isNotNull2, lte, sql as sql4 } from "drizzle-orm";
1808
2279
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
1809
2280
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
1810
2281
  const [queryVec] = await provider.embed([opts.need]);
1811
2282
  if (!queryVec) return [];
1812
2283
  const category = opts.category ?? inferCategory(opts.need);
1813
2284
  const pool = Math.max(limit * 5, 25);
1814
- let rows = await runQuery(db, queryVec, opts.constraints, category, pool);
1815
- if (category && rows.length < limit) {
1816
- const broad = await runQuery(db, queryVec, opts.constraints, null, pool);
1817
- const seen = new Set(rows.map((r) => r.name));
1818
- rows = rows.concat(broad.filter((r) => !seen.has(r.name)));
1819
- }
1820
- if (rows.length === 0) return [];
1821
- const ranked = rows.map((r) => {
1822
- const simNorm = Math.max(0, Math.min(1, (r.similarity + 1) / 2));
1823
- const health = (r.healthScore ?? 0) / 100;
1824
- return { row: r, score: SIM_WEIGHT * simNorm + HEALTH_WEIGHT * health };
2285
+ let fused = await hybridSearch(db, queryVec, opts.need, opts.constraints, category, pool);
2286
+ if (category && fused.length < limit) {
2287
+ const broad = await hybridSearch(db, queryVec, opts.need, opts.constraints, null, pool);
2288
+ const seen = new Set(fused.map((f) => f.row.name));
2289
+ fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
2290
+ }
2291
+ if (fused.length === 0) return [];
2292
+ const lambda = loadWeights().composite.lambda;
2293
+ const maxRrf = Math.max(...fused.map((f) => f.rrf));
2294
+ const ranked = fused.map((f) => {
2295
+ const relevance = maxRrf > 0 ? f.rrf / maxRrf : 0;
2296
+ const composite = computeComposite(f.row.healthScore ?? 0, f.row.qualityScore, lambda) / 100;
2297
+ return { row: f.row, score: RELEVANCE_WEIGHT * relevance + COMPOSITE_WEIGHT * composite };
1825
2298
  }).sort((a, b) => b.score - a.score).slice(0, limit);
1826
2299
  return ranked.map(({ row }) => toCandidate(row));
1827
2300
  }
1828
- async function runQuery(db, queryVec, constraints, category, pool) {
1829
- const similarity = sql3`1 - (${cosineDistance(packages.embedding, queryVec)})`;
1830
- const conditions = [isNotNull2(packages.embedding)];
2301
+ async function hybridSearch(db, queryVec, need, constraints, category, pool) {
2302
+ const [vectorRows, lexicalRows] = await Promise.all([
2303
+ runVectorQuery(db, queryVec, constraints, category, pool),
2304
+ runLexicalQuery(db, need, constraints, category, pool)
2305
+ ]);
2306
+ return rrfFuse([vectorRows, lexicalRows]);
2307
+ }
2308
+ function rrfFuse(lists, k = RRF_K) {
2309
+ const fused = /* @__PURE__ */ new Map();
2310
+ for (const list of lists) {
2311
+ list.forEach((row, idx) => {
2312
+ const contribution = 1 / (k + idx + 1);
2313
+ const prev = fused.get(row.name);
2314
+ if (prev) prev.rrf += contribution;
2315
+ else fused.set(row.name, { row, rrf: contribution });
2316
+ });
2317
+ }
2318
+ return [...fused.values()].sort((a, b) => b.rrf - a.rrf);
2319
+ }
2320
+ function buildConditions(constraints, category) {
2321
+ const conditions = [];
1831
2322
  if (category) conditions.push(eq3(packages.category, category));
1832
2323
  if (constraints?.license) conditions.push(eq3(packages.license, constraints.license));
1833
2324
  if (constraints?.maxBundleKb !== void 0) {
1834
2325
  conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
1835
2326
  }
1836
2327
  if (constraints?.minConfidence) {
1837
- const allowed = ["proven", "emerging", "unproven"].filter(
2328
+ const allowed = ["proven", "emerging", "promising", "unproven"].filter(
1838
2329
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
1839
2330
  );
1840
2331
  conditions.push(
1841
- sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
2332
+ sql4`${packages.confidence} in ${sql4.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
1842
2333
  );
1843
2334
  }
1844
- return db.select({
1845
- name: packages.name,
1846
- category: packages.category,
1847
- healthScore: packages.healthScore,
1848
- confidence: packages.confidence,
1849
- latestVersion: packages.latestVersion,
1850
- weeklyDownloads: packages.weeklyDownloads,
1851
- lastReleaseAt: packages.lastReleaseAt,
1852
- repoUrl: packages.repoUrl,
1853
- similarity
1854
- }).from(packages).where(and2(...conditions)).orderBy(desc(similarity)).limit(pool);
2335
+ return conditions;
2336
+ }
2337
+ async function runVectorQuery(db, queryVec, constraints, category, pool) {
2338
+ const distance = cosineDistance(packages.embedding, queryVec);
2339
+ const conditions = [isNotNull2(packages.embedding), ...buildConditions(constraints, category)];
2340
+ return db.select(ROW_COLUMNS).from(packages).where(and2(...conditions)).orderBy(distance).limit(pool);
2341
+ }
2342
+ async function runLexicalQuery(db, need, constraints, category, pool) {
2343
+ const tsquery = sql4`websearch_to_tsquery('english', ${need})`;
2344
+ const rank = sql4`ts_rank(${packages.searchVector}, ${tsquery})`;
2345
+ const conditions = [sql4`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
2346
+ return db.select(ROW_COLUMNS).from(packages).where(and2(...conditions)).orderBy(sql4`${rank} desc`).limit(pool);
1855
2347
  }
1856
2348
  function toCandidate(row) {
1857
2349
  return {
1858
2350
  name: row.name,
1859
2351
  category: row.category,
1860
2352
  healthScore: row.healthScore ?? 0,
2353
+ qualityScore: row.qualityScore,
1861
2354
  confidence: row.confidence ?? "unproven",
1862
2355
  why: buildWhy(row),
1863
2356
  latestVersion: row.latestVersion,
@@ -1878,22 +2371,41 @@ function formatDownloads(n) {
1878
2371
  if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
1879
2372
  return String(n);
1880
2373
  }
1881
- var CONFIDENCE_RANK, SIM_WEIGHT, HEALTH_WEIGHT;
2374
+ var CONFIDENCE_RANK, RELEVANCE_WEIGHT, COMPOSITE_WEIGHT, RRF_K, ROW_COLUMNS;
1882
2375
  var init_recommend = __esm({
1883
2376
  "src/search/recommend.ts"() {
1884
2377
  "use strict";
1885
2378
  init_esm_shims();
1886
2379
  init_schema();
2380
+ init_scoring();
2381
+ init_weights();
1887
2382
  init_categoryInference();
1888
2383
  init_embeddings();
1889
- CONFIDENCE_RANK = { unproven: 0, emerging: 1, proven: 2 };
1890
- SIM_WEIGHT = 0.6;
1891
- HEALTH_WEIGHT = 0.4;
2384
+ CONFIDENCE_RANK = {
2385
+ unproven: 0,
2386
+ promising: 1,
2387
+ emerging: 2,
2388
+ proven: 3
2389
+ };
2390
+ RELEVANCE_WEIGHT = 0.6;
2391
+ COMPOSITE_WEIGHT = 0.4;
2392
+ RRF_K = 60;
2393
+ ROW_COLUMNS = {
2394
+ name: packages.name,
2395
+ category: packages.category,
2396
+ healthScore: packages.healthScore,
2397
+ qualityScore: packages.qualityScore,
2398
+ confidence: packages.confidence,
2399
+ latestVersion: packages.latestVersion,
2400
+ weeklyDownloads: packages.weeklyDownloads,
2401
+ lastReleaseAt: packages.lastReleaseAt,
2402
+ repoUrl: packages.repoUrl
2403
+ };
1892
2404
  }
1893
2405
  });
1894
2406
 
1895
2407
  // src/mcp/handlers.ts
1896
- import { sql as sql4 } from "drizzle-orm";
2408
+ import { sql as sql5 } from "drizzle-orm";
1897
2409
  function isStale(dataAsOf) {
1898
2410
  if (!dataAsOf) return true;
1899
2411
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
@@ -1910,7 +2422,8 @@ function rowToEvaluate(row) {
1910
2422
  maintenance: 0,
1911
2423
  adoption: 0,
1912
2424
  reliability: 0,
1913
- efficiency: null
2425
+ efficiency: null,
2426
+ quality: null
1914
2427
  };
1915
2428
  return {
1916
2429
  dataAsOf: (row.dataAsOf ?? /* @__PURE__ */ new Date()).toISOString(),
@@ -1918,6 +2431,7 @@ function rowToEvaluate(row) {
1918
2431
  name: row.name,
1919
2432
  category: row.category,
1920
2433
  healthScore: row.healthScore ?? 0,
2434
+ qualityScore: row.qualityScore ?? null,
1921
2435
  confidence: row.confidence ?? "unproven",
1922
2436
  scoreBreakdown: breakdown,
1923
2437
  latestVersion: row.latestVersion,
@@ -1936,7 +2450,7 @@ function rowToEvaluate(row) {
1936
2450
  };
1937
2451
  }
1938
2452
  async function latestDataAsOf(db) {
1939
- const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
2453
+ const [row] = await db.select({ m: sql5`max(${packages.dataAsOf})` }).from(packages);
1940
2454
  return new Date(row?.m ?? Date.now()).toISOString();
1941
2455
  }
1942
2456
  async function handleRecommend(db, input) {
@@ -2239,7 +2753,7 @@ var init_server = __esm({
2239
2753
  init_handlers();
2240
2754
  init_diagram();
2241
2755
  categoryEnum = z2.enum(CATEGORIES);
2242
- confidenceEnum = z2.enum(["proven", "emerging", "unproven"]);
2756
+ confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
2243
2757
  constraintsSchema = z2.object({
2244
2758
  runtime: z2.enum(["browser", "node", "both"]).optional(),
2245
2759
  license: z2.string().optional(),
@@ -2249,27 +2763,378 @@ var init_server = __esm({
2249
2763
  }
2250
2764
  });
2251
2765
 
2252
- // src/pipeline/index.ts
2253
- var pipeline_exports = {};
2254
- __export(pipeline_exports, {
2255
- runSync: () => runSync
2256
- });
2257
- var init_pipeline = __esm({
2258
- "src/pipeline/index.ts"() {
2766
+ // src/auth/apiKeys.ts
2767
+ import { createHash as createHash3, randomBytes } from "crypto";
2768
+ import { and as and3, desc, eq as eq4, isNull } from "drizzle-orm";
2769
+ function hashKey(key) {
2770
+ return createHash3("sha256").update(key).digest("hex");
2771
+ }
2772
+ function generateApiKey() {
2773
+ const body = randomBytes(24).toString("base64url");
2774
+ const key = API_KEY_PREFIX + body;
2775
+ return { key, hash: hashKey(key), prefix: API_KEY_PREFIX + body.slice(0, DISPLAY_BODY) };
2776
+ }
2777
+ async function createKey(db, input = {}) {
2778
+ const { key, hash, prefix } = generateApiKey();
2779
+ const [row] = await db.insert(apiKeys).values({
2780
+ keyHash: hash,
2781
+ prefix,
2782
+ label: input.label,
2783
+ tier: input.tier ?? "free",
2784
+ ownerId: input.ownerId
2785
+ }).returning();
2786
+ return { key, row };
2787
+ }
2788
+ async function lookupActiveKey(db, key) {
2789
+ const hash = hashKey(key);
2790
+ const [row] = await db.select().from(apiKeys).where(and3(eq4(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
2791
+ if (!row) return null;
2792
+ db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq4(apiKeys.id, row.id)).then(void 0, () => {
2793
+ });
2794
+ return row;
2795
+ }
2796
+ async function listKeys(db) {
2797
+ return db.select().from(apiKeys).orderBy(desc(apiKeys.createdAt));
2798
+ }
2799
+ async function revokeKey(db, prefixOrId) {
2800
+ const asId = Number(prefixOrId);
2801
+ const match = Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq4(apiKeys.id, asId) : eq4(apiKeys.prefix, prefixOrId);
2802
+ const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and3(match, isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
2803
+ return rows.length;
2804
+ }
2805
+ var DISPLAY_BODY;
2806
+ var init_apiKeys = __esm({
2807
+ "src/auth/apiKeys.ts"() {
2259
2808
  "use strict";
2260
2809
  init_esm_shims();
2261
- init_sync();
2810
+ init_constants();
2811
+ init_schema();
2812
+ DISPLAY_BODY = 6;
2262
2813
  }
2263
2814
  });
2264
2815
 
2265
- // src/cli/format.ts
2266
- function table(headers, rows) {
2267
- const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
2268
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
2269
- const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
2270
- const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
2271
- for (const r of rows) out.push(line(r));
2272
- return out.join("\n");
2816
+ // src/mcp/http.ts
2817
+ var http_exports = {};
2818
+ __export(http_exports, {
2819
+ startHttpServer: () => startHttpServer
2820
+ });
2821
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2822
+ function rpcError(code, message) {
2823
+ return { jsonrpc: "2.0", error: { code, message }, id: null };
2824
+ }
2825
+ async function startHttpServer(opts = {}) {
2826
+ const config = getConfig();
2827
+ const port = opts.port ?? config.PORT;
2828
+ const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
2829
+ const { db } = createDb({ max: 20 });
2830
+ const app = express();
2831
+ app.set("trust proxy", 1);
2832
+ app.use(helmet());
2833
+ app.use(express.json({ limit: "1mb" }));
2834
+ app.get("/healthz", (_req, res) => {
2835
+ res.status(200).json({ status: "ok" });
2836
+ });
2837
+ const ipLimiter = rateLimit({
2838
+ windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
2839
+ limit: config.LURQ_IP_RATE_LIMIT_MAX,
2840
+ standardHeaders: "draft-7",
2841
+ legacyHeaders: false,
2842
+ message: rpcError(-32029, "Rate limit exceeded.")
2843
+ });
2844
+ const auth = async (req, res, next) => {
2845
+ const header = req.headers.authorization;
2846
+ const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
2847
+ if (!token) {
2848
+ res.status(401).json(rpcError(-32001, "Missing API key. Pass Authorization: Bearer <key>."));
2849
+ return;
2850
+ }
2851
+ try {
2852
+ const row = await lookupActiveKey(db, token);
2853
+ if (!row) {
2854
+ res.status(401).json(rpcError(-32001, "Invalid or revoked API key."));
2855
+ return;
2856
+ }
2857
+ req.lurqKey = row;
2858
+ next();
2859
+ } catch (err) {
2860
+ logger.error("auth lookup failed:", err instanceof Error ? err.message : String(err));
2861
+ res.status(500).json(rpcError(-32603, "Internal error."));
2862
+ }
2863
+ };
2864
+ const keyLimiter = rateLimit({
2865
+ windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
2866
+ limit: config.LURQ_RATE_LIMIT_MAX,
2867
+ standardHeaders: "draft-7",
2868
+ legacyHeaders: false,
2869
+ // Key on the resolved API key (always present — auth runs first). The IP
2870
+ // fallback uses express-rate-limit's ipKeyGenerator so IPv6 addresses are
2871
+ // normalized correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
2872
+ keyGenerator: (req) => req.lurqKey?.prefix ?? ipKeyGenerator(req.ip ?? "0.0.0.0"),
2873
+ message: rpcError(-32029, "Rate limit exceeded.")
2874
+ });
2875
+ app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
2876
+ const server = buildMcpServer(db);
2877
+ const transport = new StreamableHTTPServerTransport({
2878
+ sessionIdGenerator: void 0,
2879
+ enableJsonResponse: true
2880
+ });
2881
+ res.on("close", () => {
2882
+ void transport.close();
2883
+ void server.close();
2884
+ });
2885
+ try {
2886
+ await server.connect(transport);
2887
+ await transport.handleRequest(req, res, req.body);
2888
+ } catch (err) {
2889
+ logger.error("mcp request failed:", err instanceof Error ? err.message : String(err));
2890
+ if (!res.headersSent) res.status(500).json(rpcError(-32603, "Internal error"));
2891
+ }
2892
+ });
2893
+ app.all("/mcp", (_req, res) => {
2894
+ res.status(405).json(rpcError(-32e3, "Method not allowed."));
2895
+ });
2896
+ app.listen(port, () => {
2897
+ logger.info(`lurq HTTP MCP server listening on :${port}/mcp`);
2898
+ });
2899
+ }
2900
+ var init_http2 = __esm({
2901
+ "src/mcp/http.ts"() {
2902
+ "use strict";
2903
+ init_esm_shims();
2904
+ init_config();
2905
+ init_logger();
2906
+ init_apiKeys();
2907
+ init_client();
2908
+ init_server();
2909
+ }
2910
+ });
2911
+
2912
+ // src/pipeline/rescore.ts
2913
+ import { isNotNull as isNotNull3 } from "drizzle-orm";
2914
+ import { eq as eq5 } from "drizzle-orm";
2915
+ async function runRescore() {
2916
+ const weights = loadWeights();
2917
+ const handle = createDb({ max: 4 });
2918
+ try {
2919
+ const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull3(packages.scoreBreakdown));
2920
+ let updated = 0;
2921
+ for (const row of rows) {
2922
+ if (!row.breakdown) continue;
2923
+ const health = computeHealthScore(row.breakdown, weights.health);
2924
+ if (health !== row.healthScore) {
2925
+ await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq5(packages.id, row.id));
2926
+ updated++;
2927
+ }
2928
+ }
2929
+ logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
2930
+ return { seen: rows.length, updated };
2931
+ } finally {
2932
+ await handle.close();
2933
+ }
2934
+ }
2935
+ var init_rescore = __esm({
2936
+ "src/pipeline/rescore.ts"() {
2937
+ "use strict";
2938
+ init_esm_shims();
2939
+ init_logger();
2940
+ init_client();
2941
+ init_schema();
2942
+ init_scoring();
2943
+ init_weights();
2944
+ }
2945
+ });
2946
+
2947
+ // src/db/discovery.ts
2948
+ import { eq as eq6, inArray as inArray2, sql as sql6 } from "drizzle-orm";
2949
+ async function getKnownNames(db) {
2950
+ const [tracked, queued] = await Promise.all([
2951
+ db.select({ name: packages.name }).from(packages),
2952
+ db.select({ name: discoveryQueue.name }).from(discoveryQueue)
2953
+ ]);
2954
+ return /* @__PURE__ */ new Set([...tracked.map((r) => r.name), ...queued.map((r) => r.name)]);
2955
+ }
2956
+ async function enqueueCandidates(db, candidates) {
2957
+ if (candidates.length === 0) return 0;
2958
+ const rows = candidates.map((c) => ({ name: c.name, discoveredVia: c.via }));
2959
+ const inserted = await db.insert(discoveryQueue).values(rows).onConflictDoNothing({ target: discoveryQueue.name }).returning({ id: discoveryQueue.id });
2960
+ return inserted.length;
2961
+ }
2962
+ async function getPendingCandidates(db, limit) {
2963
+ return db.select().from(discoveryQueue).where(eq6(discoveryQueue.status, "pending")).limit(limit);
2964
+ }
2965
+ async function setDiscoveryStatus(db, name, data) {
2966
+ await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq6(discoveryQueue.name, name));
2967
+ }
2968
+ var init_discovery = __esm({
2969
+ "src/db/discovery.ts"() {
2970
+ "use strict";
2971
+ init_esm_shims();
2972
+ init_schema();
2973
+ }
2974
+ });
2975
+
2976
+ // src/pipeline/discovery.ts
2977
+ import { isNotNull as isNotNull4 } from "drizzle-orm";
2978
+ function selectCandidates(raw, known) {
2979
+ const seen = new Set(known);
2980
+ const out = [];
2981
+ for (const c of raw) {
2982
+ const name = c.name?.trim();
2983
+ if (!name || seen.has(name)) continue;
2984
+ seen.add(name);
2985
+ out.push({ name, via: c.via });
2986
+ }
2987
+ return out;
2988
+ }
2989
+ function passesGate(preScore) {
2990
+ return preScore !== null && preScore >= DISCOVERY.minPreScore;
2991
+ }
2992
+ async function preScorePackage(name, fetchImpl) {
2993
+ try {
2994
+ const registry = await fetchNpmRegistry(name, fetchImpl);
2995
+ const signals = {
2996
+ name,
2997
+ registry,
2998
+ downloads: null,
2999
+ github: null,
3000
+ depsDev: null,
3001
+ bundle: null,
3002
+ errors: []
3003
+ };
3004
+ return computeQuality(toScoringInput(signals, null));
3005
+ } catch {
3006
+ return null;
3007
+ }
3008
+ }
3009
+ async function graphChannel(db) {
3010
+ const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull4(packages.latestVersion));
3011
+ const out = [];
3012
+ for (const t of tracked) {
3013
+ if (!t.version) continue;
3014
+ const deps = (await fetchDependencyNames(t.name, t.version)).slice(
3015
+ 0,
3016
+ DISCOVERY.graphNeighborsPerSeed
3017
+ );
3018
+ for (const name of deps) out.push({ name, via: "dependency-graph" });
3019
+ }
3020
+ return out;
3021
+ }
3022
+ async function searchChannel() {
3023
+ const out = [];
3024
+ for (const category of CATEGORIES) {
3025
+ if (category === "other") continue;
3026
+ const hits = await searchNpm(`keywords:${category}`, DISCOVERY.searchSizePerCategory);
3027
+ for (const hit of hits) {
3028
+ const via = isRecent(hit.date) ? "recent" : "category-search";
3029
+ out.push({ name: hit.name, via });
3030
+ }
3031
+ }
3032
+ return out;
3033
+ }
3034
+ function isRecent(date) {
3035
+ if (!date) return false;
3036
+ const ms = Date.parse(date);
3037
+ if (Number.isNaN(ms)) return false;
3038
+ return Date.now() - ms <= 90 * 24 * 60 * 60 * 1e3;
3039
+ }
3040
+ async function runDiscovery(opts = {}) {
3041
+ const cap = opts.perRunCap ?? DISCOVERY.perRunCap;
3042
+ const handle = createDb({ max: 6 });
3043
+ try {
3044
+ logger.info("Discovery: gathering candidates from graph + search channels\u2026");
3045
+ const [graph, search] = await Promise.all([graphChannel(handle.db), searchChannel()]);
3046
+ const known = await getKnownNames(handle.db);
3047
+ const fresh = selectCandidates([...graph, ...search], known);
3048
+ const enqueued = await enqueueCandidates(handle.db, fresh);
3049
+ logger.info(
3050
+ `Discovery: ${graph.length} graph + ${search.length} search candidates \u2192 ${enqueued} new queued.`
3051
+ );
3052
+ const pending = await getPendingCandidates(handle.db, cap * 4);
3053
+ const scored = [];
3054
+ for (const cand of pending) {
3055
+ const preScore = await preScorePackage(cand.name);
3056
+ await setDiscoveryStatus(handle.db, cand.name, {
3057
+ status: passesGate(preScore) ? "pending" : "rejected",
3058
+ preScore: preScore ?? null
3059
+ });
3060
+ if (passesGate(preScore)) scored.push({ name: cand.name, preScore });
3061
+ }
3062
+ logger.info(`Discovery: gated ${pending.length}; ${scored.length} cleared the quality bar.`);
3063
+ scored.sort((a, b) => b.preScore - a.preScore);
3064
+ const toIngest = scored.slice(0, cap);
3065
+ const deferred = scored.slice(cap);
3066
+ if (deferred.length > 0) {
3067
+ logger.info(
3068
+ `Discovery: per-run cap ${cap} reached \u2014 ${deferred.length} eligible candidate(s) deferred to the next run: ${deferred.map((d) => d.name).join(", ")}`
3069
+ );
3070
+ }
3071
+ let ingested = 0;
3072
+ if (!opts.dryRun) {
3073
+ for (const cand of toIngest) {
3074
+ try {
3075
+ await syncOnePackage(handle.db, cand.name);
3076
+ await setDiscoveryStatus(handle.db, cand.name, { status: "ingested" });
3077
+ ingested++;
3078
+ } catch (err) {
3079
+ logger.warn(`Discovery: failed to ingest ${cand.name}: ${err.message}`);
3080
+ }
3081
+ }
3082
+ }
3083
+ logger.info(`Discovery: ingested ${ingested}/${toIngest.length} candidate(s).`);
3084
+ return {
3085
+ enqueued,
3086
+ gated: pending.length,
3087
+ passed: scored.length,
3088
+ ingested,
3089
+ droppedToNextRun: deferred.length
3090
+ };
3091
+ } finally {
3092
+ await handle.close();
3093
+ }
3094
+ }
3095
+ var init_discovery2 = __esm({
3096
+ "src/pipeline/discovery.ts"() {
3097
+ "use strict";
3098
+ init_esm_shims();
3099
+ init_logger();
3100
+ init_types();
3101
+ init_client();
3102
+ init_discovery();
3103
+ init_schema();
3104
+ init_depsDev();
3105
+ init_npmRegistry();
3106
+ init_npmSearch();
3107
+ init_scoring();
3108
+ init_weights();
3109
+ init_single();
3110
+ }
3111
+ });
3112
+
3113
+ // src/pipeline/index.ts
3114
+ var pipeline_exports = {};
3115
+ __export(pipeline_exports, {
3116
+ runDiscovery: () => runDiscovery,
3117
+ runRescore: () => runRescore,
3118
+ runSync: () => runSync
3119
+ });
3120
+ var init_pipeline = __esm({
3121
+ "src/pipeline/index.ts"() {
3122
+ "use strict";
3123
+ init_esm_shims();
3124
+ init_sync();
3125
+ init_rescore();
3126
+ init_discovery2();
3127
+ }
3128
+ });
3129
+
3130
+ // src/cli/format.ts
3131
+ function table(headers, rows) {
3132
+ const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
3133
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
3134
+ const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
3135
+ const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
3136
+ for (const r of rows) out.push(line(r));
3137
+ return out.join("\n");
2273
3138
  }
2274
3139
  function detail(pairs) {
2275
3140
  const labelWidth = Math.max(...pairs.map(([k]) => k.length));
@@ -2292,6 +3157,7 @@ function formatPercent(fraction) {
2292
3157
  function confidenceLabel(c) {
2293
3158
  if (c === "proven") return green(c);
2294
3159
  if (c === "emerging") return yellow(c);
3160
+ if (c === "promising") return green(c);
2295
3161
  return red(c);
2296
3162
  }
2297
3163
  var ESC, wrap, bold, dim, red, green, yellow, ANSI_RE, width;
@@ -2315,9 +3181,11 @@ var init_format = __esm({
2315
3181
  var commands_exports = {};
2316
3182
  __export(commands_exports, {
2317
3183
  runCompare: () => runCompare,
3184
+ runEditWeights: () => runEditWeights,
2318
3185
  runEvaluate: () => runEvaluate,
2319
3186
  runRecommend: () => runRecommend,
2320
- runVerify: () => runVerify
3187
+ runVerify: () => runVerify,
3188
+ runWeights: () => runWeights
2321
3189
  });
2322
3190
  async function withDb(fn) {
2323
3191
  requireConfig(["DATABASE_URL"]);
@@ -2345,10 +3213,11 @@ async function runRecommend(need, opts) {
2345
3213
  }
2346
3214
  console.log(
2347
3215
  table(
2348
- ["Package", "Health", "Confidence", "Weekly", "Latest", "Category"],
3216
+ ["Package", "Health", "Quality", "Confidence", "Weekly", "Latest", "Category"],
2349
3217
  res.candidates.map((c) => [
2350
3218
  c.name,
2351
3219
  String(c.healthScore),
3220
+ c.qualityScore != null ? String(c.qualityScore) : "\u2014",
2352
3221
  confidenceLabel(c.confidence),
2353
3222
  formatNumber(c.weeklyDownloads),
2354
3223
  c.latestVersion ?? "\u2014",
@@ -2373,9 +3242,10 @@ async function runEvaluate(pkg, opts) {
2373
3242
  console.log(
2374
3243
  detail([
2375
3244
  ["health", `${res.healthScore} ${confidenceLabel(res.confidence)}`],
3245
+ ["quality", res.qualityScore != null ? String(res.qualityScore) : "\u2014"],
2376
3246
  [
2377
3247
  "breakdown",
2378
- `maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"}`
3248
+ `maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"} \xB7 qual ${b.quality ?? "\u2014"}`
2379
3249
  ],
2380
3250
  ["version", res.latestVersion ?? "\u2014"],
2381
3251
  ["weekly dl", `${formatNumber(res.weeklyDownloads)} (${formatPercent(res.downloadGrowth90d)} 90d)`],
@@ -2429,6 +3299,72 @@ not found: ${res.missing.join(", ")}`));
2429
3299
  console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
2430
3300
  });
2431
3301
  }
3302
+ function runWeights(opts = {}) {
3303
+ const w = loadWeights();
3304
+ const active = activeWeightsPath();
3305
+ if (opts.json) {
3306
+ console.log(JSON.stringify({ ...w, source: active?.source ?? "defaults" }, null, 2));
3307
+ return;
3308
+ }
3309
+ const pct = (n) => n.toFixed(2);
3310
+ console.log(bold("Two axes, blended for default sort:"));
3311
+ console.log(` composite = (1\u2212\u03BB)\xB7health + \u03BB\xB7quality \u03BB = ${pct(w.composite.lambda)}
3312
+ `);
3313
+ console.log(bold("Health (proven-ness) \u2014 weighted sum of 4 components:"));
3314
+ console.log(
3315
+ detail([
3316
+ ["maintenance", `${pct(w.health.maintenance)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.maintenance)}`],
3317
+ ["adoption", `${pct(w.health.adoption)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.adoption)}`],
3318
+ ["reliability", `${pct(w.health.reliability)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.reliability)}`],
3319
+ ["efficiency", `${pct(w.health.efficiency)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.efficiency)}`]
3320
+ ])
3321
+ );
3322
+ console.log("\n" + bold("Quality (intrinsic, adoption-independent):"));
3323
+ console.log(" " + dim(Object.keys(QUALITY_WEIGHTS).join(", ")));
3324
+ console.log("\n" + bold("Confidence thresholds:"));
3325
+ console.log(
3326
+ detail([
3327
+ ["proven", `\u2265 ${formatNumber(CONFIDENCE.proven.minWeeklyDownloads)} weekly dl, \u2265 ${CONFIDENCE.proven.minAgeMonths}mo old`],
3328
+ ["emerging", `\u2265 ${formatNumber(CONFIDENCE.emerging.minWeeklyDownloads)} weekly dl OR \u2265 ${CONFIDENCE.emerging.strongGrowth * 100}% 90d growth`],
3329
+ ["promising", `\u2265 ${CONFIDENCE.promising.minQuality} quality score (adoption-independent)`]
3330
+ ])
3331
+ );
3332
+ console.log(dim(`
3333
+ Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
3334
+ }
3335
+ function runEditWeights(opts) {
3336
+ if (opts.reset) {
3337
+ const removed = resetWeights();
3338
+ console.log(removed.length ? `Removed overrides:
3339
+ ${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
3340
+ return;
3341
+ }
3342
+ if (opts.explain) {
3343
+ const key = opts.explain;
3344
+ const text2 = WEIGHT_EXPLANATIONS[key];
3345
+ if (!text2) {
3346
+ throw new Error(`No explanation for "${key}". Known: ${Object.keys(WEIGHT_EXPLANATIONS).join(", ")}.`);
3347
+ }
3348
+ console.log(`${bold(key)} \u2014 ${text2}`);
3349
+ return;
3350
+ }
3351
+ if (opts.set && opts.set.length > 0) {
3352
+ const next = applyOverrides(loadWeights(), opts.set);
3353
+ const { weights, normalized } = validateWeights(next);
3354
+ const path2 = saveWeights(weights, opts.project ? "project" : "user");
3355
+ console.log(`Saved overrides to ${path2}`);
3356
+ if (normalized) {
3357
+ console.log(
3358
+ yellow("Health weights did not sum to 1.0 \u2014 renormalized to: ") + `maint ${weights.health.maintenance.toFixed(3)}, adopt ${weights.health.adoption.toFixed(3)}, rel ${weights.health.reliability.toFixed(3)}, eff ${weights.health.efficiency.toFixed(3)}`
3359
+ );
3360
+ }
3361
+ console.log(dim("\nRun `lurq rescore` to apply the new health weights to the stored index."));
3362
+ return;
3363
+ }
3364
+ console.log(dim(`No changes. Settable keys: ${settableKeys().join(", ")}.
3365
+ `));
3366
+ runWeights(opts);
3367
+ }
2432
3368
  async function runVerify(pkg, opts) {
2433
3369
  await withDb(async (db) => {
2434
3370
  const res = await handleVerify(db, { package: pkg });
@@ -2454,57 +3390,33 @@ var init_commands = __esm({
2454
3390
  init_types();
2455
3391
  init_client();
2456
3392
  init_handlers();
3393
+ init_weights();
3394
+ init_weights();
2457
3395
  init_format();
2458
3396
  }
2459
3397
  });
2460
3398
 
2461
- // src/core/paths.ts
2462
- import { existsSync as existsSync2 } from "fs";
2463
- import { dirname as dirname2, join as join2 } from "path";
2464
- import { fileURLToPath as fileURLToPath2 } from "url";
2465
- function packageRoot() {
2466
- if (cachedRoot) return cachedRoot;
2467
- let dir = dirname2(fileURLToPath2(import.meta.url));
2468
- for (; ; ) {
2469
- if (existsSync2(join2(dir, "package.json"))) {
2470
- cachedRoot = dir;
2471
- return dir;
2472
- }
2473
- const parent = dirname2(dir);
2474
- if (parent === dir) break;
2475
- dir = parent;
2476
- }
2477
- cachedRoot = process.cwd();
2478
- return cachedRoot;
2479
- }
2480
- function migrationsDir() {
2481
- return join2(packageRoot(), "drizzle");
2482
- }
2483
- function seedJsonPath() {
2484
- return join2(packageRoot(), "src", "data", "seed.json");
2485
- }
2486
- var cachedRoot;
2487
- var init_paths = __esm({
2488
- "src/core/paths.ts"() {
2489
- "use strict";
2490
- init_esm_shims();
2491
- }
2492
- });
2493
-
2494
3399
  // src/cli/installSkill.ts
2495
3400
  var installSkill_exports = {};
2496
3401
  __export(installSkill_exports, {
2497
3402
  SUPPORTED_AGENTS: () => SUPPORTED_AGENTS,
3403
+ agentSpecs: () => agentSpecs,
3404
+ buildRemoteServerEntry: () => buildRemoteServerEntry,
3405
+ buildRemoteTomlBlock: () => buildRemoteTomlBlock,
2498
3406
  buildServerEntry: () => buildServerEntry,
2499
3407
  buildTomlBlock: () => buildTomlBlock,
3408
+ installAgent: () => installAgent,
3409
+ installInstructionsFile: () => installInstructionsFile,
3410
+ printInstallReport: () => printInstallReport,
3411
+ resolveAgents: () => resolveAgents,
2500
3412
  runInstallSkill: () => runInstallSkill
2501
3413
  });
2502
- import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
3414
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2503
3415
  import { copyFileSync } from "fs";
2504
- import { homedir as homedir2 } from "os";
2505
- import { dirname as dirname3, join as join3 } from "path";
3416
+ import { homedir as homedir3 } from "os";
3417
+ import { dirname as dirname4, join as join3 } from "path";
2506
3418
  function home(...p) {
2507
- return join3(homedir2(), ...p);
3419
+ return join3(homedir3(), ...p);
2508
3420
  }
2509
3421
  function agentSpecs() {
2510
3422
  return [
@@ -2513,35 +3425,35 @@ function agentSpecs() {
2513
3425
  label: "Claude Code",
2514
3426
  format: "mcpServers",
2515
3427
  path: home(".claude.json"),
2516
- detected: existsSync3(home(".claude.json")) || existsSync3(home(".claude"))
3428
+ detected: existsSync4(home(".claude.json")) || existsSync4(home(".claude"))
2517
3429
  },
2518
3430
  {
2519
3431
  id: "cursor",
2520
3432
  label: "Cursor",
2521
3433
  format: "mcpServers",
2522
3434
  path: home(".cursor", "mcp.json"),
2523
- detected: existsSync3(home(".cursor"))
3435
+ detected: existsSync4(home(".cursor"))
2524
3436
  },
2525
3437
  {
2526
3438
  id: "windsurf",
2527
3439
  label: "Windsurf",
2528
3440
  format: "mcpServers",
2529
3441
  path: home(".codeium", "windsurf", "mcp_config.json"),
2530
- detected: existsSync3(home(".codeium"))
3442
+ detected: existsSync4(home(".codeium"))
2531
3443
  },
2532
3444
  {
2533
3445
  id: "copilot",
2534
3446
  label: "VS Code / GitHub Copilot",
2535
3447
  format: "servers",
2536
3448
  path: home("Library", "Application Support", "Code", "User", "mcp.json"),
2537
- detected: existsSync3(home("Library", "Application Support", "Code", "User"))
3449
+ detected: existsSync4(home("Library", "Application Support", "Code", "User"))
2538
3450
  },
2539
3451
  {
2540
3452
  id: "codex",
2541
3453
  label: "OpenAI Codex CLI",
2542
3454
  format: "toml",
2543
3455
  path: home(".codex", "config.toml"),
2544
- detected: existsSync3(home(".codex"))
3456
+ detected: existsSync4(home(".codex"))
2545
3457
  }
2546
3458
  ];
2547
3459
  }
@@ -2554,14 +3466,14 @@ function collectEnv() {
2554
3466
  return env;
2555
3467
  }
2556
3468
  function readJsonObject(path2) {
2557
- if (!existsSync3(path2)) return {};
2558
- const text2 = readFileSync(path2, "utf8").trim();
3469
+ if (!existsSync4(path2)) return {};
3470
+ const text2 = readFileSync2(path2, "utf8").trim();
2559
3471
  if (!text2) return {};
2560
3472
  return JSON.parse(text2);
2561
3473
  }
2562
3474
  function writeJson(path2, obj) {
2563
- mkdirSync(dirname3(path2), { recursive: true });
2564
- writeFileSync(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
3475
+ mkdirSync2(dirname4(path2), { recursive: true });
3476
+ writeFileSync2(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
2565
3477
  }
2566
3478
  function buildServerEntry(env, withType) {
2567
3479
  const entry = { command: "npx", args: ["-y", PACKAGE_NAME, "serve"] };
@@ -2569,13 +3481,18 @@ function buildServerEntry(env, withType) {
2569
3481
  if (Object.keys(env).length) entry.env = env;
2570
3482
  return entry;
2571
3483
  }
2572
- function installJson(spec, env) {
2573
- const key = spec.format === "servers" ? "servers" : "mcpServers";
2574
- const config = readJsonObject(spec.path);
2575
- if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
2576
- config[key].lurq = buildServerEntry(env, spec.format === "servers");
2577
- writeJson(spec.path, config);
2578
- return { agent: spec.id, path: spec.path, status: "installed" };
3484
+ function buildRemoteServerEntry(agentId, opts) {
3485
+ const headers = { Authorization: `Bearer ${opts.apiKey}` };
3486
+ switch (agentId) {
3487
+ case "cursor":
3488
+ return { url: opts.url, headers };
3489
+ case "windsurf":
3490
+ return { serverUrl: opts.url, headers };
3491
+ case "claude-code":
3492
+ case "copilot":
3493
+ default:
3494
+ return { type: "http", url: opts.url, headers };
3495
+ }
2579
3496
  }
2580
3497
  function buildTomlBlock(env) {
2581
3498
  const lines = ["[mcp_servers.lurq]", 'command = "npx"', `args = ["-y", "${PACKAGE_NAME}", "serve"]`];
@@ -2585,8 +3502,23 @@ function buildTomlBlock(env) {
2585
3502
  }
2586
3503
  return lines.join("\n") + "\n";
2587
3504
  }
2588
- function installToml(spec, env) {
2589
- const existing = existsSync3(spec.path) ? readFileSync(spec.path, "utf8") : "";
3505
+ function buildRemoteTomlBlock(opts) {
3506
+ return [
3507
+ "[mcp_servers.lurq]",
3508
+ `url = ${JSON.stringify(opts.url)}`,
3509
+ `http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.apiKey}`)} }`
3510
+ ].join("\n") + "\n";
3511
+ }
3512
+ function installJsonEntry(spec, entry) {
3513
+ const key = spec.format === "servers" ? "servers" : "mcpServers";
3514
+ const config = readJsonObject(spec.path);
3515
+ if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
3516
+ config[key].lurq = entry;
3517
+ writeJson(spec.path, config);
3518
+ return { agent: spec.id, path: spec.path, status: "installed" };
3519
+ }
3520
+ function installTomlBlock(spec, block) {
3521
+ const existing = existsSync4(spec.path) ? readFileSync2(spec.path, "utf8") : "";
2590
3522
  if (existing.includes("[mcp_servers.lurq]")) {
2591
3523
  return {
2592
3524
  agent: spec.id,
@@ -2595,59 +3527,48 @@ function installToml(spec, env) {
2595
3527
  message: "lurq already present; edit manually to change it."
2596
3528
  };
2597
3529
  }
2598
- mkdirSync(dirname3(spec.path), { recursive: true });
3530
+ mkdirSync2(dirname4(spec.path), { recursive: true });
2599
3531
  const sep = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
2600
- writeFileSync(spec.path, existing + sep + buildTomlBlock(env), "utf8");
3532
+ writeFileSync2(spec.path, existing + sep + block, "utf8");
2601
3533
  return { agent: spec.id, path: spec.path, status: "installed" };
2602
3534
  }
3535
+ function installAgent(spec, mode) {
3536
+ try {
3537
+ if (spec.format === "toml") {
3538
+ const block = mode.kind === "remote" ? buildRemoteTomlBlock(mode) : buildTomlBlock(mode.env);
3539
+ return installTomlBlock(spec, block);
3540
+ }
3541
+ const entry = mode.kind === "remote" ? buildRemoteServerEntry(spec.id, mode) : buildServerEntry(mode.env, spec.format === "servers");
3542
+ return installJsonEntry(spec, entry);
3543
+ } catch (err) {
3544
+ return {
3545
+ agent: spec.id,
3546
+ path: spec.path,
3547
+ status: "error",
3548
+ message: err instanceof Error ? err.message : String(err)
3549
+ };
3550
+ }
3551
+ }
2603
3552
  function installInstructionsFile() {
2604
3553
  const src = join3(packageRoot(), "templates", "skill-instructions.md");
2605
- if (!existsSync3(src)) return null;
3554
+ if (!existsSync4(src)) return null;
2606
3555
  const destDir = home(".lurq");
2607
3556
  const dest = join3(destDir, "skill-instructions.md");
2608
- mkdirSync(destDir, { recursive: true });
3557
+ mkdirSync2(destDir, { recursive: true });
2609
3558
  copyFileSync(src, dest);
2610
3559
  return dest;
2611
3560
  }
2612
- async function runInstallSkill(opts) {
2613
- const target = opts.agent ?? "claude-code";
3561
+ function resolveAgents(target) {
2614
3562
  const specs = agentSpecs();
2615
- const env = collectEnv();
2616
- let selected;
2617
- if (target === "all") {
2618
- selected = specs.filter((s) => s.detected);
2619
- if (selected.length === 0) {
2620
- console.log("No supported agents detected on this machine.");
2621
- return;
2622
- }
2623
- } else {
2624
- const spec = specs.find((s) => s.id === target);
2625
- if (!spec) {
2626
- throw new Error(
2627
- `Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`
2628
- );
2629
- }
2630
- selected = [spec];
2631
- }
2632
- if (!env.DATABASE_URL) {
2633
- logger.warn(
2634
- "DATABASE_URL is not set in the current environment \u2014 the installed server entry will have no DATABASE_URL. Set it (in .env) and re-run, or edit the config."
2635
- );
2636
- }
2637
- const results = [];
2638
- for (const spec of selected) {
2639
- try {
2640
- results.push(spec.format === "toml" ? installToml(spec, env) : installJson(spec, env));
2641
- } catch (err) {
2642
- results.push({
2643
- agent: spec.id,
2644
- path: spec.path,
2645
- status: "error",
2646
- message: err instanceof Error ? err.message : String(err)
2647
- });
2648
- }
3563
+ if (target === "all") return specs.filter((s) => s.detected);
3564
+ const spec = specs.find((s) => s.id === target);
3565
+ if (!spec) {
3566
+ throw new Error(`Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`);
2649
3567
  }
2650
- const instructionsPath = installInstructionsFile();
3568
+ return [spec];
3569
+ }
3570
+ function printInstallReport(results, instructionsPath, mode) {
3571
+ const specs = agentSpecs();
2651
3572
  console.log("lurq MCP server registration:");
2652
3573
  for (const r of results) {
2653
3574
  const spec = specs.find((s) => s.id === r.agent);
@@ -2657,9 +3578,43 @@ async function runInstallSkill(opts) {
2657
3578
  if (instructionsPath) console.log(`
2658
3579
  Skill instructions written to ${instructionsPath}`);
2659
3580
  console.log("\nNext steps:");
2660
- console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
2661
- console.log(" 2. Restart the agent so it picks up the new MCP server.");
2662
- console.log(" 3. Ask it to recommend a library \u2014 it should call lurq.");
3581
+ if (mode.kind === "local") {
3582
+ console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
3583
+ console.log(" 2. Restart the agent so it picks up the new MCP server.");
3584
+ } else {
3585
+ console.log(" 1. Restart the agent so it picks up the new MCP server.");
3586
+ }
3587
+ console.log(" \u2022 Ask it to recommend a library \u2014 it should call lurq.");
3588
+ }
3589
+ async function runInstallSkill(opts) {
3590
+ const selected = resolveAgents(opts.agent ?? "claude-code");
3591
+ if (selected.length === 0) {
3592
+ console.log("No supported agents detected on this machine.");
3593
+ return;
3594
+ }
3595
+ const remote = !opts.local;
3596
+ let mode;
3597
+ if (remote) {
3598
+ const apiKey = opts.apiKey ?? process.env.LURQ_API_KEY;
3599
+ if (!apiKey) {
3600
+ throw new Error(
3601
+ "An API key is required for a hosted install. Pass --api-key <key> (or set LURQ_API_KEY), or use `lurq install` for the guided setup, or --local to self-host."
3602
+ );
3603
+ }
3604
+ const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
3605
+ mode = { kind: "remote", url, apiKey };
3606
+ } else {
3607
+ const env = collectEnv();
3608
+ if (!env.DATABASE_URL) {
3609
+ logger.warn(
3610
+ "DATABASE_URL is not set \u2014 the local server entry will have no DATABASE_URL. Set it (in .env) and re-run, or edit the config."
3611
+ );
3612
+ }
3613
+ mode = { kind: "local", env };
3614
+ }
3615
+ const results = selected.map((spec) => installAgent(spec, mode));
3616
+ const instructionsPath = installInstructionsFile();
3617
+ printInstallReport(results, instructionsPath, mode);
2663
3618
  }
2664
3619
  var ENV_KEYS, SUPPORTED_AGENTS;
2665
3620
  var init_installSkill = __esm({
@@ -2683,12 +3638,228 @@ var init_installSkill = __esm({
2683
3638
  }
2684
3639
  });
2685
3640
 
3641
+ // src/cli/install.ts
3642
+ var install_exports = {};
3643
+ __export(install_exports, {
3644
+ runInstallWizard: () => runInstallWizard
3645
+ });
3646
+ async function validateKey(url, apiKey) {
3647
+ try {
3648
+ const res = await fetch(url, {
3649
+ method: "POST",
3650
+ headers: {
3651
+ "Content-Type": "application/json",
3652
+ Accept: "application/json, text/event-stream",
3653
+ Authorization: `Bearer ${apiKey}`
3654
+ },
3655
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
3656
+ });
3657
+ return res.ok;
3658
+ } catch {
3659
+ return false;
3660
+ }
3661
+ }
3662
+ async function runInstallWizard(opts) {
3663
+ const interactive = !opts.yes;
3664
+ const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
3665
+ let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
3666
+ if (interactive) {
3667
+ const { input, checkbox, confirm } = await import("@inquirer/prompts");
3668
+ console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
3669
+ if (!apiKey) {
3670
+ console.log(` Need a key? Get one at ${GET_KEY_URL}
3671
+ `);
3672
+ apiKey = (await input({
3673
+ message: "Paste your lurq API key",
3674
+ validate: (v) => v.trim().startsWith("lurq_") ? true : "Keys look like lurq_live_\u2026 "
3675
+ })).trim();
3676
+ }
3677
+ process.stdout.write(" Validating key\u2026 ");
3678
+ const ok = await validateKey(url, apiKey);
3679
+ console.log(ok ? "ok" : "could not reach endpoint");
3680
+ if (!ok) {
3681
+ const proceed = await confirm({
3682
+ message: `Couldn't validate the key against ${url}. Continue anyway?`,
3683
+ default: false
3684
+ });
3685
+ if (!proceed) {
3686
+ console.log("Aborted. No config was changed.");
3687
+ return;
3688
+ }
3689
+ }
3690
+ let selected2;
3691
+ if (opts.agent) {
3692
+ selected2 = resolveAgents(opts.agent);
3693
+ } else {
3694
+ const specs = agentSpecs();
3695
+ const ids = await checkbox({
3696
+ message: "Which assistant(s) should I configure?",
3697
+ choices: specs.map((s) => ({
3698
+ name: `${s.label}${s.detected ? " (detected)" : ""}`,
3699
+ value: s.id,
3700
+ checked: s.detected
3701
+ }))
3702
+ });
3703
+ selected2 = specs.filter((s) => ids.includes(s.id));
3704
+ }
3705
+ await finish(selected2, { url, apiKey });
3706
+ return;
3707
+ }
3708
+ if (!apiKey) {
3709
+ throw new Error(
3710
+ "No API key. Pass --api-key <key> or set LURQ_API_KEY (or drop --yes to be prompted)."
3711
+ );
3712
+ }
3713
+ const selected = opts.agent ? resolveAgents(opts.agent) : agentSpecs().filter((s) => s.detected);
3714
+ await finish(selected, { url, apiKey });
3715
+ }
3716
+ async function finish(selected, remote) {
3717
+ if (selected.length === 0) {
3718
+ console.log(
3719
+ "\nNo agents selected or detected. Re-run with --agent <id>, or install an assistant first."
3720
+ );
3721
+ return;
3722
+ }
3723
+ const mode = { kind: "remote", ...remote };
3724
+ const results = selected.map((s) => installAgent(s, mode));
3725
+ const instructionsPath = installInstructionsFile();
3726
+ console.log("");
3727
+ printInstallReport(results, instructionsPath, mode);
3728
+ }
3729
+ var GET_KEY_URL;
3730
+ var init_install = __esm({
3731
+ "src/cli/install.ts"() {
3732
+ "use strict";
3733
+ init_esm_shims();
3734
+ init_constants();
3735
+ init_installSkill();
3736
+ GET_KEY_URL = "https://lurq.run";
3737
+ }
3738
+ });
3739
+
3740
+ // src/cli/keys.ts
3741
+ var keys_exports = {};
3742
+ __export(keys_exports, {
3743
+ runKeysCreate: () => runKeysCreate,
3744
+ runKeysList: () => runKeysList,
3745
+ runKeysRevoke: () => runKeysRevoke
3746
+ });
3747
+ function waitForEnter(prompt) {
3748
+ return new Promise((resolve) => {
3749
+ process.stdout.write(prompt);
3750
+ process.stdin.resume();
3751
+ process.stdin.once("data", () => {
3752
+ process.stdin.pause();
3753
+ resolve();
3754
+ });
3755
+ });
3756
+ }
3757
+ async function runKeysCreate(opts) {
3758
+ requireConfig(["DATABASE_URL"]);
3759
+ const { db, close } = createDb({ max: 1 });
3760
+ try {
3761
+ const { key, row } = await createKey(db, { label: opts.label, tier: opts.tier });
3762
+ if (opts.json) {
3763
+ console.log(JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label }));
3764
+ return;
3765
+ }
3766
+ const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
3767
+ const block = [bold("API key created."), "", ` ${green(key)}`, "", dim(meta)];
3768
+ const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
3769
+ console.log(block.join("\n"));
3770
+ if (interactive) {
3771
+ await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
3772
+ process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
3773
+ console.log(
3774
+ dim(`API key created (prefix ${row.prefix}) \u2014 value erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
3775
+ );
3776
+ } else {
3777
+ console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
3778
+ }
3779
+ } finally {
3780
+ await close();
3781
+ }
3782
+ }
3783
+ async function runKeysList(opts) {
3784
+ requireConfig(["DATABASE_URL"]);
3785
+ const { db, close } = createDb({ max: 1 });
3786
+ try {
3787
+ const rows = await listKeys(db);
3788
+ if (opts.json) {
3789
+ console.log(
3790
+ JSON.stringify(
3791
+ rows.map((r) => ({
3792
+ id: r.id,
3793
+ prefix: r.prefix,
3794
+ label: r.label,
3795
+ tier: r.tier,
3796
+ ownerId: r.ownerId,
3797
+ createdAt: r.createdAt,
3798
+ lastUsedAt: r.lastUsedAt,
3799
+ revokedAt: r.revokedAt
3800
+ })),
3801
+ null,
3802
+ 2
3803
+ )
3804
+ );
3805
+ return;
3806
+ }
3807
+ if (rows.length === 0) {
3808
+ console.log("No API keys yet. Create one with `lurq keys create --label <name>`.");
3809
+ return;
3810
+ }
3811
+ console.log(
3812
+ table(
3813
+ ["prefix", "label", "tier", "created", "last used", "status"],
3814
+ rows.map((r) => [
3815
+ r.prefix,
3816
+ r.label ?? "\u2014",
3817
+ r.tier,
3818
+ isoDay(r.createdAt),
3819
+ isoDay(r.lastUsedAt),
3820
+ r.revokedAt ? "revoked" : "active"
3821
+ ])
3822
+ )
3823
+ );
3824
+ } finally {
3825
+ await close();
3826
+ }
3827
+ }
3828
+ async function runKeysRevoke(prefixOrId) {
3829
+ requireConfig(["DATABASE_URL"]);
3830
+ const { db, close } = createDb({ max: 1 });
3831
+ try {
3832
+ const n = await revokeKey(db, prefixOrId);
3833
+ if (n === 0) {
3834
+ logger.warn(`No active key matched "${prefixOrId}".`);
3835
+ process.exitCode = 1;
3836
+ return;
3837
+ }
3838
+ console.log(`Revoked ${n} key(s) matching "${prefixOrId}".`);
3839
+ } finally {
3840
+ await close();
3841
+ }
3842
+ }
3843
+ var isoDay;
3844
+ var init_keys = __esm({
3845
+ "src/cli/keys.ts"() {
3846
+ "use strict";
3847
+ init_esm_shims();
3848
+ init_config();
3849
+ init_logger();
3850
+ init_apiKeys();
3851
+ init_client();
3852
+ init_format();
3853
+ isoDay = (d) => d ? d.toISOString().slice(0, 10) : "\u2014";
3854
+ }
3855
+ });
3856
+
2686
3857
  // src/db/seed.ts
2687
- import { readFileSync as readFileSync2 } from "fs";
2688
- import { sql as sql5 } from "drizzle-orm";
3858
+ import { readFileSync as readFileSync3 } from "fs";
3859
+ import { sql as sql7 } from "drizzle-orm";
2689
3860
  import { z as z3 } from "zod";
2690
3861
  function loadSeedFile(path2 = seedJsonPath()) {
2691
- const raw = JSON.parse(readFileSync2(path2, "utf8"));
3862
+ const raw = JSON.parse(readFileSync3(path2, "utf8"));
2692
3863
  const parsed = SeedFileSchema.parse(raw);
2693
3864
  const byName = /* @__PURE__ */ new Map();
2694
3865
  for (const entry of parsed) {
@@ -2702,7 +3873,7 @@ async function loadSeedPackages(db, path2) {
2702
3873
  await db.insert(seedPackages).values(entries.map((e) => ({ name: e.name, category: e.category ?? null }))).onConflictDoUpdate({
2703
3874
  target: seedPackages.name,
2704
3875
  // Refresh category to the incoming value on conflict (EXCLUDED.category).
2705
- set: { category: sql5`excluded.category` }
3876
+ set: { category: sql7`excluded.category` }
2706
3877
  });
2707
3878
  logger.info(`Loaded ${entries.length} packages into seed_packages.`);
2708
3879
  return entries.length;
@@ -2732,7 +3903,13 @@ __export(migrate_exports, {
2732
3903
  });
2733
3904
  import { migrate } from "drizzle-orm/postgres-js/migrator";
2734
3905
  async function ensureVectorExtension(handle) {
2735
- await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
3906
+ try {
3907
+ await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
3908
+ } catch (err) {
3909
+ logger.warn(
3910
+ `CREATE EXTENSION vector did not run cleanly (continuing to migrations): ${err instanceof Error ? err.message : String(err)}`
3911
+ );
3912
+ }
2736
3913
  }
2737
3914
  async function runMigrate() {
2738
3915
  const handle = createDb({ max: 1 });
@@ -2791,13 +3968,17 @@ function buildProgram() {
2791
3968
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
2792
3969
  await startMcpServer2();
2793
3970
  });
3971
+ program.command("serve-http").description("start the hosted MCP server over HTTP with API-key auth").option("--port <n>", "port to listen on (default: $PORT or 8080)", (v) => parseInt(v, 10)).action(async (opts) => {
3972
+ const { startHttpServer: startHttpServer2 } = await Promise.resolve().then(() => (init_http2(), http_exports));
3973
+ await startHttpServer2({ port: opts.port });
3974
+ });
2794
3975
  program.command("sync").description("run ingestion: refresh scores for the seed list (or one package)").option("--full", "force a full re-sync, ignoring cache TTLs").option("--package <name>", "sync a single package by name").option("--json", "output the run summary as JSON").action(async (opts) => {
2795
3976
  const { runSync: runSync2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
2796
3977
  const summary = await runSync2({ full: opts.full, packageName: opts.package });
2797
3978
  if (opts.json) console.log(JSON.stringify(summary, null, 2));
2798
3979
  if (summary.status === "failed") process.exitCode = 1;
2799
3980
  });
2800
- program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
3981
+ program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | promising | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
2801
3982
  const { runRecommend: runRecommend2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
2802
3983
  await runRecommend2(need, opts);
2803
3984
  });
@@ -2813,14 +3994,53 @@ function buildProgram() {
2813
3994
  const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
2814
3995
  await runVerify2(pkg, opts);
2815
3996
  });
2816
- program.command("install-skill").description("register lurq as an MCP server in supported AI assistants").option(
3997
+ program.command("weights").description("show and explain the scoring weight model (health, quality, composite \u03BB)").option("--json", "output the weight model as JSON").action(async (opts) => {
3998
+ const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
3999
+ runWeights2(opts);
4000
+ });
4001
+ program.command("edit-weights").description("override, reset, or explain the scoring weights (layered over defaults)").option("--set <pair>", "override key=value, e.g. composite.lambda=0.5 (repeatable)", (v, acc) => acc.concat(v), []).option("--reset", "remove all overrides and restore defaults").option("--explain <component>", "explain a component (e.g. adoption, quality, lambda)").option("--project", "write to project-local .lurq/weights.json instead of the user config").action(async (opts) => {
4002
+ const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
4003
+ runEditWeights2(opts);
4004
+ });
4005
+ program.command("discover").description("operator-side: proactively crawl for new packages and queue/gate them (\xA72B)").option("--cap <n>", "max candidates to fully ingest this run", (v) => parseInt(v, 10)).option("--dry-run", "discover, queue, and gate, but do not ingest survivors").option("--json", "output the discovery summary as JSON").action(async (opts) => {
4006
+ const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4007
+ requireConfig2(["DATABASE_URL"]);
4008
+ const { runDiscovery: runDiscovery2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
4009
+ const summary = await runDiscovery2({ perRunCap: opts.cap, dryRun: opts.dryRun });
4010
+ if (opts.json) console.log(JSON.stringify(summary, null, 2));
4011
+ });
4012
+ program.command("rescore").description("re-derive health scores from cached breakdowns using current weights (no re-ingest)").option("--json", "output the rescore summary as JSON").action(async (opts) => {
4013
+ const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4014
+ requireConfig2(["DATABASE_URL"]);
4015
+ const { runRescore: runRescore2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
4016
+ const summary = await runRescore2();
4017
+ if (opts.json) console.log(JSON.stringify(summary, null, 2));
4018
+ });
4019
+ program.command("install").description("guided setup: connect lurq to your AI assistant(s)").option("--api-key <key>", "hosted API key (skips the prompt)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--agent <agent>", "claude-code | cursor | copilot | windsurf | codex | all").option("--yes", "non-interactive: use flags/env and detected agents without prompting").action(async (opts) => {
4020
+ const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
4021
+ await runInstallWizard2(opts);
4022
+ });
4023
+ program.command("install-skill").description("register lurq as an MCP server in supported AI assistants (scriptable)").option(
2817
4024
  "--agent <agent>",
2818
4025
  "claude-code | cursor | copilot | windsurf | codex | all",
2819
4026
  "claude-code"
2820
- ).action(async (opts) => {
4027
+ ).option("--api-key <key>", "hosted API key (remote install; default mode)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--local", "self-host: write a local stdio entry using your own DATABASE_URL").action(async (opts) => {
2821
4028
  const { runInstallSkill: runInstallSkill2 } = await Promise.resolve().then(() => (init_installSkill(), installSkill_exports));
2822
4029
  await runInstallSkill2(opts);
2823
4030
  });
4031
+ const keys = program.command("keys").description("manage API keys for the hosted service (operator; needs DATABASE_URL)");
4032
+ keys.command("create").description("create a new API key (shown once; erased from the terminal after you copy it)").option("--label <label>", "human label (owner / org / purpose)").option("--tier <tier>", "tier name", "free").option("--json", "print the key as JSON and skip the interactive erase (for scripts)").action(async (opts) => {
4033
+ const { runKeysCreate: runKeysCreate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4034
+ await runKeysCreate2(opts);
4035
+ });
4036
+ keys.command("list").description("list issued API keys (hashes are never shown)").option("--json", "output as JSON").action(async (opts) => {
4037
+ const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4038
+ await runKeysList2(opts);
4039
+ });
4040
+ keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
4041
+ const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4042
+ await runKeysRevoke2(prefixOrId);
4043
+ });
2824
4044
  const db = program.command("db").description("database management");
2825
4045
  db.command("migrate").description("apply database migrations and load the seed list").action(async () => {
2826
4046
  const { runMigrate: runMigrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
@@ -2840,9 +4060,11 @@ function buildProgram() {
2840
4060
  return program;
2841
4061
  }
2842
4062
  export {
4063
+ API_KEY_PREFIX,
2843
4064
  CACHE_TTL,
2844
4065
  CATEGORIES,
2845
4066
  ConfigError,
4067
+ DEFAULT_ENDPOINT,
2846
4068
  EMBEDDING_DIM,
2847
4069
  FRONTEND_CATEGORIES,
2848
4070
  PACKAGE_NAME,