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