lurqrun 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -70,14 +70,16 @@ var init_types = __esm({
70
70
  });
71
71
 
72
72
  // src/core/constants.ts
73
- var SERVER_NAME, PACKAGE_NAME, VERSION, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
73
+ var SERVER_NAME, PACKAGE_NAME, VERSION, DEFAULT_ENDPOINT, API_KEY_PREFIX, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
74
74
  var init_constants = __esm({
75
75
  "src/core/constants.ts"() {
76
76
  "use strict";
77
77
  init_esm_shims();
78
78
  SERVER_NAME = "lurq";
79
79
  PACKAGE_NAME = "lurqrun";
80
- VERSION = "0.0.1";
80
+ VERSION = "0.0.2";
81
+ DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
82
+ API_KEY_PREFIX = "lurq_live_";
81
83
  EMBEDDING_DIM = 1536;
82
84
  STALENESS_DAYS = 7;
83
85
  HOUR = 60 * 60 * 1e3;
@@ -93,6 +95,14 @@ var init_constants = __esm({
93
95
  });
94
96
 
95
97
  // src/core/config.ts
98
+ var config_exports = {};
99
+ __export(config_exports, {
100
+ ConfigError: () => ConfigError,
101
+ getConfig: () => getConfig,
102
+ loadEnv: () => loadEnv,
103
+ requireConfig: () => requireConfig,
104
+ resetConfigCache: () => resetConfigCache
105
+ });
96
106
  import { config as dotenvConfig } from "dotenv";
97
107
  import { z } from "zod";
98
108
  function loadEnv() {
@@ -112,6 +122,9 @@ ${issues}`);
112
122
  cached = parsed.data;
113
123
  return cached;
114
124
  }
125
+ function resetConfigCache() {
126
+ cached = void 0;
127
+ }
115
128
  function requireConfig(keys) {
116
129
  const config = getConfig();
117
130
  const missing = keys.filter((k) => config[k] === void 0 || config[k] === "");
@@ -139,7 +152,18 @@ var init_config = __esm({
139
152
  SUMMARY_API_KEY: z.string().min(1).optional(),
140
153
  SUMMARY_MODEL: z.string().min(1).default("gpt-4o-mini"),
141
154
  LURQ_SYNC_CONCURRENCY: z.coerce.number().int().positive().max(50).default(5),
142
- LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info")
155
+ LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
156
+ // Hosted HTTP service (`serve-http`). Server-side only.
157
+ PORT: z.coerce.number().int().positive().default(8080),
158
+ /** Per-API-key rate limit: max requests per window. */
159
+ LURQ_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(120),
160
+ /** Coarser per-IP rate limit (blunts unauthenticated floods before auth). */
161
+ LURQ_IP_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(240),
162
+ /** Rate-limit window, milliseconds (applies to both limiters). */
163
+ LURQ_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(6e4),
164
+ // Client-side (install wizard / CLI talking to a remote endpoint).
165
+ LURQ_ENDPOINT: z.string().url().optional(),
166
+ LURQ_API_KEY: z.string().min(1).optional()
143
167
  });
144
168
  ConfigError = class extends Error {
145
169
  constructor(message) {
@@ -188,29 +212,44 @@ var init_logger = __esm({
188
212
  // src/db/schema.ts
189
213
  var schema_exports = {};
190
214
  __export(schema_exports, {
215
+ apiKeys: () => apiKeys,
216
+ compatEdges: () => compatEdges,
217
+ discoveryQueue: () => discoveryQueue,
218
+ packageVersions: () => packageVersions,
191
219
  packages: () => packages,
192
220
  seedPackages: () => seedPackages,
193
- syncRuns: () => syncRuns
221
+ syncRuns: () => syncRuns,
222
+ verificationRuns: () => verificationRuns,
223
+ watchState: () => watchState
194
224
  });
225
+ import { sql } from "drizzle-orm";
195
226
  import {
196
227
  bigint,
197
228
  boolean,
229
+ customType,
198
230
  index,
199
231
  integer,
200
232
  jsonb,
201
233
  pgTable,
234
+ primaryKey,
202
235
  real,
203
236
  serial,
204
237
  text,
205
238
  timestamp,
239
+ uniqueIndex,
206
240
  vector
207
241
  } from "drizzle-orm/pg-core";
208
- var ts, packages, syncRuns, seedPackages;
242
+ var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges;
209
243
  var init_schema = __esm({
210
244
  "src/db/schema.ts"() {
211
245
  "use strict";
212
246
  init_esm_shims();
213
247
  init_constants();
248
+ tsvector = customType({
249
+ dataType() {
250
+ return "tsvector";
251
+ }
252
+ });
214
253
  ts = (name) => timestamp(name, { withTimezone: true, mode: "date" });
215
254
  packages = pgTable(
216
255
  "packages",
@@ -220,6 +259,8 @@ var init_schema = __esm({
220
259
  ecosystem: text("ecosystem").notNull().default("npm"),
221
260
  // Classification + descriptive text
222
261
  category: text("category").$type(),
262
+ /** Whether `category` was hand-curated or inferred at ingest (§2A). */
263
+ categorySource: text("category_source").$type(),
223
264
  description: text("description"),
224
265
  summary: text("summary"),
225
266
  repoUrl: text("repo_url"),
@@ -235,7 +276,6 @@ var init_schema = __esm({
235
276
  // Adoption signals
236
277
  weeklyDownloads: bigint("weekly_downloads", { mode: "number" }),
237
278
  downloadGrowth90d: real("download_growth_90d"),
238
- dependentsCount: integer("dependents_count"),
239
279
  stars: integer("stars"),
240
280
  openIssues: integer("open_issues"),
241
281
  closedIssues: integer("closed_issues"),
@@ -243,12 +283,30 @@ var init_schema = __esm({
243
283
  scorecard: real("scorecard"),
244
284
  bundleMinGzipKb: real("bundle_min_gzip_kb"),
245
285
  advisories: jsonb("advisories").$type(),
286
+ // Compatibility metadata (Tier-1): declared peer-deps + engines of the
287
+ // latest version, so a whole-stack peer-range check is one indexed query.
288
+ peerDependencies: jsonb("peer_dependencies").$type(),
289
+ peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
290
+ engines: jsonb("engines").$type(),
246
291
  // Computed outputs
247
292
  healthScore: integer("health_score"),
293
+ /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
294
+ * ranking time (composite); never folded into health_score itself. */
295
+ qualityScore: integer("quality_score"),
248
296
  confidence: text("confidence").$type(),
249
297
  scoreBreakdown: jsonb("score_breakdown").$type(),
250
298
  usageGuide: jsonb("usage_guide").$type(),
251
299
  embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
300
+ // Identity of the vector space `embedding` was produced in (e.g.
301
+ // `openai:text-embedding-3-small`, `local`). Vector search filters on the
302
+ // active provider so switching models can't compare incompatible spaces.
303
+ embeddingProvider: text("embedding_provider"),
304
+ // Lexical search vector (§3): name weighted highest (A), then category (B),
305
+ // then summary/description (C). Generated + STORED so it stays in sync with
306
+ // the row automatically; indexed with GIN for fast `@@` matching.
307
+ searchVector: tsvector("search_vector").generatedAlwaysAs(
308
+ sql`setweight(to_tsvector('english', coalesce(name, '')), 'A') || setweight(to_tsvector('english', coalesce(category, '')), 'B') || setweight(to_tsvector('english', coalesce(summary, description, '')), 'C')`
309
+ ),
252
310
  // Freshness + bookkeeping
253
311
  dataAsOf: timestamp("data_as_of", { withTimezone: true, mode: "date" }),
254
312
  createdAt: ts("created_at").notNull().defaultNow(),
@@ -258,7 +316,9 @@ var init_schema = __esm({
258
316
  index("packages_category_idx").on(table2.category),
259
317
  index("packages_health_score_idx").on(table2.healthScore),
260
318
  // pgvector HNSW index for cosine similarity search (§11).
261
- index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops"))
319
+ index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops")),
320
+ // GIN index for lexical full-text search (§3).
321
+ index("packages_search_vector_idx").using("gin", table2.searchVector)
262
322
  ]
263
323
  );
264
324
  syncRuns = pgTable("sync_runs", {
@@ -275,6 +335,93 @@ var init_schema = __esm({
275
335
  category: text("category").$type(),
276
336
  addedAt: ts("added_at").notNull().defaultNow()
277
337
  });
338
+ discoveryQueue = pgTable(
339
+ "discovery_queue",
340
+ {
341
+ id: serial("id").primaryKey(),
342
+ name: text("name").notNull().unique(),
343
+ discoveredVia: text("discovered_via").$type().notNull(),
344
+ /** Lightweight quality-only pre-score (§2B). Null until the gate runs. */
345
+ preScore: integer("pre_score"),
346
+ status: text("status").$type().notNull().default("pending"),
347
+ discoveredAt: ts("discovered_at").notNull().defaultNow()
348
+ },
349
+ (table2) => [index("discovery_queue_status_idx").on(table2.status)]
350
+ );
351
+ apiKeys = pgTable(
352
+ "api_keys",
353
+ {
354
+ id: serial("id").primaryKey(),
355
+ /** sha256 hex of the full key — the only form persisted. */
356
+ keyHash: text("key_hash").notNull().unique(),
357
+ /** Identifiable display prefix, e.g. `lurq_live_ab12cd` (safe to show/log). */
358
+ prefix: text("prefix").notNull(),
359
+ /** Free-text label (owner / org / purpose). */
360
+ label: text("label"),
361
+ /** Reserved for self-serve issuance: maps to a Clerk user id. */
362
+ ownerId: text("owner_id"),
363
+ tier: text("tier").notNull().default("free"),
364
+ createdAt: ts("created_at").notNull().defaultNow(),
365
+ lastUsedAt: ts("last_used_at"),
366
+ revokedAt: ts("revoked_at")
367
+ },
368
+ (table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
369
+ );
370
+ packageVersions = pgTable(
371
+ "package_versions",
372
+ {
373
+ packageName: text("package_name").notNull(),
374
+ version: text("version").notNull(),
375
+ publishedAt: ts("published_at")
376
+ },
377
+ (table2) => [
378
+ primaryKey({ columns: [table2.packageName, table2.version] }),
379
+ index("package_versions_name_published_idx").on(table2.packageName, table2.publishedAt)
380
+ ]
381
+ );
382
+ watchState = pgTable("watch_state", {
383
+ id: text("id").primaryKey(),
384
+ seq: text("seq").notNull(),
385
+ updatedAt: ts("updated_at")
386
+ });
387
+ verificationRuns = pgTable(
388
+ "verification_runs",
389
+ {
390
+ id: serial("id").primaryKey(),
391
+ packageName: text("package_name").notNull(),
392
+ version: text("version").notNull(),
393
+ driver: text("driver").notNull(),
394
+ moduleSystem: text("module_system").notNull(),
395
+ installed: boolean("installed").notNull(),
396
+ imported: boolean("imported"),
397
+ ranScripts: boolean("ran_scripts").notNull().default(false),
398
+ durationMs: integer("duration_ms"),
399
+ error: text("error"),
400
+ ranAt: ts("ran_at")
401
+ },
402
+ (table2) => [index("verification_runs_pkg_idx").on(table2.packageName, table2.version)]
403
+ );
404
+ compatEdges = pgTable(
405
+ "compat_edges",
406
+ {
407
+ id: serial("id").primaryKey(),
408
+ packageA: text("package_a").notNull(),
409
+ versionA: text("version_a").notNull(),
410
+ packageB: text("package_b").notNull(),
411
+ versionB: text("version_b").notNull(),
412
+ status: text("status").$type().notNull(),
413
+ driver: text("driver").notNull(),
414
+ ranAt: ts("ran_at")
415
+ },
416
+ (table2) => [
417
+ uniqueIndex("compat_edges_pair_idx").on(
418
+ table2.packageA,
419
+ table2.versionA,
420
+ table2.packageB,
421
+ table2.versionB
422
+ )
423
+ ]
424
+ );
278
425
  }
279
426
  });
280
427
 
@@ -297,6 +444,131 @@ var init_client = __esm({
297
444
  }
298
445
  });
299
446
 
447
+ // src/core/cache.ts
448
+ var cache_exports = {};
449
+ __export(cache_exports, {
450
+ cached: () => cached2,
451
+ invalidateCache: () => invalidateCache
452
+ });
453
+ function ttlSeconds() {
454
+ const n = Number(process.env.LURQ_CACHE_TTL_SEC);
455
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_TTL_SEC;
456
+ }
457
+ async function getClient() {
458
+ if (!process.env.REDIS_URL) return null;
459
+ if (!clientPromise) {
460
+ clientPromise = (async () => {
461
+ try {
462
+ const { default: Redis } = await import("ioredis");
463
+ const client = new Redis(process.env.REDIS_URL, {
464
+ maxRetriesPerRequest: 1,
465
+ enableOfflineQueue: false,
466
+ lazyConnect: false,
467
+ // Railway's private network (redis.railway.internal) is IPv6-only and
468
+ // ioredis defaults to IPv4 — family:0 lets it resolve either stack, so
469
+ // both the private URL and a public/Upstash URL work unchanged.
470
+ family: 0
471
+ });
472
+ client.on("error", (err) => logger.warn(`redis: ${err.message}`));
473
+ logger.info("Response cache enabled (REDIS_URL set).");
474
+ return client;
475
+ } catch (err) {
476
+ logger.warn(`redis disabled (init failed): ${err.message}`);
477
+ return null;
478
+ }
479
+ })();
480
+ }
481
+ return clientPromise;
482
+ }
483
+ async function namespaceVersion(client) {
484
+ if (versionMemo && Date.now() - versionMemo.at < VERSION_MEMO_MS) return versionMemo.value;
485
+ const value = await client.get(VERSION_KEY).catch(() => null) ?? "0";
486
+ versionMemo = { value, at: Date.now() };
487
+ return value;
488
+ }
489
+ async function cached2(namespace, key, compute, opts = {}) {
490
+ const client = await getClient();
491
+ if (!client) return compute();
492
+ let fullKey;
493
+ try {
494
+ const version = await namespaceVersion(client);
495
+ fullKey = `lurq:${namespace}:${version}:${key}`;
496
+ const hit = await client.get(fullKey);
497
+ if (hit != null) return JSON.parse(hit);
498
+ } catch (err) {
499
+ logger.warn(`redis read failed, bypassing cache: ${err.message}`);
500
+ return compute();
501
+ }
502
+ const value = await compute();
503
+ if (!opts.skipCache?.(value)) {
504
+ client.set(fullKey, JSON.stringify(value), "EX", ttlSeconds()).catch(() => {
505
+ });
506
+ }
507
+ return value;
508
+ }
509
+ async function invalidateCache() {
510
+ const client = await getClient();
511
+ if (!client) return;
512
+ try {
513
+ await client.incr(VERSION_KEY);
514
+ versionMemo = null;
515
+ } catch (err) {
516
+ logger.warn(`redis invalidate failed: ${err.message}`);
517
+ }
518
+ }
519
+ var VERSION_KEY, DEFAULT_TTL_SEC, VERSION_MEMO_MS, clientPromise, versionMemo;
520
+ var init_cache = __esm({
521
+ "src/core/cache.ts"() {
522
+ "use strict";
523
+ init_esm_shims();
524
+ init_logger();
525
+ VERSION_KEY = "lurq:cachever";
526
+ DEFAULT_TTL_SEC = 3600;
527
+ VERSION_MEMO_MS = 1e4;
528
+ clientPromise = null;
529
+ versionMemo = null;
530
+ }
531
+ });
532
+
533
+ // src/db/compat.ts
534
+ import { and, inArray } from "drizzle-orm";
535
+ async function getCompatMetadata(db, names) {
536
+ if (names.length === 0) return [];
537
+ return db.select({
538
+ name: packages.name,
539
+ latestVersion: packages.latestVersion,
540
+ peerDependencies: packages.peerDependencies,
541
+ peerDependenciesMeta: packages.peerDependenciesMeta,
542
+ engines: packages.engines
543
+ }).from(packages).where(inArray(packages.name, names));
544
+ }
545
+ function canonicalPair(a, b) {
546
+ const [low, high] = a.name <= b.name ? [a, b] : [b, a];
547
+ return { packageA: low.name, versionA: low.version, packageB: high.name, versionB: high.version };
548
+ }
549
+ async function upsertCompatEdge(db, edge) {
550
+ await db.insert(compatEdges).values(edge).onConflictDoUpdate({
551
+ target: [
552
+ compatEdges.packageA,
553
+ compatEdges.versionA,
554
+ compatEdges.packageB,
555
+ compatEdges.versionB
556
+ ],
557
+ set: { status: edge.status, driver: edge.driver, ranAt: edge.ranAt }
558
+ });
559
+ }
560
+ async function getCompatEdges(db, names) {
561
+ if (names.length === 0) return [];
562
+ return db.select().from(compatEdges).where(and(inArray(compatEdges.packageA, names), inArray(compatEdges.packageB, names)));
563
+ }
564
+ var init_compat = __esm({
565
+ "src/db/compat.ts"() {
566
+ "use strict";
567
+ init_esm_shims();
568
+ init_schema();
569
+ }
570
+ });
571
+
300
572
  // src/core/http.ts
301
573
  import { createHash } from "crypto";
302
574
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -361,9 +633,9 @@ async function httpRequest(url, opts) {
361
633
  } = opts;
362
634
  const key = opts.cacheKey ?? `${method} ${url} ${body ?? ""}`;
363
635
  if (ttlMs > 0 && !bypassCacheRead) {
364
- const cached2 = await readCache(key, ttlMs);
365
- if (cached2) {
366
- return { status: cached2.status, data: decode(cached2.body, accept), fromCache: true };
636
+ const cached3 = await readCache(key, ttlMs);
637
+ if (cached3) {
638
+ return { status: cached3.status, data: decode(cached3.body, accept), fromCache: true };
367
639
  }
368
640
  }
369
641
  const limiter = limiterFor(host);
@@ -530,8 +802,9 @@ function parseNpmRegistry(json2) {
530
802
  const { repoUrl, repo } = normalizeRepoUrl(repository, homepage);
531
803
  const deprecated = Boolean(latestManifest?.deprecated ?? json2?.deprecated);
532
804
  const maintainers = Array.isArray(json2?.maintainers) ? json2.maintainers.length : null;
805
+ const name = json2?.name ?? "";
533
806
  return {
534
- name: json2?.name ?? "",
807
+ name,
535
808
  description: json2?.description ?? latestManifest?.description ?? null,
536
809
  latestVersion: latest,
537
810
  license: pickLicense(json2?.license ?? latestManifest?.license),
@@ -542,9 +815,76 @@ function parseNpmRegistry(json2) {
542
815
  lastReleaseAt: toDate(latest ? time?.[latest] : null) ?? toDate(time?.modified),
543
816
  deprecated,
544
817
  maintainersCount: maintainers,
545
- readme: typeof json2?.readme === "string" ? json2.readme : null
818
+ readme: typeof json2?.readme === "string" ? json2.readme : null,
819
+ keywords: parseKeywords(json2?.keywords ?? latestManifest?.keywords),
820
+ hasTypes: detectTypes(name, latestManifest),
821
+ hasTestScript: detectTestScript(latestManifest),
822
+ directDependenciesCount: countDeps(latestManifest?.dependencies),
823
+ hasProvenance: detectProvenance(latestManifest),
824
+ hasInstallScripts: detectInstallScripts(latestManifest),
825
+ peerDependencies: parseDepMap(latestManifest?.peerDependencies),
826
+ peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
827
+ engines: parseDepMap(latestManifest?.engines),
828
+ versionTimeline: parseVersionTimeline(json2)
546
829
  };
547
830
  }
831
+ function parseDepMap(value) {
832
+ if (!value || typeof value !== "object") return null;
833
+ const out = {};
834
+ for (const [name, range] of Object.entries(value)) {
835
+ if (typeof range === "string" && range.trim() !== "") out[name] = range;
836
+ }
837
+ return Object.keys(out).length ? out : null;
838
+ }
839
+ function parsePeerMeta(value) {
840
+ if (!value || typeof value !== "object") return null;
841
+ const out = {};
842
+ for (const [name, meta] of Object.entries(value)) {
843
+ if (meta && typeof meta === "object" && "optional" in meta) {
844
+ out[name] = { optional: Boolean(meta.optional) };
845
+ }
846
+ }
847
+ return Object.keys(out).length ? out : null;
848
+ }
849
+ function detectInstallScripts(manifest) {
850
+ const scripts = manifest?.scripts;
851
+ if (!scripts || typeof scripts !== "object") return false;
852
+ return ["preinstall", "install", "postinstall"].some(
853
+ (hook) => typeof scripts[hook] === "string" && scripts[hook].trim() !== ""
854
+ );
855
+ }
856
+ function parseVersionTimeline(json2) {
857
+ const versions = json2?.versions;
858
+ if (!versions || typeof versions !== "object") return [];
859
+ const time = json2?.time ?? {};
860
+ return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
861
+ }
862
+ function parseKeywords(value) {
863
+ if (!Array.isArray(value)) return [];
864
+ return value.filter((k) => typeof k === "string");
865
+ }
866
+ function detectTypes(name, manifest) {
867
+ if (typeof manifest?.types === "string" || typeof manifest?.typings === "string") return true;
868
+ if (name.startsWith("@types/")) return true;
869
+ const exportsField = manifest?.exports;
870
+ if (exportsField && typeof exportsField === "object") {
871
+ const serialized = JSON.stringify(exportsField);
872
+ if (serialized.includes('"types"')) return true;
873
+ }
874
+ return false;
875
+ }
876
+ function detectTestScript(manifest) {
877
+ const test = manifest?.scripts?.test;
878
+ if (typeof test !== "string" || test.trim() === "") return false;
879
+ return !/no test specified/i.test(test);
880
+ }
881
+ function countDeps(deps) {
882
+ if (!deps || typeof deps !== "object") return 0;
883
+ return Object.keys(deps).length;
884
+ }
885
+ function detectProvenance(manifest) {
886
+ return Boolean(manifest?.dist?.attestations);
887
+ }
548
888
  async function fetchNpmRegistry(name, fetchImpl) {
549
889
  const url = `https://${HOST}/${encodeNpmName(name)}`;
550
890
  const { data } = await httpGetJson(url, {
@@ -582,6 +922,32 @@ var init_npmRegistry = __esm({
582
922
  }
583
923
  });
584
924
 
925
+ // src/ingestion/sources/npmSearch.ts
926
+ async function searchNpm(query, size = 20, fetchImpl) {
927
+ const url = `https://${HOST2}/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`;
928
+ try {
929
+ const { data } = await httpGetJson(url, {
930
+ host: HOST2,
931
+ ttlMs: CACHE_TTL.npmRegistry,
932
+ fetchImpl
933
+ });
934
+ const objects = Array.isArray(data?.objects) ? data.objects : [];
935
+ return objects.map((o) => ({ name: o?.package?.name, date: o?.package?.date ?? null })).filter((h) => typeof h.name === "string");
936
+ } catch {
937
+ return [];
938
+ }
939
+ }
940
+ var HOST2;
941
+ var init_npmSearch = __esm({
942
+ "src/ingestion/sources/npmSearch.ts"() {
943
+ "use strict";
944
+ init_esm_shims();
945
+ init_constants();
946
+ init_http();
947
+ HOST2 = "registry.npmjs.org";
948
+ }
949
+ });
950
+
585
951
  // src/ingestion/sources/npmDownloads.ts
586
952
  function parseWeeklyDownloads(json2) {
587
953
  const value = json2?.downloads;
@@ -627,8 +993,8 @@ function chunk(items, size) {
627
993
  async function fetchWeeklyDownloads(name, fetchImpl) {
628
994
  try {
629
995
  const { data } = await httpGetJson(
630
- `https://${HOST2}/downloads/point/last-week/${name}`,
631
- { host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
996
+ `https://${HOST3}/downloads/point/last-week/${name}`,
997
+ { host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
632
998
  );
633
999
  return parseWeeklyDownloads(data);
634
1000
  } catch (err) {
@@ -639,8 +1005,8 @@ async function fetchWeeklyDownloads(name, fetchImpl) {
639
1005
  async function fetchDownloadGrowth(name, fetchImpl) {
640
1006
  try {
641
1007
  const { data } = await httpGetJson(
642
- `https://${HOST2}/downloads/range/${last90DayRange()}/${name}`,
643
- { host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
1008
+ `https://${HOST3}/downloads/range/${last90DayRange()}/${name}`,
1009
+ { host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
644
1010
  );
645
1011
  return parseDownloadGrowth(data);
646
1012
  } catch {
@@ -654,8 +1020,8 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
654
1020
  for (const batch of chunk(unscoped, BULK_CHUNK)) {
655
1021
  try {
656
1022
  const { data } = await httpGetJson(
657
- `https://${HOST2}/downloads/point/last-week/${batch.join(",")}`,
658
- { host: HOST2, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
1023
+ `https://${HOST3}/downloads/point/last-week/${batch.join(",")}`,
1024
+ { host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
659
1025
  );
660
1026
  for (const [k, v] of parseBulkWeekly(data, batch)) result.set(k, v);
661
1027
  } catch {
@@ -669,14 +1035,14 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
669
1035
  }
670
1036
  return result;
671
1037
  }
672
- var HOST2, BULK_CHUNK;
1038
+ var HOST3, BULK_CHUNK;
673
1039
  var init_npmDownloads = __esm({
674
1040
  "src/ingestion/sources/npmDownloads.ts"() {
675
1041
  "use strict";
676
1042
  init_esm_shims();
677
1043
  init_constants();
678
1044
  init_http();
679
- HOST2 = "api.npmjs.org";
1045
+ HOST3 = "api.npmjs.org";
680
1046
  BULK_CHUNK = 128;
681
1047
  }
682
1048
  });
@@ -700,7 +1066,7 @@ function parseGithub(json2, now = /* @__PURE__ */ new Date()) {
700
1066
  async function fetchGithubRepo(owner, repo, token, fetchImpl) {
701
1067
  const body = JSON.stringify({ query: QUERY, variables: { owner, name: repo } });
702
1068
  const { data } = await httpRequest(ENDPOINT, {
703
- host: HOST3,
1069
+ host: HOST4,
704
1070
  ttlMs: CACHE_TTL.github,
705
1071
  method: "POST",
706
1072
  headers: {
@@ -715,15 +1081,15 @@ async function fetchGithubRepo(owner, repo, token, fetchImpl) {
715
1081
  });
716
1082
  return parseGithub(data);
717
1083
  }
718
- var HOST3, ENDPOINT, QUERY, ONE_YEAR_MS;
1084
+ var HOST4, ENDPOINT, QUERY, ONE_YEAR_MS;
719
1085
  var init_github = __esm({
720
1086
  "src/ingestion/sources/github.ts"() {
721
1087
  "use strict";
722
1088
  init_esm_shims();
723
1089
  init_constants();
724
1090
  init_http();
725
- HOST3 = "api.github.com";
726
- ENDPOINT = `https://${HOST3}/graphql`;
1091
+ HOST4 = "api.github.com";
1092
+ ENDPOINT = `https://${HOST4}/graphql`;
727
1093
  QUERY = `query($owner:String!, $name:String!) {
728
1094
  repository(owner:$owner, name:$name) {
729
1095
  stargazerCount
@@ -769,10 +1135,10 @@ function parseAdvisoryDetail(json2) {
769
1135
  }
770
1136
  async function fetchScorecard(owner, repo, fetchImpl) {
771
1137
  const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
772
- const url = `https://${HOST4}/v3/projects/${projectKey}`;
1138
+ const url = `https://${HOST5}/v3/projects/${projectKey}`;
773
1139
  try {
774
1140
  const { data } = await httpGetJson(url, {
775
- host: HOST4,
1141
+ host: HOST5,
776
1142
  ttlMs: CACHE_TTL.depsDev,
777
1143
  fetchImpl
778
1144
  });
@@ -782,11 +1148,11 @@ async function fetchScorecard(owner, repo, fetchImpl) {
782
1148
  }
783
1149
  }
784
1150
  async function fetchAdvisories(name, version, fetchImpl) {
785
- const versionUrl = `https://${HOST4}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
1151
+ const versionUrl = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
786
1152
  let keys = [];
787
1153
  try {
788
1154
  const { data } = await httpGetJson(versionUrl, {
789
- host: HOST4,
1155
+ host: HOST5,
790
1156
  ttlMs: CACHE_TTL.depsDev,
791
1157
  fetchImpl
792
1158
  });
@@ -796,8 +1162,8 @@ async function fetchAdvisories(name, version, fetchImpl) {
796
1162
  }
797
1163
  const details = await Promise.allSettled(
798
1164
  keys.map(
799
- (key) => httpGetJson(`https://${HOST4}/v3/advisories/${encodeURIComponent(key)}`, {
800
- host: HOST4,
1165
+ (key) => httpGetJson(`https://${HOST5}/v3/advisories/${encodeURIComponent(key)}`, {
1166
+ host: HOST5,
801
1167
  ttlMs: CACHE_TTL.depsDev,
802
1168
  fetchImpl
803
1169
  })
@@ -807,6 +1173,21 @@ async function fetchAdvisories(name, version, fetchImpl) {
807
1173
  (d) => d.status === "fulfilled"
808
1174
  ).map((d) => parseAdvisoryDetail(d.value.data));
809
1175
  }
1176
+ async function fetchDependencyNames(name, version, fetchImpl) {
1177
+ const url = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}:dependencies`;
1178
+ try {
1179
+ const { data } = await httpGetJson(url, {
1180
+ host: HOST5,
1181
+ ttlMs: CACHE_TTL.depsDev,
1182
+ fetchImpl
1183
+ });
1184
+ const nodes = Array.isArray(data?.nodes) ? data.nodes : [];
1185
+ const names = nodes.map((n) => n?.versionKey?.name).filter((n) => typeof n === "string" && n !== name);
1186
+ return [...new Set(names)];
1187
+ } catch {
1188
+ return [];
1189
+ }
1190
+ }
810
1191
  async function fetchDepsDev(name, version, repo, fetchImpl) {
811
1192
  const [scorecard, advisories] = await Promise.all([
812
1193
  repo ? fetchScorecard(repo.owner, repo.repo, fetchImpl) : Promise.resolve(null),
@@ -814,14 +1195,14 @@ async function fetchDepsDev(name, version, repo, fetchImpl) {
814
1195
  ]);
815
1196
  return { scorecard, advisories };
816
1197
  }
817
- var HOST4, MAX_ADVISORIES;
1198
+ var HOST5, MAX_ADVISORIES;
818
1199
  var init_depsDev = __esm({
819
1200
  "src/ingestion/sources/depsDev.ts"() {
820
1201
  "use strict";
821
1202
  init_esm_shims();
822
1203
  init_constants();
823
1204
  init_http();
824
- HOST4 = "api.deps.dev";
1205
+ HOST5 = "api.deps.dev";
825
1206
  MAX_ADVISORIES = 10;
826
1207
  }
827
1208
  });
@@ -834,10 +1215,10 @@ function parseBundleSize(json2) {
834
1215
  }
835
1216
  async function fetchBundlephobia(name, category, fetchImpl) {
836
1217
  if (!isFrontendCategory(category)) return { bundleMinGzipKb: null };
837
- const url = `https://${HOST5}/api/size?package=${encodeURIComponent(name)}`;
1218
+ const url = `https://${HOST6}/api/size?package=${encodeURIComponent(name)}`;
838
1219
  try {
839
1220
  const { data } = await httpGetJson(url, {
840
- host: HOST5,
1221
+ host: HOST6,
841
1222
  ttlMs: CACHE_TTL.bundlephobia,
842
1223
  timeoutMs: 12e3,
843
1224
  retries: 1,
@@ -848,7 +1229,7 @@ async function fetchBundlephobia(name, category, fetchImpl) {
848
1229
  return { bundleMinGzipKb: null };
849
1230
  }
850
1231
  }
851
- var HOST5;
1232
+ var HOST6;
852
1233
  var init_bundlephobia = __esm({
853
1234
  "src/ingestion/sources/bundlephobia.ts"() {
854
1235
  "use strict";
@@ -856,7 +1237,7 @@ var init_bundlephobia = __esm({
856
1237
  init_constants();
857
1238
  init_http();
858
1239
  init_types();
859
- HOST5 = "bundlephobia.com";
1240
+ HOST6 = "bundlephobia.com";
860
1241
  }
861
1242
  });
862
1243
 
@@ -866,6 +1247,7 @@ var init_sources = __esm({
866
1247
  "use strict";
867
1248
  init_esm_shims();
868
1249
  init_npmRegistry();
1250
+ init_npmSearch();
869
1251
  init_npmDownloads();
870
1252
  init_github();
871
1253
  init_depsDev();
@@ -873,13 +1255,255 @@ var init_sources = __esm({
873
1255
  }
874
1256
  });
875
1257
 
1258
+ // src/compat/members.ts
1259
+ async function assembleMembers(db, names) {
1260
+ const tracked = new Map((await getCompatMetadata(db, names)).map((r) => [r.name, r]));
1261
+ const members = [];
1262
+ const unverified = [];
1263
+ await Promise.all(
1264
+ names.map(async (name) => {
1265
+ const row = tracked.get(name);
1266
+ if (row) {
1267
+ members.push({
1268
+ name,
1269
+ version: row.latestVersion,
1270
+ peerDependencies: row.peerDependencies,
1271
+ peerDependenciesMeta: row.peerDependenciesMeta,
1272
+ engines: row.engines
1273
+ });
1274
+ return;
1275
+ }
1276
+ const reg = await fetchNpmRegistry(name).catch(() => null);
1277
+ if (reg) {
1278
+ members.push({
1279
+ name,
1280
+ version: reg.latestVersion,
1281
+ peerDependencies: reg.peerDependencies,
1282
+ peerDependenciesMeta: reg.peerDependenciesMeta,
1283
+ engines: reg.engines
1284
+ });
1285
+ } else {
1286
+ unverified.push(name);
1287
+ }
1288
+ })
1289
+ );
1290
+ return { members, unverified };
1291
+ }
1292
+ var init_members = __esm({
1293
+ "src/compat/members.ts"() {
1294
+ "use strict";
1295
+ init_esm_shims();
1296
+ init_compat();
1297
+ init_sources();
1298
+ }
1299
+ });
1300
+
1301
+ // src/compat/peerCompat.ts
1302
+ import semver from "semver";
1303
+ function satisfiesRange(version, range) {
1304
+ if (!semver.validRange(range)) return null;
1305
+ const v = semver.valid(version) ? version : semver.coerce(version)?.version;
1306
+ if (!v) return null;
1307
+ return semver.satisfies(v, range, { includePrerelease: true });
1308
+ }
1309
+ function rangesIntersect(a, b) {
1310
+ if (!semver.validRange(a) || !semver.validRange(b)) return null;
1311
+ try {
1312
+ return semver.intersects(a, b, { includePrerelease: true });
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ function resolveArchitectureCompat(members) {
1318
+ const conflicts = [];
1319
+ const pinned = /* @__PURE__ */ new Map();
1320
+ for (const m of members) if (m.version) pinned.set(m.name, m.version);
1321
+ const constraints = [];
1322
+ for (const m of members) {
1323
+ if (!m.peerDependencies) continue;
1324
+ for (const [peer, range] of Object.entries(m.peerDependencies)) {
1325
+ constraints.push({
1326
+ requirer: m.name,
1327
+ peer,
1328
+ range,
1329
+ optional: Boolean(m.peerDependenciesMeta?.[peer]?.optional)
1330
+ });
1331
+ }
1332
+ }
1333
+ for (const c of constraints) {
1334
+ const pv = pinned.get(c.peer);
1335
+ if (pv && satisfiesRange(pv, c.range) === false) {
1336
+ conflicts.push({
1337
+ source: "peer-deps",
1338
+ packages: [c.requirer, c.peer],
1339
+ detail: `${c.requirer} needs peer ${c.peer}@${c.range}, but the stack uses ${c.peer}@${pv}`
1340
+ });
1341
+ }
1342
+ }
1343
+ const byPeer = /* @__PURE__ */ new Map();
1344
+ for (const c of constraints) {
1345
+ if (pinned.has(c.peer) || c.optional) continue;
1346
+ const arr = byPeer.get(c.peer);
1347
+ if (arr) arr.push(c);
1348
+ else byPeer.set(c.peer, [c]);
1349
+ }
1350
+ for (const [peer, cs] of byPeer) {
1351
+ for (let i = 0; i < cs.length; i++) {
1352
+ for (let j = i + 1; j < cs.length; j++) {
1353
+ if (cs[i].range !== cs[j].range && rangesIntersect(cs[i].range, cs[j].range) === false) {
1354
+ conflicts.push({
1355
+ source: "peer-deps",
1356
+ packages: [cs[i].requirer, cs[j].requirer],
1357
+ detail: `${cs[i].requirer} needs ${peer}@${cs[i].range} but ${cs[j].requirer} needs ${peer}@${cs[j].range} \u2014 no overlapping version`
1358
+ });
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+ const nodeReqs = members.map((m) => ({ name: m.name, range: m.engines?.node })).filter((r) => typeof r.range === "string");
1364
+ for (let i = 0; i < nodeReqs.length; i++) {
1365
+ for (let j = i + 1; j < nodeReqs.length; j++) {
1366
+ if (rangesIntersect(nodeReqs[i].range, nodeReqs[j].range) === false) {
1367
+ conflicts.push({
1368
+ source: "engines",
1369
+ packages: [nodeReqs[i].name, nodeReqs[j].name],
1370
+ detail: `${nodeReqs[i].name} needs node ${nodeReqs[i].range} but ${nodeReqs[j].name} needs node ${nodeReqs[j].range} \u2014 no overlap`
1371
+ });
1372
+ }
1373
+ }
1374
+ }
1375
+ return conflicts;
1376
+ }
1377
+ var init_peerCompat = __esm({
1378
+ "src/compat/peerCompat.ts"() {
1379
+ "use strict";
1380
+ init_esm_shims();
1381
+ }
1382
+ });
1383
+
1384
+ // src/compat/check.ts
1385
+ async function checkCompat(db, packages2) {
1386
+ const names = [...new Set(packages2)];
1387
+ const { members, unverified } = await assembleMembers(db, names);
1388
+ const conflicts = resolveArchitectureCompat(members);
1389
+ for (const edge of await getCompatEdges(db, names)) {
1390
+ if (edge.status === "conflict") {
1391
+ conflicts.push({
1392
+ source: "sandbox",
1393
+ packages: [edge.packageA, edge.packageB],
1394
+ detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
1395
+ });
1396
+ }
1397
+ }
1398
+ const overall = conflicts.length ? "conflict" : unverified.length ? "unknown" : "compatible";
1399
+ return {
1400
+ packages: names,
1401
+ overall,
1402
+ conflicts,
1403
+ unverified,
1404
+ checked: members.map((m) => ({ name: m.name, version: m.version }))
1405
+ };
1406
+ }
1407
+ var init_check = __esm({
1408
+ "src/compat/check.ts"() {
1409
+ "use strict";
1410
+ init_esm_shims();
1411
+ init_compat();
1412
+ init_members();
1413
+ init_peerCompat();
1414
+ }
1415
+ });
1416
+
1417
+ // src/db/packages.ts
1418
+ import { desc, eq, isNotNull } from "drizzle-orm";
1419
+ async function getSeedTargets(db) {
1420
+ const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
1421
+ return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
1422
+ }
1423
+ async function getPackageByName(db, name) {
1424
+ const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
1425
+ return rows[0] ?? null;
1426
+ }
1427
+ async function getAllPackageNames(db) {
1428
+ const rows = await db.select({ name: packages.name }).from(packages);
1429
+ return rows.map((r) => r.name);
1430
+ }
1431
+ async function upsertPackageVersions(db, name, versions) {
1432
+ if (versions.length === 0) return;
1433
+ for (let i = 0; i < versions.length; i += VERSION_CHUNK) {
1434
+ const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
1435
+ packageName: name,
1436
+ version: v.version,
1437
+ publishedAt: v.publishedAt
1438
+ }));
1439
+ await db.insert(packageVersions).values(rows).onConflictDoNothing();
1440
+ }
1441
+ }
1442
+ async function getPackageVersions(db, name, limit = 50) {
1443
+ const rows = await db.select({ version: packageVersions.version, publishedAt: packageVersions.publishedAt }).from(packageVersions).where(eq(packageVersions.packageName, name)).orderBy(desc(packageVersions.publishedAt)).limit(limit);
1444
+ return rows.map((r) => ({ version: r.version, publishedAt: r.publishedAt }));
1445
+ }
1446
+ async function getTopPackageNames(db, limit = 1e3) {
1447
+ const rows = await db.select({ name: packages.name }).from(packages).where(isNotNull(packages.weeklyDownloads)).orderBy(desc(packages.weeklyDownloads)).limit(limit);
1448
+ return rows.map((r) => r.name);
1449
+ }
1450
+ async function ensureSeedEntry(db, name, category) {
1451
+ await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
1452
+ }
1453
+ async function upsertPackage(db, row) {
1454
+ const { name: _name, createdAt: _createdAt, ...mutable } = row;
1455
+ await db.insert(packages).values(row).onConflictDoUpdate({
1456
+ target: packages.name,
1457
+ set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
1458
+ });
1459
+ }
1460
+ async function startSyncRun(db) {
1461
+ const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
1462
+ return row.id;
1463
+ }
1464
+ async function finishSyncRun(db, id, data) {
1465
+ await db.update(syncRuns).set({
1466
+ finishedAt: /* @__PURE__ */ new Date(),
1467
+ packagesSeen: data.packagesSeen,
1468
+ packagesUpdated: data.packagesUpdated,
1469
+ errors: data.errors,
1470
+ status: data.status
1471
+ }).where(eq(syncRuns.id, id));
1472
+ }
1473
+ var VERSION_CHUNK;
1474
+ var init_packages = __esm({
1475
+ "src/db/packages.ts"() {
1476
+ "use strict";
1477
+ init_esm_shims();
1478
+ init_schema();
1479
+ VERSION_CHUNK = 500;
1480
+ }
1481
+ });
1482
+
1483
+ // src/db/verification.ts
1484
+ import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
1485
+ async function storeVerificationRun(db, run) {
1486
+ await db.insert(verificationRuns).values(run);
1487
+ }
1488
+ async function getLatestVerificationByName(db, packageName) {
1489
+ const rows = await db.select().from(verificationRuns).where(eq2(verificationRuns.packageName, packageName)).orderBy(desc2(verificationRuns.ranAt)).limit(1);
1490
+ return rows[0] ?? null;
1491
+ }
1492
+ var init_verification = __esm({
1493
+ "src/db/verification.ts"() {
1494
+ "use strict";
1495
+ init_esm_shims();
1496
+ init_schema();
1497
+ }
1498
+ });
1499
+
876
1500
  // src/ingestion/sources/githubReadme.ts
877
1501
  async function fetchGithubReadme(owner, repo, fetchImpl) {
878
1502
  for (const file of CANDIDATES) {
879
- const url = `https://${HOST6}/${owner}/${repo}/HEAD/${file}`;
1503
+ const url = `https://${HOST7}/${owner}/${repo}/HEAD/${file}`;
880
1504
  try {
881
1505
  const { data } = await httpRequest(url, {
882
- host: HOST6,
1506
+ host: HOST7,
883
1507
  ttlMs: CACHE_TTL.github,
884
1508
  accept: "text",
885
1509
  retries: 0,
@@ -891,14 +1515,14 @@ async function fetchGithubReadme(owner, repo, fetchImpl) {
891
1515
  }
892
1516
  return null;
893
1517
  }
894
- var HOST6, CANDIDATES;
1518
+ var HOST7, CANDIDATES;
895
1519
  var init_githubReadme = __esm({
896
1520
  "src/ingestion/sources/githubReadme.ts"() {
897
1521
  "use strict";
898
1522
  init_esm_shims();
899
1523
  init_constants();
900
1524
  init_http();
901
- HOST6 = "raw.githubusercontent.com";
1525
+ HOST7 = "raw.githubusercontent.com";
902
1526
  CANDIDATES = ["README.md", "readme.md", "README.markdown", "README"];
903
1527
  }
904
1528
  });
@@ -915,6 +1539,7 @@ function truncateSentences(text2, max = 3) {
915
1539
  }
916
1540
  function buildUserPrompt(input) {
917
1541
  const readme = (input.readme ?? "").slice(0, 4e3);
1542
+ const needsCategory = input.category == null;
918
1543
  return [
919
1544
  `Package: ${input.name}`,
920
1545
  `Category: ${input.category ?? "unknown"}`,
@@ -928,7 +1553,8 @@ ${readme || "(none)"}`,
928
1553
  '- "whenToUse": one sentence on the ideal use case.',
929
1554
  '- "whenNotToUse": one sentence on when to pick something else (or "").',
930
1555
  '- "whereItFits": one sentence on its architectural role.',
931
- '- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".'
1556
+ '- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".',
1557
+ ...needsCategory ? [`- "category": the single best fit from this list: ${CATEGORIES.join(", ")}.`] : []
932
1558
  ].join("\n");
933
1559
  }
934
1560
  function str(value) {
@@ -962,6 +1588,7 @@ var init_summarize = __esm({
962
1588
  "src/ingestion/summarize.ts"() {
963
1589
  "use strict";
964
1590
  init_esm_shims();
1591
+ init_types();
965
1592
  init_config();
966
1593
  init_http();
967
1594
  init_logger();
@@ -1003,7 +1630,7 @@ var init_summarize = __esm({
1003
1630
  whereItFits: `Sits at ${role}.`,
1004
1631
  context7Hint: context7Hint(input.name)
1005
1632
  };
1006
- return { summary, usageGuide: guide };
1633
+ return { summary, usageGuide: guide, inferredCategory: null };
1007
1634
  }
1008
1635
  };
1009
1636
  SYSTEM_PROMPT = "You summarize npm packages factually for an AI coding agent choosing dependencies. Use ONLY the provided README/description. Never invent capabilities or APIs. Be concise and concrete. Respond with a JSON object.";
@@ -1051,7 +1678,13 @@ var init_summarize = __esm({
1051
1678
  howToWireIn: str(parsed.howToWireIn) || void 0,
1052
1679
  context7Hint: context7Hint(input.name)
1053
1680
  };
1054
- return { summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null, usageGuide: guide };
1681
+ const rawCategory = str(parsed.category);
1682
+ const inferredCategory = rawCategory && isCategory(rawCategory) ? rawCategory : null;
1683
+ return {
1684
+ summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null,
1685
+ usageGuide: guide,
1686
+ inferredCategory
1687
+ };
1055
1688
  } catch (err) {
1056
1689
  logger.warn(`summary LLM failed for ${input.name}, using fallback: ${err.message}`);
1057
1690
  return new FallbackSummaryProvider().generate(input);
@@ -1103,47 +1736,256 @@ var init_collect = __esm({
1103
1736
  }
1104
1737
  });
1105
1738
 
1106
- // src/scoring/weights.ts
1107
- var HEALTH_WEIGHTS, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, CONFIDENCE;
1108
- var init_weights = __esm({
1109
- "src/scoring/weights.ts"() {
1739
+ // src/search/categoryInference.ts
1740
+ function inferCategory(need) {
1741
+ const text2 = need.toLowerCase();
1742
+ for (const rule of RULES) {
1743
+ if (rule.patterns.test(text2)) return rule.category;
1744
+ }
1745
+ return null;
1746
+ }
1747
+ function inferCategoryFromSignals(signals) {
1748
+ const r = signals.registry;
1749
+ const keywords = (r?.keywords ?? []).join(" ");
1750
+ const text2 = [
1751
+ signals.name,
1752
+ keywords,
1753
+ keywords,
1754
+ // weight keywords higher via repetition
1755
+ r?.description ?? "",
1756
+ (r?.readme ?? "").slice(0, 600)
1757
+ ].filter(Boolean).join(" ");
1758
+ return inferCategory(text2);
1759
+ }
1760
+ var RULES;
1761
+ var init_categoryInference = __esm({
1762
+ "src/search/categoryInference.ts"() {
1110
1763
  "use strict";
1111
1764
  init_esm_shims();
1112
- HEALTH_WEIGHTS = {
1113
- maintenance: 0.35,
1114
- adoption: 0.3,
1115
- reliability: 0.25,
1116
- efficiency: 0.1
1117
- };
1118
- MAINTENANCE_WEIGHTS = {
1119
- recency: 0.5,
1120
- cadence: 0.25,
1121
- closeRatio: 0.25
1122
- };
1123
- MAINTENANCE = {
1124
- /** A release within this many days scores full recency. */
1125
- freshDays: 30,
1126
- /** Recency decays linearly to 0 by this many days (~24 months). */
1127
- staleDays: 730,
1128
- /** Releases/12mo at or above this cap full cadence. */
1129
- cadenceCap: 12,
1130
- /** deprecated/archived hard-cap maintenance at this value. */
1131
- deadCap: 15
1132
- };
1133
- ADOPTION = {
1134
- /** log10(weekly downloads): this maps to 0. */
1135
- downloadsLogMin: 1,
1136
- // 10/wk
1137
- /** log10(weekly downloads): this maps to 100. */
1138
- downloadsLogMax: 7,
1139
- // 10M/wk
1140
- /** log10(stars) that maps to 100. */
1141
- starsLogMax: 5,
1142
- // 100k stars
1143
- /** Weight of the stars component when available (downloads gets the rest). */
1144
- starsWeight: 0.2,
1145
- /** Growth bonus: growth fraction × this, clamped to ±cap. */
1146
- growthMultiplier: 30,
1765
+ RULES = [
1766
+ { category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
1767
+ { category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
1768
+ { category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
1769
+ { category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
1770
+ { category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
1771
+ { category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
1772
+ { category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
1773
+ { category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
1774
+ { category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
1775
+ { category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
1776
+ { category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
1777
+ { category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
1778
+ { category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
1779
+ { category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
1780
+ { category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
1781
+ // date-time precedes linting: "date formatting" must not be captured by linting's
1782
+ // bare "formatting" token before the date rule is reached (first-match-wins).
1783
+ { category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
1784
+ { category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
1785
+ { category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
1786
+ { category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
1787
+ { category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
1788
+ { category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
1789
+ { category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
1790
+ ];
1791
+ }
1792
+ });
1793
+
1794
+ // src/core/paths.ts
1795
+ import { existsSync as existsSync2 } from "fs";
1796
+ import { homedir as homedir2 } from "os";
1797
+ import { dirname as dirname2, join as join2 } from "path";
1798
+ import { fileURLToPath as fileURLToPath2 } from "url";
1799
+ function packageRoot() {
1800
+ if (cachedRoot) return cachedRoot;
1801
+ let dir = dirname2(fileURLToPath2(import.meta.url));
1802
+ for (; ; ) {
1803
+ if (existsSync2(join2(dir, "package.json"))) {
1804
+ cachedRoot = dir;
1805
+ return dir;
1806
+ }
1807
+ const parent = dirname2(dir);
1808
+ if (parent === dir) break;
1809
+ dir = parent;
1810
+ }
1811
+ cachedRoot = process.cwd();
1812
+ return cachedRoot;
1813
+ }
1814
+ function migrationsDir() {
1815
+ return join2(packageRoot(), "drizzle");
1816
+ }
1817
+ function seedJsonPath() {
1818
+ return join2(packageRoot(), "src", "data", "seed.json");
1819
+ }
1820
+ function userWeightsPath() {
1821
+ const base = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
1822
+ return join2(base, "lurq", "weights.json");
1823
+ }
1824
+ function projectWeightsPath() {
1825
+ return join2(process.cwd(), ".lurq", "weights.json");
1826
+ }
1827
+ var cachedRoot;
1828
+ var init_paths = __esm({
1829
+ "src/core/paths.ts"() {
1830
+ "use strict";
1831
+ init_esm_shims();
1832
+ }
1833
+ });
1834
+
1835
+ // src/scoring/weights.ts
1836
+ import { existsSync as existsSync3, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
1837
+ import { dirname as dirname3 } from "path";
1838
+ function activeWeightsPath() {
1839
+ if (existsSync3(projectWeightsPath())) return { path: projectWeightsPath(), source: "project-config" };
1840
+ if (existsSync3(userWeightsPath())) return { path: userWeightsPath(), source: "user-config" };
1841
+ return null;
1842
+ }
1843
+ function loadWeights() {
1844
+ if (cachedWeights) return cachedWeights;
1845
+ let merged = structuredClone(DEFAULT_WEIGHTS);
1846
+ const active = activeWeightsPath();
1847
+ if (active) {
1848
+ try {
1849
+ const fromFile = JSON.parse(readFileSync(active.path, "utf8"));
1850
+ merged = mergeWeights(merged, fromFile);
1851
+ } catch {
1852
+ }
1853
+ }
1854
+ const envLambda = process.env.LURQ_COMPOSITE_LAMBDA;
1855
+ if (envLambda !== void 0 && envLambda !== "") {
1856
+ const n = Number(envLambda);
1857
+ if (Number.isFinite(n)) merged.composite.lambda = n;
1858
+ }
1859
+ cachedWeights = validateWeights(merged).weights;
1860
+ return cachedWeights;
1861
+ }
1862
+ function resetWeightsCache() {
1863
+ cachedWeights = void 0;
1864
+ }
1865
+ function mergeWeights(base, over) {
1866
+ return {
1867
+ health: { ...base.health, ...over.health ?? {} },
1868
+ composite: { ...base.composite, ...over.composite ?? {} }
1869
+ };
1870
+ }
1871
+ function validateWeights(w) {
1872
+ const lambda = Math.max(0, Math.min(1, w.composite.lambda));
1873
+ const h = w.health;
1874
+ const sum = h.maintenance + h.adoption + h.reliability + h.efficiency;
1875
+ let normalized = false;
1876
+ let health = h;
1877
+ if (sum <= 0) {
1878
+ health = { ...DEFAULT_WEIGHTS.health };
1879
+ normalized = true;
1880
+ } else if (Math.abs(sum - 1) > 1e-6) {
1881
+ health = {
1882
+ maintenance: h.maintenance / sum,
1883
+ adoption: h.adoption / sum,
1884
+ reliability: h.reliability / sum,
1885
+ efficiency: h.efficiency / sum
1886
+ };
1887
+ normalized = true;
1888
+ }
1889
+ return { weights: { health, composite: { lambda } }, normalized };
1890
+ }
1891
+ function settableKeys() {
1892
+ return Object.keys(SETTABLE);
1893
+ }
1894
+ function applyOverrides(base, sets) {
1895
+ const next = structuredClone(base);
1896
+ for (const entry of sets) {
1897
+ const eq9 = entry.indexOf("=");
1898
+ if (eq9 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
1899
+ const key = entry.slice(0, eq9).trim();
1900
+ const value = Number(entry.slice(eq9 + 1).trim());
1901
+ const apply = SETTABLE[key];
1902
+ if (!apply) {
1903
+ throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
1904
+ }
1905
+ if (!Number.isFinite(value)) throw new Error(`Value for "${key}" must be a number.`);
1906
+ apply(next, value);
1907
+ }
1908
+ return next;
1909
+ }
1910
+ function saveWeights(w, scope = "user") {
1911
+ const path2 = scope === "project" ? projectWeightsPath() : userWeightsPath();
1912
+ mkdirSync(dirname3(path2), { recursive: true });
1913
+ writeFileSync(path2, JSON.stringify(w, null, 2) + "\n", "utf8");
1914
+ resetWeightsCache();
1915
+ return path2;
1916
+ }
1917
+ function resetWeights() {
1918
+ const removed = [];
1919
+ for (const path2 of [projectWeightsPath(), userWeightsPath()]) {
1920
+ if (existsSync3(path2)) {
1921
+ rmSync(path2);
1922
+ removed.push(path2);
1923
+ }
1924
+ }
1925
+ resetWeightsCache();
1926
+ return removed;
1927
+ }
1928
+ var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DISCOVERY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
1929
+ var init_weights = __esm({
1930
+ "src/scoring/weights.ts"() {
1931
+ "use strict";
1932
+ init_esm_shims();
1933
+ init_paths();
1934
+ HEALTH_WEIGHTS = {
1935
+ maintenance: 0.35,
1936
+ adoption: 0.3,
1937
+ reliability: 0.25,
1938
+ efficiency: 0.1
1939
+ };
1940
+ COMPOSITE = {
1941
+ lambda: 0.35
1942
+ };
1943
+ QUALITY_WEIGHTS = {
1944
+ types: 0.25,
1945
+ tests: 0.2,
1946
+ docs: 0.2,
1947
+ changelog: 0.1,
1948
+ deps: 0.1,
1949
+ license: 0.1,
1950
+ provenance: 0.05
1951
+ };
1952
+ QUALITY = {
1953
+ /** README at/above this many characters scores full marks on doc length. */
1954
+ docsFullLengthChars: 1200,
1955
+ /** Direct-dependency count at/above this scores 0 on the deps component (log-scaled). */
1956
+ depsLogMax: 30,
1957
+ /** A license string present but not OSI-recognized scores this (vs 100 / 0). */
1958
+ licenseUnrecognizedScore: 60
1959
+ };
1960
+ MAINTENANCE_WEIGHTS = {
1961
+ recency: 0.5,
1962
+ cadence: 0.25,
1963
+ closeRatio: 0.25
1964
+ };
1965
+ MAINTENANCE = {
1966
+ /** A release within this many days scores full recency. */
1967
+ freshDays: 30,
1968
+ /** Recency decays linearly to 0 by this many days (~24 months). */
1969
+ staleDays: 730,
1970
+ /** Releases/12mo at or above this cap full cadence. */
1971
+ cadenceCap: 12,
1972
+ /** deprecated/archived hard-cap maintenance at this value. */
1973
+ deadCap: 15
1974
+ };
1975
+ ADOPTION = {
1976
+ /** log10(weekly downloads): this maps to 0. */
1977
+ downloadsLogMin: 1,
1978
+ // 10/wk
1979
+ /** log10(weekly downloads): this maps to 100. */
1980
+ downloadsLogMax: 7,
1981
+ // 10M/wk
1982
+ /** log10(stars) that maps to 100. */
1983
+ starsLogMax: 5,
1984
+ // 100k stars
1985
+ /** Weight of the stars component when available (downloads gets the rest). */
1986
+ starsWeight: 0.2,
1987
+ /** Growth bonus: growth fraction × this, clamped to ±cap. */
1988
+ growthMultiplier: 30,
1147
1989
  growthBonusCap: 20
1148
1990
  };
1149
1991
  RELIABILITY = {
@@ -1162,6 +2004,35 @@ var init_weights = __esm({
1162
2004
  /** Bundle size at the category median maps to this score; smaller → higher. */
1163
2005
  medianScore: 50
1164
2006
  };
2007
+ DISCOVERY = {
2008
+ /** A queued candidate must clear this quality pre-score to graduate to ingest. */
2009
+ minPreScore: 45,
2010
+ /** Max candidates fully ingested per crawler run (cost bound). */
2011
+ perRunCap: 25,
2012
+ /** npm-search hits to pull per category keyword. */
2013
+ searchSizePerCategory: 10,
2014
+ /** Max dependency-graph neighbors to enqueue per tracked seed. */
2015
+ graphNeighborsPerSeed: 20
2016
+ };
2017
+ DEFAULT_WEIGHTS = {
2018
+ health: { ...HEALTH_WEIGHTS },
2019
+ composite: { ...COMPOSITE }
2020
+ };
2021
+ SETTABLE = {
2022
+ "health.maintenance": (w, v) => w.health.maintenance = v,
2023
+ "health.adoption": (w, v) => w.health.adoption = v,
2024
+ "health.reliability": (w, v) => w.health.reliability = v,
2025
+ "health.efficiency": (w, v) => w.health.efficiency = v,
2026
+ "composite.lambda": (w, v) => w.composite.lambda = v
2027
+ };
2028
+ WEIGHT_EXPLANATIONS = {
2029
+ maintenance: "release recency, cadence, and issue close-ratio (weights.ts MAINTENANCE_WEIGHTS).",
2030
+ adoption: "weekly downloads (log-scaled), stars, and 90-day growth (weights.ts ADOPTION).",
2031
+ reliability: "OpenSSF Scorecard scaled 0\u2013100, minus advisory penalties (weights.ts RELIABILITY).",
2032
+ efficiency: "bundle size vs the category median; frontend categories only (weights.ts EFFICIENCY).",
2033
+ quality: "intrinsic, adoption-independent: types, tests, docs, changelog, deps, license, provenance (weights.ts QUALITY_WEIGHTS). A standalone axis \u2014 it does NOT feed health.",
2034
+ lambda: "how much the default sort composite favors quality over health: (1\u2212\u03BB)\xB7health + \u03BB\xB7quality."
2035
+ };
1165
2036
  CONFIDENCE = {
1166
2037
  proven: {
1167
2038
  minWeeklyDownloads: 1e5,
@@ -1172,6 +2043,11 @@ var init_weights = __esm({
1172
2043
  minWeeklyDownloads: 5e3,
1173
2044
  strongGrowth: 0.5,
1174
2045
  maxLastReleaseMonths: 9
2046
+ },
2047
+ /** `promising` (§1): new but intrinsically high-quality, regardless of adoption. */
2048
+ promising: {
2049
+ minQuality: 70,
2050
+ maxLastReleaseMonths: 12
1175
2051
  }
1176
2052
  };
1177
2053
  }
@@ -1180,12 +2056,11 @@ var init_weights = __esm({
1180
2056
  // src/scoring/score.ts
1181
2057
  function toScoringInput(signals, category) {
1182
2058
  const { registry, downloads, github, depsDev, bundle } = signals;
2059
+ const readme = registry?.readme ?? null;
1183
2060
  return {
1184
2061
  weeklyDownloads: downloads?.weeklyDownloads ?? null,
1185
2062
  downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
1186
2063
  stars: github?.stars ?? null,
1187
- dependentsCount: null,
1188
- // not collected in v1 (see spec R1 note)
1189
2064
  firstPublishedAt: registry?.firstPublishedAt ?? null,
1190
2065
  lastReleaseAt: github?.lastReleaseAt ?? registry?.lastReleaseAt ?? null,
1191
2066
  releasesLast12mo: github?.releasesLast12mo ?? null,
@@ -1196,7 +2071,18 @@ function toScoringInput(signals, category) {
1196
2071
  deprecated: registry?.deprecated ?? false,
1197
2072
  archived: github?.archived ?? false,
1198
2073
  bundleMinGzipKb: bundle?.bundleMinGzipKb ?? null,
1199
- category
2074
+ category,
2075
+ // Quality signals — null (not false) when the manifest itself is missing, so
2076
+ // weightedAverage drops the component rather than scoring it 0.
2077
+ hasTypes: registry ? registry.hasTypes : null,
2078
+ hasTestScript: registry ? registry.hasTestScript : null,
2079
+ readmeLength: readme !== null ? readme.length : null,
2080
+ hasExamples: readme !== null ? /```/.test(readme) : null,
2081
+ hasHomepage: registry ? registry.homepage !== null : null,
2082
+ hasReleaseNotes: github?.releasesLast12mo != null ? github.releasesLast12mo > 0 : null,
2083
+ directDependenciesCount: registry?.directDependenciesCount ?? null,
2084
+ license: registry?.license ?? null,
2085
+ hasProvenance: registry ? registry.hasProvenance : null
1200
2086
  };
1201
2087
  }
1202
2088
  function computeMaintenance(input, now) {
@@ -1254,23 +2140,68 @@ function computeEfficiency(bundleMinGzipKb, category, categoryMedian) {
1254
2140
  const ratio = bundleMinGzipKb / categoryMedian;
1255
2141
  return clamp(100 - ratio * EFFICIENCY.medianScore);
1256
2142
  }
1257
- function computeHealthScore(breakdown) {
2143
+ function isRecognizedLicense(license) {
2144
+ if (!license) return false;
2145
+ const id = license.trim().toLowerCase();
2146
+ return RECOGNIZED_LICENSES.some((l) => id === l || id.startsWith(l));
2147
+ }
2148
+ function computeQuality(input) {
2149
+ const components = [];
2150
+ if (input.hasTypes !== null) {
2151
+ components.push({ value: input.hasTypes ? 100 : 0, weight: QUALITY_WEIGHTS.types });
2152
+ }
2153
+ if (input.hasTestScript !== null) {
2154
+ components.push({ value: input.hasTestScript ? 100 : 0, weight: QUALITY_WEIGHTS.tests });
2155
+ }
2156
+ if (input.readmeLength !== null || input.hasHomepage !== null) {
2157
+ const lenScore = Math.min((input.readmeLength ?? 0) / QUALITY.docsFullLengthChars, 1) * 60;
2158
+ const examples = input.hasExamples ? 25 : 0;
2159
+ const homepage = input.hasHomepage ? 15 : 0;
2160
+ components.push({ value: clamp(lenScore + examples + homepage), weight: QUALITY_WEIGHTS.docs });
2161
+ }
2162
+ if (input.hasReleaseNotes !== null) {
2163
+ components.push({ value: input.hasReleaseNotes ? 100 : 0, weight: QUALITY_WEIGHTS.changelog });
2164
+ }
2165
+ if (input.directDependenciesCount !== null) {
2166
+ const deps = input.directDependenciesCount;
2167
+ const depsScore = clamp(
2168
+ 100 - Math.log10(deps + 1) / Math.log10(QUALITY.depsLogMax + 1) * 100
2169
+ );
2170
+ components.push({ value: depsScore, weight: QUALITY_WEIGHTS.deps });
2171
+ }
2172
+ if (input.license !== null) {
2173
+ components.push({
2174
+ value: isRecognizedLicense(input.license) ? 100 : QUALITY.licenseUnrecognizedScore,
2175
+ weight: QUALITY_WEIGHTS.license
2176
+ });
2177
+ }
2178
+ if (input.hasProvenance) {
2179
+ components.push({ value: 100, weight: QUALITY_WEIGHTS.provenance });
2180
+ }
2181
+ if (components.length === 0) return null;
2182
+ return weightedAverage(components);
2183
+ }
2184
+ function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
1258
2185
  const { maintenance, adoption, reliability, efficiency } = breakdown;
2186
+ const w = weights;
1259
2187
  if (efficiency === null) {
1260
- const w = HEALTH_WEIGHTS;
1261
2188
  const denom = w.maintenance + w.adoption + w.reliability;
1262
2189
  return Math.round(
1263
2190
  (w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
1264
2191
  );
1265
2192
  }
1266
2193
  return Math.round(
1267
- HEALTH_WEIGHTS.maintenance * maintenance + HEALTH_WEIGHTS.adoption * adoption + HEALTH_WEIGHTS.reliability * reliability + HEALTH_WEIGHTS.efficiency * efficiency
2194
+ w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability + w.efficiency * efficiency
1268
2195
  );
1269
2196
  }
2197
+ function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
2198
+ if (qualityScore === null) return healthScore;
2199
+ return Math.round((1 - lambda) * healthScore + lambda * qualityScore);
2200
+ }
1270
2201
  function hasCriticalOrHighAdvisory(advisories) {
1271
2202
  return advisories.some((a) => a.severity === "critical" || a.severity === "high");
1272
2203
  }
1273
- function computeConfidence(input, now) {
2204
+ function computeConfidence(input, now, qualityScore = null) {
1274
2205
  const dl = input.weeklyDownloads ?? 0;
1275
2206
  const ageMonths = monthsSince(input.firstPublishedAt, now);
1276
2207
  const lastReleaseMonths = monthsSince(input.lastReleaseAt, now);
@@ -1285,6 +2216,11 @@ function computeConfidence(input, now) {
1285
2216
  if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
1286
2217
  return "emerging";
1287
2218
  }
2219
+ const promisingRecencyMonths = lastReleaseMonths ?? ageMonths;
2220
+ const promisingReleaseOk = promisingRecencyMonths !== null && promisingRecencyMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
2221
+ if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
2222
+ return "promising";
2223
+ }
1288
2224
  return "unproven";
1289
2225
  }
1290
2226
  function weightedAverage(components) {
@@ -1299,7 +2235,7 @@ function median(values) {
1299
2235
  const mid = Math.floor(sorted.length / 2);
1300
2236
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
1301
2237
  }
1302
- var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince;
2238
+ var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince, RECOGNIZED_LICENSES;
1303
2239
  var init_score = __esm({
1304
2240
  "src/scoring/score.ts"() {
1305
2241
  "use strict";
@@ -1311,6 +2247,20 @@ var init_score = __esm({
1311
2247
  clamp = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
1312
2248
  daysSince = (date, now) => date ? (now.getTime() - date.getTime()) / DAY_MS : null;
1313
2249
  monthsSince = (date, now) => date ? (now.getTime() - date.getTime()) / MONTH_MS : null;
2250
+ RECOGNIZED_LICENSES = [
2251
+ "mit",
2252
+ "isc",
2253
+ "apache-2.0",
2254
+ "bsd-2-clause",
2255
+ "bsd-3-clause",
2256
+ "mpl-2.0",
2257
+ "lgpl",
2258
+ "gpl",
2259
+ "agpl",
2260
+ "unlicense",
2261
+ "0bsd",
2262
+ "cc0-1.0"
2263
+ ];
1314
2264
  }
1315
2265
  });
1316
2266
 
@@ -1380,6 +2330,7 @@ var init_embeddings = __esm({
1380
2330
  LocalEmbeddingProvider = class {
1381
2331
  kind = "local";
1382
2332
  dimensions = EMBEDDING_DIM;
2333
+ id = "local";
1383
2334
  async embed(texts) {
1384
2335
  return texts.map((t) => localEmbed(t));
1385
2336
  }
@@ -1390,17 +2341,19 @@ var init_embeddings = __esm({
1390
2341
  this.apiKey = apiKey;
1391
2342
  this.model = model;
1392
2343
  this.fetchImpl = fetchImpl;
2344
+ this.id = `openai:${model}`;
1393
2345
  }
1394
2346
  apiKey;
1395
2347
  model;
1396
2348
  fetchImpl;
1397
2349
  kind = "openai";
1398
2350
  dimensions = EMBEDDING_DIM;
2351
+ id;
1399
2352
  async embed(texts) {
1400
2353
  const out = [];
1401
2354
  for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
1402
2355
  const batch = texts.slice(i, i + OPENAI_BATCH);
1403
- const body = JSON.stringify({ model: this.model, input: batch });
2356
+ const body = JSON.stringify({ model: this.model, input: batch, dimensions: EMBEDDING_DIM });
1404
2357
  const { data } = await httpRequest("https://api.openai.com/v1/embeddings", {
1405
2358
  host: "api.openai.com",
1406
2359
  method: "POST",
@@ -1413,7 +2366,14 @@ var init_embeddings = __esm({
1413
2366
  });
1414
2367
  const vectors = data?.data ?? [];
1415
2368
  vectors.sort((a, b) => a.index - b.index);
1416
- for (const v of vectors) out.push(v.embedding);
2369
+ for (const v of vectors) {
2370
+ if (v.embedding.length !== EMBEDDING_DIM) {
2371
+ throw new Error(
2372
+ `OpenAI model ${this.model} returned ${v.embedding.length}-dim vectors; expected ${EMBEDDING_DIM}. Set EMBEDDING_MODEL to a text-embedding-3 model or update EMBEDDING_DIM.`
2373
+ );
2374
+ }
2375
+ out.push(v.embedding);
2376
+ }
1417
2377
  }
1418
2378
  return out;
1419
2379
  }
@@ -1421,47 +2381,6 @@ var init_embeddings = __esm({
1421
2381
  }
1422
2382
  });
1423
2383
 
1424
- // src/db/packages.ts
1425
- import { eq, sql } from "drizzle-orm";
1426
- async function getSeedTargets(db) {
1427
- const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
1428
- return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
1429
- }
1430
- async function getPackageByName(db, name) {
1431
- const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
1432
- return rows[0] ?? null;
1433
- }
1434
- async function ensureSeedEntry(db, name, category) {
1435
- await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
1436
- }
1437
- async function upsertPackage(db, row) {
1438
- const { name: _name, createdAt: _createdAt, ...mutable } = row;
1439
- await db.insert(packages).values(row).onConflictDoUpdate({
1440
- target: packages.name,
1441
- set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
1442
- });
1443
- }
1444
- async function startSyncRun(db) {
1445
- const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
1446
- return row.id;
1447
- }
1448
- async function finishSyncRun(db, id, data) {
1449
- await db.update(syncRuns).set({
1450
- finishedAt: /* @__PURE__ */ new Date(),
1451
- packagesSeen: data.packagesSeen,
1452
- packagesUpdated: data.packagesUpdated,
1453
- errors: data.errors,
1454
- status: data.status
1455
- }).where(eq(syncRuns.id, id));
1456
- }
1457
- var init_packages = __esm({
1458
- "src/db/packages.ts"() {
1459
- "use strict";
1460
- init_esm_shims();
1461
- init_schema();
1462
- }
1463
- });
1464
-
1465
2384
  // src/core/concurrency.ts
1466
2385
  async function pMap(items, mapper, concurrency) {
1467
2386
  const results = new Array(items.length);
@@ -1508,18 +2427,28 @@ async function runSync(opts = {}) {
1508
2427
  prefetchedWeekly: weeklyMap.has(target.name) ? weeklyMap.get(target.name) : void 0
1509
2428
  });
1510
2429
  for (const e of signals.errors) allErrors.push({ package: target.name, ...e });
1511
- const input = toScoringInput(signals, target.category);
1512
2430
  const summaryInput = await buildSummaryInput(signals, target.category);
1513
- const { summary, usageGuide } = await provider.generate(summaryInput);
2431
+ const { summary, usageGuide, inferredCategory } = await provider.generate(summaryInput);
2432
+ let category = target.category;
2433
+ let categorySource = target.category ? "curated" : null;
2434
+ if (!category) {
2435
+ category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
2436
+ categorySource = category ? "inferred" : null;
2437
+ }
2438
+ const input = toScoringInput(signals, category);
2439
+ const quality = computeQuality(input);
1514
2440
  if (++done % 25 === 0) logger.info(` \u2026${done}/${targets.length}`);
1515
2441
  return {
1516
2442
  target,
2443
+ category,
2444
+ categorySource,
1517
2445
  signals,
1518
2446
  input,
1519
2447
  maintenance: computeMaintenance(input, now),
1520
2448
  adoption: computeAdoption(input),
1521
2449
  reliability: computeReliability(input),
1522
- confidence: computeConfidence(input, now),
2450
+ quality,
2451
+ confidence: computeConfidence(input, now, quality),
1523
2452
  summary,
1524
2453
  usageGuide
1525
2454
  };
@@ -1542,7 +2471,7 @@ async function runSync(opts = {}) {
1542
2471
  ok.map(
1543
2472
  (c) => buildEmbeddingText({
1544
2473
  name: c.target.name,
1545
- category: c.target.category,
2474
+ category: c.category,
1546
2475
  summary: c.summary,
1547
2476
  description: c.signals.registry?.description ?? null
1548
2477
  })
@@ -1553,21 +2482,23 @@ async function runSync(opts = {}) {
1553
2482
  const c = ok[i];
1554
2483
  const efficiency = computeEfficiency(
1555
2484
  c.input.bundleMinGzipKb,
1556
- c.target.category,
1557
- c.target.category ? medians.get(c.target.category) ?? null : null
2485
+ c.category,
2486
+ c.category ? medians.get(c.category) ?? null : null
1558
2487
  );
1559
2488
  const breakdown = {
1560
2489
  maintenance: c.maintenance,
1561
2490
  adoption: c.adoption,
1562
2491
  reliability: c.reliability,
1563
- efficiency
2492
+ efficiency,
2493
+ quality: c.quality
1564
2494
  };
1565
2495
  const healthScore = computeHealthScore(breakdown);
1566
2496
  await upsertPackage(
1567
2497
  handle.db,
1568
2498
  assemblePackageRow({
1569
2499
  name: c.target.name,
1570
- category: c.target.category,
2500
+ category: c.category,
2501
+ categorySource: c.categorySource,
1571
2502
  signals: c.signals,
1572
2503
  input: c.input,
1573
2504
  summary: c.summary,
@@ -1575,7 +2506,9 @@ async function runSync(opts = {}) {
1575
2506
  confidence: c.confidence,
1576
2507
  breakdown,
1577
2508
  healthScore,
2509
+ qualityScore: c.quality,
1578
2510
  embedding: embeddings[i] ?? null,
2511
+ embeddingProvider: embProvider.id,
1579
2512
  now
1580
2513
  })
1581
2514
  );
@@ -1589,6 +2522,7 @@ async function runSync(opts = {}) {
1589
2522
  status
1590
2523
  });
1591
2524
  logger.info(`Sync ${status}: ${updated}/${targets.length} updated, ${allErrors.length} source errors.`);
2525
+ if (updated > 0) await invalidateCache();
1592
2526
  return { seen: targets.length, updated, errors: allErrors.length, status };
1593
2527
  } catch (err) {
1594
2528
  await finishSyncRun(handle.db, runId, {
@@ -1614,7 +2548,7 @@ async function resolveTargets(db, opts) {
1614
2548
  function computeCategoryMedians(computed) {
1615
2549
  const byCategory = /* @__PURE__ */ new Map();
1616
2550
  for (const c of computed) {
1617
- const cat = c.target.category;
2551
+ const cat = c.category;
1618
2552
  const kb = c.input.bundleMinGzipKb;
1619
2553
  if (cat && isFrontendCategory(cat) && kb !== null) {
1620
2554
  const list = byCategory.get(cat) ?? [];
@@ -1635,6 +2569,7 @@ function assemblePackageRow(p) {
1635
2569
  name: p.name,
1636
2570
  ecosystem: "npm",
1637
2571
  category: p.category,
2572
+ categorySource: p.categorySource,
1638
2573
  description: r?.description ?? null,
1639
2574
  summary: p.summary,
1640
2575
  repoUrl: r?.repoUrl ?? null,
@@ -1647,18 +2582,22 @@ function assemblePackageRow(p) {
1647
2582
  lastReleaseAt: p.input.lastReleaseAt,
1648
2583
  weeklyDownloads: p.input.weeklyDownloads,
1649
2584
  downloadGrowth90d: p.input.downloadGrowth90d,
1650
- dependentsCount: p.input.dependentsCount,
1651
2585
  stars: p.input.stars,
1652
2586
  openIssues: p.input.openIssues,
1653
2587
  closedIssues: p.input.closedIssues,
1654
2588
  scorecard: p.input.scorecard,
1655
2589
  bundleMinGzipKb: p.input.bundleMinGzipKb,
1656
2590
  advisories: p.input.advisories,
2591
+ peerDependencies: r?.peerDependencies ?? null,
2592
+ peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
2593
+ engines: r?.engines ?? null,
1657
2594
  healthScore: p.healthScore,
2595
+ qualityScore: p.qualityScore,
1658
2596
  confidence: p.confidence,
1659
2597
  scoreBreakdown: p.breakdown,
1660
2598
  usageGuide: p.usageGuide,
1661
2599
  embedding: p.embedding,
2600
+ embeddingProvider: p.embedding ? p.embeddingProvider : null,
1662
2601
  dataAsOf: p.now
1663
2602
  };
1664
2603
  }
@@ -1666,6 +2605,7 @@ var init_sync = __esm({
1666
2605
  "src/pipeline/sync.ts"() {
1667
2606
  "use strict";
1668
2607
  init_esm_shims();
2608
+ init_cache();
1669
2609
  init_config();
1670
2610
  init_concurrency();
1671
2611
  init_http();
@@ -1673,6 +2613,7 @@ var init_sync = __esm({
1673
2613
  init_collect();
1674
2614
  init_npmDownloads();
1675
2615
  init_summarize();
2616
+ init_categoryInference();
1676
2617
  init_embeddings();
1677
2618
  init_scoring();
1678
2619
  init_client();
@@ -1682,40 +2623,56 @@ var init_sync = __esm({
1682
2623
  });
1683
2624
 
1684
2625
  // src/pipeline/single.ts
1685
- import { and, eq as eq2, isNotNull, sql as sql2 } from "drizzle-orm";
2626
+ import { and as and3, eq as eq3, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
1686
2627
  async function getSeedCategory(db, name) {
1687
- const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq2(seedPackages.name, name)).limit(1);
2628
+ const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq3(seedPackages.name, name)).limit(1);
1688
2629
  return row?.category ?? null;
1689
2630
  }
1690
2631
  async function getCategoryMedianBundle(db, category) {
1691
2632
  const [row] = await db.select({
1692
2633
  m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
1693
- }).from(packages).where(and(eq2(packages.category, category), isNotNull(packages.bundleMinGzipKb)));
2634
+ }).from(packages).where(and3(eq3(packages.category, category), isNotNull2(packages.bundleMinGzipKb)));
1694
2635
  return row?.m ?? null;
1695
2636
  }
1696
2637
  async function syncOnePackage(db, name, opts = {}) {
1697
2638
  const config = getConfig();
1698
2639
  const now = /* @__PURE__ */ new Date();
1699
2640
  const existing = await getPackageByName(db, name);
1700
- const category = opts.category ?? existing?.category ?? await getSeedCategory(db, name) ?? null;
2641
+ const curatedCategory = opts.category ?? await getSeedCategory(db, name) ?? null;
2642
+ const initialCategory = curatedCategory ?? existing?.category ?? null;
1701
2643
  const prefetchedWeekly = await fetchWeeklyDownloads(name).catch(() => null);
1702
- const signals = await collectSignals(name, category, {
2644
+ const signals = await collectSignals(name, initialCategory, {
1703
2645
  githubToken: config.GITHUB_TOKEN,
1704
2646
  prefetchedWeekly
1705
2647
  });
2648
+ const summaryInput = await buildSummaryInput(signals, initialCategory);
2649
+ const { summary, usageGuide, inferredCategory } = await createSummaryProvider().generate(summaryInput);
2650
+ let category;
2651
+ let categorySource;
2652
+ if (curatedCategory) {
2653
+ category = curatedCategory;
2654
+ categorySource = "curated";
2655
+ } else if (existing?.category) {
2656
+ category = existing.category;
2657
+ categorySource = existing.categorySource ?? "inferred";
2658
+ } else {
2659
+ category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
2660
+ categorySource = category ? "inferred" : null;
2661
+ }
1706
2662
  const input = toScoringInput(signals, category);
1707
- const summaryInput = await buildSummaryInput(signals, category);
1708
- const { summary, usageGuide } = await createSummaryProvider().generate(summaryInput);
1709
2663
  const median2 = category ? await getCategoryMedianBundle(db, category) : null;
2664
+ const quality = computeQuality(input);
1710
2665
  const breakdown = {
1711
2666
  maintenance: computeMaintenance(input, now),
1712
2667
  adoption: computeAdoption(input),
1713
2668
  reliability: computeReliability(input),
1714
- efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2)
2669
+ efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
2670
+ quality
1715
2671
  };
1716
2672
  const healthScore = computeHealthScore(breakdown);
1717
- const confidence = computeConfidence(input, now);
1718
- const [embedding] = await createEmbeddingProvider().embed([
2673
+ const confidence = computeConfidence(input, now, quality);
2674
+ const embProvider = createEmbeddingProvider();
2675
+ const [embedding] = await embProvider.embed([
1719
2676
  buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
1720
2677
  ]);
1721
2678
  await upsertPackage(
@@ -1723,6 +2680,7 @@ async function syncOnePackage(db, name, opts = {}) {
1723
2680
  assemblePackageRow({
1724
2681
  name,
1725
2682
  category,
2683
+ categorySource,
1726
2684
  signals,
1727
2685
  input,
1728
2686
  summary,
@@ -1730,10 +2688,16 @@ async function syncOnePackage(db, name, opts = {}) {
1730
2688
  confidence,
1731
2689
  breakdown,
1732
2690
  healthScore,
2691
+ qualityScore: quality,
1733
2692
  embedding: embedding ?? null,
2693
+ embeddingProvider: embProvider.id,
1734
2694
  now
1735
2695
  })
1736
2696
  );
2697
+ await upsertPackageVersions(db, name, signals.registry?.versionTimeline ?? []).catch(
2698
+ () => {
2699
+ }
2700
+ );
1737
2701
  return await getPackageByName(db, name);
1738
2702
  }
1739
2703
  async function getOrFetchPackage(db, name) {
@@ -1742,7 +2706,9 @@ async function getOrFetchPackage(db, name) {
1742
2706
  const exists = await npmPackageExists(name);
1743
2707
  if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
1744
2708
  const row = await syncOnePackage(db, name);
1745
- await ensureSeedEntry(db, name, row.category);
2709
+ if (row.confidence && row.confidence !== "unproven") {
2710
+ await ensureSeedEntry(db, name, row.category);
2711
+ }
1746
2712
  return { row, wasTracked: false, existsOnNpm: true };
1747
2713
  }
1748
2714
  var init_single = __esm({
@@ -1753,6 +2719,7 @@ var init_single = __esm({
1753
2719
  init_collect();
1754
2720
  init_sources();
1755
2721
  init_summarize();
2722
+ init_categoryInference();
1756
2723
  init_scoring();
1757
2724
  init_embeddings();
1758
2725
  init_packages();
@@ -1761,103 +2728,87 @@ var init_single = __esm({
1761
2728
  }
1762
2729
  });
1763
2730
 
1764
- // src/search/categoryInference.ts
1765
- function inferCategory(need) {
1766
- const text2 = need.toLowerCase();
1767
- for (const rule of RULES) {
1768
- if (rule.patterns.test(text2)) return rule.category;
1769
- }
1770
- return null;
1771
- }
1772
- var RULES;
1773
- var init_categoryInference = __esm({
1774
- "src/search/categoryInference.ts"() {
1775
- "use strict";
1776
- init_esm_shims();
1777
- RULES = [
1778
- { category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
1779
- { category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
1780
- { category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
1781
- { category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
1782
- { category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
1783
- { category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
1784
- { category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
1785
- { category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
1786
- { category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
1787
- { category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
1788
- { category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
1789
- { category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
1790
- { category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
1791
- { category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
1792
- { category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
1793
- // date-time precedes linting: "date formatting" must not be captured by linting's
1794
- // bare "formatting" token before the date rule is reached (first-match-wins).
1795
- { category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
1796
- { category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
1797
- { category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
1798
- { category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
1799
- { category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
1800
- { category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
1801
- { category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
1802
- ];
1803
- }
1804
- });
1805
-
1806
2731
  // src/search/recommend.ts
1807
- import { and as and2, cosineDistance, desc, eq as eq3, isNotNull as isNotNull2, lte, sql as sql3 } from "drizzle-orm";
2732
+ import { and as and4, cosineDistance, eq as eq4, isNotNull as isNotNull3, lte, sql as sql3 } from "drizzle-orm";
1808
2733
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
1809
2734
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
1810
2735
  const [queryVec] = await provider.embed([opts.need]);
1811
2736
  if (!queryVec) return [];
1812
2737
  const category = opts.category ?? inferCategory(opts.need);
1813
2738
  const pool = Math.max(limit * 5, 25);
1814
- let rows = await runQuery(db, queryVec, opts.constraints, category, pool);
1815
- if (category && rows.length < limit) {
1816
- const broad = await runQuery(db, queryVec, opts.constraints, null, pool);
1817
- const seen = new Set(rows.map((r) => r.name));
1818
- rows = rows.concat(broad.filter((r) => !seen.has(r.name)));
1819
- }
1820
- if (rows.length === 0) return [];
1821
- const ranked = rows.map((r) => {
1822
- const simNorm = Math.max(0, Math.min(1, (r.similarity + 1) / 2));
1823
- const health = (r.healthScore ?? 0) / 100;
1824
- return { row: r, score: SIM_WEIGHT * simNorm + HEALTH_WEIGHT * health };
2739
+ let fused = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, category, pool);
2740
+ if (category && fused.length < limit) {
2741
+ const broad = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, null, pool);
2742
+ const seen = new Set(fused.map((f) => f.row.name));
2743
+ fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
2744
+ }
2745
+ if (fused.length === 0) return [];
2746
+ const lambda = loadWeights().composite.lambda;
2747
+ const maxRrf = Math.max(...fused.map((f) => f.rrf));
2748
+ const ranked = fused.map((f) => {
2749
+ const relevance = maxRrf > 0 ? f.rrf / maxRrf : 0;
2750
+ const composite = computeComposite(f.row.healthScore ?? 0, f.row.qualityScore, lambda) / 100;
2751
+ return { row: f.row, score: RELEVANCE_WEIGHT * relevance + COMPOSITE_WEIGHT * composite };
1825
2752
  }).sort((a, b) => b.score - a.score).slice(0, limit);
1826
2753
  return ranked.map(({ row }) => toCandidate(row));
1827
2754
  }
1828
- async function runQuery(db, queryVec, constraints, category, pool) {
1829
- const similarity = sql3`1 - (${cosineDistance(packages.embedding, queryVec)})`;
1830
- const conditions = [isNotNull2(packages.embedding)];
1831
- if (category) conditions.push(eq3(packages.category, category));
1832
- if (constraints?.license) conditions.push(eq3(packages.license, constraints.license));
2755
+ async function hybridSearch(db, queryVec, providerId, need, constraints, category, pool) {
2756
+ const [vectorRows, lexicalRows] = await Promise.all([
2757
+ runVectorQuery(db, queryVec, providerId, constraints, category, pool),
2758
+ runLexicalQuery(db, need, constraints, category, pool)
2759
+ ]);
2760
+ return rrfFuse([vectorRows, lexicalRows]);
2761
+ }
2762
+ function rrfFuse(lists, k = RRF_K) {
2763
+ const fused = /* @__PURE__ */ new Map();
2764
+ for (const list of lists) {
2765
+ list.forEach((row, idx) => {
2766
+ const contribution = 1 / (k + idx + 1);
2767
+ const prev = fused.get(row.name);
2768
+ if (prev) prev.rrf += contribution;
2769
+ else fused.set(row.name, { row, rrf: contribution });
2770
+ });
2771
+ }
2772
+ return [...fused.values()].sort((a, b) => b.rrf - a.rrf);
2773
+ }
2774
+ function buildConditions(constraints, category) {
2775
+ const conditions = [];
2776
+ if (category) conditions.push(eq4(packages.category, category));
2777
+ if (constraints?.license) conditions.push(eq4(packages.license, constraints.license));
1833
2778
  if (constraints?.maxBundleKb !== void 0) {
1834
2779
  conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
1835
2780
  }
1836
2781
  if (constraints?.minConfidence) {
1837
- const allowed = ["proven", "emerging", "unproven"].filter(
2782
+ const allowed = ["proven", "emerging", "promising", "unproven"].filter(
1838
2783
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
1839
2784
  );
1840
2785
  conditions.push(
1841
2786
  sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
1842
2787
  );
1843
2788
  }
1844
- return db.select({
1845
- name: packages.name,
1846
- category: packages.category,
1847
- healthScore: packages.healthScore,
1848
- confidence: packages.confidence,
1849
- latestVersion: packages.latestVersion,
1850
- weeklyDownloads: packages.weeklyDownloads,
1851
- lastReleaseAt: packages.lastReleaseAt,
1852
- repoUrl: packages.repoUrl,
1853
- similarity
1854
- }).from(packages).where(and2(...conditions)).orderBy(desc(similarity)).limit(pool);
2789
+ return conditions;
2790
+ }
2791
+ async function runVectorQuery(db, queryVec, providerId, constraints, category, pool) {
2792
+ const distance = cosineDistance(packages.embedding, queryVec);
2793
+ const conditions = [
2794
+ isNotNull3(packages.embedding),
2795
+ eq4(packages.embeddingProvider, providerId),
2796
+ ...buildConditions(constraints, category)
2797
+ ];
2798
+ return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(distance).limit(pool);
2799
+ }
2800
+ async function runLexicalQuery(db, need, constraints, category, pool) {
2801
+ const tsquery = sql3`websearch_to_tsquery('english', ${need})`;
2802
+ const rank = sql3`ts_rank(${packages.searchVector}, ${tsquery})`;
2803
+ const conditions = [sql3`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
2804
+ return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(sql3`${rank} desc`).limit(pool);
1855
2805
  }
1856
2806
  function toCandidate(row) {
1857
2807
  return {
1858
2808
  name: row.name,
1859
2809
  category: row.category,
1860
2810
  healthScore: row.healthScore ?? 0,
2811
+ qualityScore: row.qualityScore,
1861
2812
  confidence: row.confidence ?? "unproven",
1862
2813
  why: buildWhy(row),
1863
2814
  latestVersion: row.latestVersion,
@@ -1878,26 +2829,132 @@ function formatDownloads(n) {
1878
2829
  if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
1879
2830
  return String(n);
1880
2831
  }
1881
- var CONFIDENCE_RANK, SIM_WEIGHT, HEALTH_WEIGHT;
2832
+ var CONFIDENCE_RANK, RELEVANCE_WEIGHT, COMPOSITE_WEIGHT, RRF_K, ROW_COLUMNS;
1882
2833
  var init_recommend = __esm({
1883
2834
  "src/search/recommend.ts"() {
1884
2835
  "use strict";
1885
2836
  init_esm_shims();
1886
2837
  init_schema();
2838
+ init_scoring();
2839
+ init_weights();
1887
2840
  init_categoryInference();
1888
2841
  init_embeddings();
1889
- CONFIDENCE_RANK = { unproven: 0, emerging: 1, proven: 2 };
1890
- SIM_WEIGHT = 0.6;
1891
- HEALTH_WEIGHT = 0.4;
2842
+ CONFIDENCE_RANK = {
2843
+ unproven: 0,
2844
+ promising: 1,
2845
+ emerging: 2,
2846
+ proven: 3
2847
+ };
2848
+ RELEVANCE_WEIGHT = 0.6;
2849
+ COMPOSITE_WEIGHT = 0.4;
2850
+ RRF_K = 60;
2851
+ ROW_COLUMNS = {
2852
+ name: packages.name,
2853
+ category: packages.category,
2854
+ healthScore: packages.healthScore,
2855
+ qualityScore: packages.qualityScore,
2856
+ confidence: packages.confidence,
2857
+ latestVersion: packages.latestVersion,
2858
+ weeklyDownloads: packages.weeklyDownloads,
2859
+ lastReleaseAt: packages.lastReleaseAt,
2860
+ repoUrl: packages.repoUrl
2861
+ };
1892
2862
  }
1893
2863
  });
1894
2864
 
1895
- // src/mcp/handlers.ts
2865
+ // src/security/risk.ts
2866
+ function assessRisk(i) {
2867
+ const malwarePattern = i.installScripts && i.brandNew && i.lowTrust;
2868
+ if (i.typosquat || i.hasCriticalOrHighAdvisory || malwarePattern) return "high";
2869
+ if (i.deprecatedOrArchived || i.installScripts && (i.lowTrust || i.brandNew) || i.lowTrust && i.flags.includes("single-maintainer")) {
2870
+ return "medium";
2871
+ }
2872
+ return "low";
2873
+ }
2874
+ var init_risk = __esm({
2875
+ "src/security/risk.ts"() {
2876
+ "use strict";
2877
+ init_esm_shims();
2878
+ }
2879
+ });
2880
+
2881
+ // src/security/typosquat.ts
2882
+ function bareName(name) {
2883
+ const slash = name.indexOf("/");
2884
+ return (slash >= 0 ? name.slice(slash + 1) : name).toLowerCase();
2885
+ }
2886
+ function editDistance(a, b) {
2887
+ const m = a.length;
2888
+ const n = b.length;
2889
+ if (m === 0) return n;
2890
+ if (n === 0) return m;
2891
+ let prevPrev = new Array(n + 1).fill(0);
2892
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
2893
+ for (let i = 1; i <= m; i++) {
2894
+ const cur = new Array(n + 1).fill(0);
2895
+ cur[0] = i;
2896
+ for (let j = 1; j <= n; j++) {
2897
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2898
+ let val = Math.min(
2899
+ (prev[j] ?? 0) + 1,
2900
+ (cur[j - 1] ?? 0) + 1,
2901
+ (prev[j - 1] ?? 0) + cost
2902
+ );
2903
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
2904
+ val = Math.min(val, (prevPrev[j - 2] ?? 0) + 1);
2905
+ }
2906
+ cur[j] = val;
2907
+ }
2908
+ prevPrev = prev;
2909
+ prev = cur;
2910
+ }
2911
+ return prev[n] ?? 0;
2912
+ }
2913
+ function detectTyposquat(name, popular, maxDistance = 2) {
2914
+ const target = bareName(name);
2915
+ if (target.length < 4) return null;
2916
+ if (popular.some((p) => p.toLowerCase() === name.toLowerCase())) return null;
2917
+ let best = null;
2918
+ for (const p of popular) {
2919
+ const cand = bareName(p);
2920
+ if (cand === target) continue;
2921
+ if (Math.abs(cand.length - target.length) > maxDistance) continue;
2922
+ const dist = editDistance(target, cand);
2923
+ if (dist >= 1 && dist <= maxDistance && (!best || dist < best.distance)) {
2924
+ best = { target: p, distance: dist };
2925
+ if (dist === 1) break;
2926
+ }
2927
+ }
2928
+ return best;
2929
+ }
2930
+ var init_typosquat = __esm({
2931
+ "src/security/typosquat.ts"() {
2932
+ "use strict";
2933
+ init_esm_shims();
2934
+ }
2935
+ });
2936
+
2937
+ // src/mcp/handlers.ts
2938
+ var handlers_exports = {};
2939
+ __export(handlers_exports, {
2940
+ handleCompare: () => handleCompare,
2941
+ handleCompat: () => handleCompat,
2942
+ handleEvaluate: () => handleEvaluate,
2943
+ handleRecommend: () => handleRecommend,
2944
+ handleVerify: () => handleVerify,
2945
+ latestDataAsOf: () => latestDataAsOf,
2946
+ rowToEvaluate: () => rowToEvaluate
2947
+ });
2948
+ import { createHash as createHash3 } from "crypto";
1896
2949
  import { sql as sql4 } from "drizzle-orm";
1897
2950
  function isStale(dataAsOf) {
1898
2951
  if (!dataAsOf) return true;
1899
2952
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
1900
2953
  }
2954
+ function refreshStale(out) {
2955
+ out.stale = isStale(out.dataAsOf ? new Date(out.dataAsOf) : null) || void 0;
2956
+ return out;
2957
+ }
1901
2958
  function withinDays(date, days) {
1902
2959
  return date ? Date.now() - date.getTime() <= days * DAY_MS2 : false;
1903
2960
  }
@@ -1910,7 +2967,8 @@ function rowToEvaluate(row) {
1910
2967
  maintenance: 0,
1911
2968
  adoption: 0,
1912
2969
  reliability: 0,
1913
- efficiency: null
2970
+ efficiency: null,
2971
+ quality: null
1914
2972
  };
1915
2973
  return {
1916
2974
  dataAsOf: (row.dataAsOf ?? /* @__PURE__ */ new Date()).toISOString(),
@@ -1918,13 +2976,13 @@ function rowToEvaluate(row) {
1918
2976
  name: row.name,
1919
2977
  category: row.category,
1920
2978
  healthScore: row.healthScore ?? 0,
2979
+ qualityScore: row.qualityScore ?? null,
1921
2980
  confidence: row.confidence ?? "unproven",
1922
2981
  scoreBreakdown: breakdown,
1923
2982
  latestVersion: row.latestVersion,
1924
2983
  lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
1925
2984
  weeklyDownloads: row.weeklyDownloads,
1926
2985
  downloadGrowth90d: row.downloadGrowth90d,
1927
- dependentsCount: row.dependentsCount,
1928
2986
  scorecard: row.scorecard,
1929
2987
  bundleMinGzipKb: row.bundleMinGzipKb,
1930
2988
  deprecated: row.deprecated,
@@ -1939,37 +2997,80 @@ async function latestDataAsOf(db) {
1939
2997
  const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
1940
2998
  return new Date(row?.m ?? Date.now()).toISOString();
1941
2999
  }
3000
+ function cacheKey(parts) {
3001
+ return createHash3("sha1").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
3002
+ }
1942
3003
  async function handleRecommend(db, input) {
1943
- const candidates = await recommend(db, {
1944
- need: input.need,
1945
- category: input.category,
1946
- constraints: input.constraints,
1947
- limit: 5
1948
- });
1949
- return { dataAsOf: await latestDataAsOf(db), candidates };
3004
+ return cached2(
3005
+ "rec",
3006
+ cacheKey([input.need, input.category ?? null, input.constraints ?? null]),
3007
+ async () => {
3008
+ const candidates = await recommend(db, {
3009
+ need: input.need,
3010
+ category: input.category,
3011
+ constraints: input.constraints,
3012
+ limit: 5
3013
+ });
3014
+ return { dataAsOf: await latestDataAsOf(db), candidates };
3015
+ },
3016
+ // Don't cache empty results — the index may still be populating.
3017
+ { skipCache: (r) => r.candidates.length === 0 }
3018
+ );
1950
3019
  }
1951
3020
  async function handleEvaluate(db, input) {
1952
- const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
1953
- if (!row) {
1954
- return {
1955
- tracked: false,
1956
- suggestion: existsOnNpm ? `"${input.package}" exists on npm but could not be scored right now; try again.` : `"${input.package}" was not found on the npm registry. Check the package name.`
1957
- };
1958
- }
1959
- return rowToEvaluate(row);
3021
+ const out = await cached2(
3022
+ "eval",
3023
+ cacheKey([input.package]),
3024
+ async () => {
3025
+ const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
3026
+ if (!row) {
3027
+ return {
3028
+ tracked: false,
3029
+ suggestion: existsOnNpm ? `"${input.package}" exists on npm but could not be scored right now; try again.` : `"${input.package}" was not found on the npm registry. Check the package name.`
3030
+ };
3031
+ }
3032
+ const evaluated = rowToEvaluate(row);
3033
+ const verification = await getLatestVerificationByName(db, row.name);
3034
+ return verification ? { ...evaluated, buildVerified: toBuildVerified(verification) } : evaluated;
3035
+ },
3036
+ // Don't cache "not found / not scored yet" — it may resolve on a later fetch.
3037
+ { skipCache: (r) => "tracked" in r }
3038
+ );
3039
+ return "tracked" in out ? out : refreshStale(out);
1960
3040
  }
1961
3041
  async function handleCompare(db, input) {
1962
- const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
1963
- const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
1964
- const missing = input.packages.filter(
1965
- (name) => !rows.some((r) => r.name === name)
3042
+ const out = await cached2(
3043
+ "cmp",
3044
+ cacheKey(input.packages),
3045
+ async () => {
3046
+ const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
3047
+ const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
3048
+ const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
3049
+ return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
3050
+ },
3051
+ // Don't cache a transient miss (a package that momentarily failed to fetch).
3052
+ { skipCache: (r) => Boolean(r.missing?.length) }
1966
3053
  );
1967
- return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
3054
+ out.rows = out.rows.map(refreshStale);
3055
+ return out;
3056
+ }
3057
+ function toBuildVerified(v) {
3058
+ return {
3059
+ version: v.version,
3060
+ installed: v.installed,
3061
+ loaded: v.imported,
3062
+ driver: v.driver,
3063
+ ranAt: v.ranAt ? v.ranAt.toISOString() : ""
3064
+ };
3065
+ }
3066
+ async function handleCompat(db, input) {
3067
+ return checkCompat(db, input.packages);
1968
3068
  }
1969
3069
  async function handleVerify(db, input) {
1970
3070
  const name = input.package;
1971
3071
  const exists = await npmPackageExists(name);
1972
3072
  if (!exists) {
3073
+ const typo2 = detectTyposquat(name, await getTopPackageNames(db).catch(() => []));
1973
3074
  return {
1974
3075
  exists: false,
1975
3076
  tracked: false,
@@ -1977,25 +3078,46 @@ async function handleVerify(db, input) {
1977
3078
  archived: false,
1978
3079
  latestVersion: null,
1979
3080
  weeklyDownloads: null,
1980
- riskFlags: ["not-found-on-registry"],
3081
+ riskFlags: typo2 ? ["not-found-on-registry", `possible-typosquat-of:${typo2.target}`] : ["not-found-on-registry"],
3082
+ risk: "high",
3083
+ typosquatOf: typo2?.target ?? null,
1981
3084
  confidence: null,
1982
3085
  advisoryCount: 0
1983
3086
  };
1984
3087
  }
1985
- const registry = await fetchNpmRegistry(name).catch(() => null);
1986
- const { row, wasTracked } = await getOrFetchPackage(db, name);
3088
+ const [registry, { row, wasTracked }, popular] = await Promise.all([
3089
+ fetchNpmRegistry(name).catch(() => null),
3090
+ getOrFetchPackage(db, name),
3091
+ getTopPackageNames(db).catch(() => [])
3092
+ ]);
1987
3093
  const weeklyDownloads = row?.weeklyDownloads ?? null;
1988
- const advisoryCount = row?.advisories?.length ?? 0;
3094
+ const advisories = row?.advisories ?? [];
3095
+ const advisoryCount = advisories.length;
1989
3096
  const deprecated = Boolean(row?.deprecated || registry?.deprecated);
1990
3097
  const archived = Boolean(row?.archived);
3098
+ const brandNew = withinDays(registry?.firstPublishedAt ?? null, 7);
3099
+ const lowTrust = weeklyDownloads === null || weeklyDownloads < 1e3;
3100
+ const installScripts = registry?.hasInstallScripts ?? false;
3101
+ const typo = detectTyposquat(name, popular);
1991
3102
  const riskFlags = [];
3103
+ if (typo) riskFlags.push(`possible-typosquat-of:${typo.target}`);
1992
3104
  if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
1993
3105
  else if (weeklyDownloads < 1e3) riskFlags.push("low-downloads");
1994
- if (withinDays(registry?.firstPublishedAt ?? null, 7)) riskFlags.push("published-within-7-days");
3106
+ if (brandNew) riskFlags.push("published-within-7-days");
1995
3107
  if (registry?.maintainersCount === 1) riskFlags.push("single-maintainer");
3108
+ if (installScripts) riskFlags.push("runs-install-scripts");
1996
3109
  if (advisoryCount > 0) riskFlags.push("has-known-advisory");
1997
3110
  if (deprecated) riskFlags.push("deprecated");
1998
3111
  if (archived) riskFlags.push("archived");
3112
+ const risk = assessRisk({
3113
+ flags: riskFlags,
3114
+ hasCriticalOrHighAdvisory: hasCriticalOrHighAdvisory(advisories),
3115
+ typosquat: Boolean(typo),
3116
+ installScripts,
3117
+ brandNew,
3118
+ lowTrust,
3119
+ deprecatedOrArchived: deprecated || archived
3120
+ });
1999
3121
  return {
2000
3122
  exists: true,
2001
3123
  tracked: wasTracked,
@@ -2004,6 +3126,8 @@ async function handleVerify(db, input) {
2004
3126
  latestVersion: registry?.latestVersion ?? row?.latestVersion ?? null,
2005
3127
  weeklyDownloads,
2006
3128
  riskFlags,
3129
+ risk,
3130
+ typosquatOf: typo?.target ?? null,
2007
3131
  confidence: row?.confidence ?? null,
2008
3132
  advisoryCount
2009
3133
  };
@@ -2013,12 +3137,19 @@ var init_handlers = __esm({
2013
3137
  "src/mcp/handlers.ts"() {
2014
3138
  "use strict";
2015
3139
  init_esm_shims();
3140
+ init_cache();
2016
3141
  init_constants();
3142
+ init_check();
3143
+ init_packages();
3144
+ init_verification();
2017
3145
  init_schema();
2018
3146
  init_sources();
2019
3147
  init_summarize();
2020
3148
  init_single();
3149
+ init_score();
2021
3150
  init_recommend();
3151
+ init_risk();
3152
+ init_typosquat();
2022
3153
  SEVERITY_RANK = {
2023
3154
  critical: 4,
2024
3155
  high: 3,
@@ -2031,7 +3162,7 @@ var init_handlers = __esm({
2031
3162
  });
2032
3163
 
2033
3164
  // src/mcp/diagram.ts
2034
- import { inArray } from "drizzle-orm";
3165
+ import { inArray as inArray2 } from "drizzle-orm";
2035
3166
  function layerFor(item) {
2036
3167
  if (!item.category) return UNCLASSIFIED;
2037
3168
  if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
@@ -2084,7 +3215,7 @@ async function handleDiagram(db, input) {
2084
3215
  note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
2085
3216
  };
2086
3217
  }
2087
- const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray(packages.name, input.stack));
3218
+ const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray2(packages.name, input.stack));
2088
3219
  const known = new Map(rows.map((r) => [r.name, r.category]));
2089
3220
  const items = input.stack.map((name) => ({
2090
3221
  label: name,
@@ -2138,10 +3269,407 @@ var init_diagram = __esm({
2138
3269
  }
2139
3270
  });
2140
3271
 
3272
+ // src/compat/optimize.ts
3273
+ function conflictsFor(members, sandboxConflicts) {
3274
+ const out = resolveArchitectureCompat(members);
3275
+ for (let i = 0; i < members.length; i++) {
3276
+ for (let j = i + 1; j < members.length; j++) {
3277
+ const a = members[i].name;
3278
+ const b = members[j].name;
3279
+ const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
3280
+ if (sandboxConflicts.has(key)) {
3281
+ out.push({
3282
+ source: "sandbox",
3283
+ packages: [a, b],
3284
+ detail: `${a} and ${b} are recorded as incompatible (sandbox)`
3285
+ });
3286
+ }
3287
+ }
3288
+ }
3289
+ return out;
3290
+ }
3291
+ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
3292
+ if (slots.some((s) => s.length === 0)) {
3293
+ throw new Error("optimizeStack: every slot must have at least one candidate");
3294
+ }
3295
+ const n = slots.length;
3296
+ const budget = 5e4;
3297
+ let nodes = 0;
3298
+ let bestSelection = new Array(n).fill(0);
3299
+ let bestRegret = conflictsFor(
3300
+ slots.map((c) => c[0]).filter((m) => Boolean(m)),
3301
+ sandboxConflicts
3302
+ ).length === 0 ? 0 : Infinity;
3303
+ const chosen = new Array(n).fill(0);
3304
+ const dfs = (slot, regret) => {
3305
+ if (nodes++ > budget || regret >= bestRegret) return;
3306
+ if (slot === n) {
3307
+ bestSelection = chosen.slice();
3308
+ bestRegret = regret;
3309
+ return;
3310
+ }
3311
+ const candidates = slots[slot];
3312
+ for (let i = 0; i < candidates.length; i++) {
3313
+ chosen[slot] = i;
3314
+ const assigned = chosen.slice(0, slot + 1).map((idx, s) => slots[s][idx]);
3315
+ if (conflictsFor(assigned, sandboxConflicts).length === 0) {
3316
+ dfs(slot + 1, regret + i);
3317
+ }
3318
+ if (nodes > budget) return;
3319
+ }
3320
+ };
3321
+ dfs(0, 0);
3322
+ const members = bestSelection.map((idx, s) => slots[s][idx]);
3323
+ return {
3324
+ selection: bestSelection,
3325
+ conflicts: conflictsFor(members, sandboxConflicts),
3326
+ regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret
3327
+ };
3328
+ }
3329
+ var init_optimize = __esm({
3330
+ "src/compat/optimize.ts"() {
3331
+ "use strict";
3332
+ init_esm_shims();
3333
+ init_peerCompat();
3334
+ }
3335
+ });
3336
+
3337
+ // src/mcp/plan.ts
3338
+ var plan_exports = {};
3339
+ __export(plan_exports, {
3340
+ decomposeHeuristic: () => decomposeHeuristic,
3341
+ familyOf: () => familyOf,
3342
+ flagSlotConflicts: () => flagSlotConflicts,
3343
+ handlePlan: () => handlePlan,
3344
+ orderCandidates: () => orderCandidates,
3345
+ resolvePins: () => resolvePins
3346
+ });
3347
+ import { createHash as createHash4 } from "crypto";
3348
+ import { inArray as inArray3 } from "drizzle-orm";
3349
+ function packageToCandidate(row) {
3350
+ return {
3351
+ name: row.name,
3352
+ category: row.category,
3353
+ healthScore: row.healthScore ?? 0,
3354
+ qualityScore: row.qualityScore,
3355
+ confidence: row.confidence ?? "unproven",
3356
+ why: "pinned by you",
3357
+ latestVersion: row.latestVersion,
3358
+ weeklyDownloads: row.weeklyDownloads,
3359
+ lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
3360
+ repoUrl: row.repoUrl
3361
+ };
3362
+ }
3363
+ async function resolvePins(db, using, recommendedNames) {
3364
+ const slots = [];
3365
+ const unresolved = [];
3366
+ for (const name of new Set(using ?? [])) {
3367
+ if (recommendedNames.has(name)) continue;
3368
+ const { row } = await getOrFetchPackage(db, name);
3369
+ if (!row) {
3370
+ unresolved.push(name);
3371
+ continue;
3372
+ }
3373
+ slots.push({
3374
+ need: `using ${name}`,
3375
+ category: row.category,
3376
+ layer: layerFor({ label: name, category: row.category }),
3377
+ recommended: packageToCandidate(row),
3378
+ alternatives: [],
3379
+ // fixed: the user chose this, so it never gets swapped
3380
+ note: "pinned by you"
3381
+ });
3382
+ }
3383
+ return { slots, unresolved };
3384
+ }
3385
+ async function handlePlan(db, input) {
3386
+ const optimize = input.optimize ?? "balanced";
3387
+ const decomposed = input.needs?.length ? { needs: dedupeNeeds(input.needs), source: "needs" } : input.document?.trim() ? await decompose(input.document) : null;
3388
+ const hasPins = Boolean(input.using?.length);
3389
+ if ((!decomposed || decomposed.needs.length === 0) && !hasPins) {
3390
+ return {
3391
+ note: "Provide a `document` (a detailed description of your program), a `needs` array, or a `using` list of packages you have already chosen. lurq recommends evidence-scored packages per component \u2014 it does not invent an architecture from a bare prompt."
3392
+ };
3393
+ }
3394
+ const needs = (decomposed?.needs ?? []).slice(0, MAX_SLOTS);
3395
+ const source = decomposed?.source ?? "needs";
3396
+ const safeRecommend = (need, category) => recommend(db, { need, category, limit: PER_SLOT }).catch((err) => {
3397
+ logger.warn(`plan: recommend failed for "${need}": ${err.message}`);
3398
+ return [];
3399
+ });
3400
+ const effCat = (n) => n.category ?? inferCategory(n.need) ?? void 0;
3401
+ const anchorFamily = familyOf(`${input.document ?? ""} ${needs.map((n) => n.need).join(" ")}`);
3402
+ const metaIdx = needs.findIndex((n) => effCat(n) === "meta-framework");
3403
+ const anchorIdx = metaIdx >= 0 ? metaIdx : needs.findIndex((n) => effCat(n) === "framework");
3404
+ const recs = new Array(needs.length);
3405
+ let framework = anchorFamily;
3406
+ if (anchorIdx >= 0) {
3407
+ recs[anchorIdx] = await safeRecommend(needs[anchorIdx].need, effCat(needs[anchorIdx]));
3408
+ framework = recs[anchorIdx][0]?.name ?? anchorFamily;
3409
+ }
3410
+ const [, dataAsOf] = await Promise.all([
3411
+ Promise.all(
3412
+ needs.map(async (n, i) => {
3413
+ if (i === anchorIdx) return;
3414
+ const need = framework ? `${n.need} (for a ${framework} app)` : n.need;
3415
+ recs[i] = await safeRecommend(need, effCat(n));
3416
+ })
3417
+ ),
3418
+ latestDataAsOf(db)
3419
+ ]);
3420
+ const bundleByName = optimize === "speed" ? await bundleSizes(db, recs.flat()) : /* @__PURE__ */ new Map();
3421
+ const recSlots = needs.map((n, i) => {
3422
+ const candidates = orderCandidates(recs[i], anchorFamily, optimize, bundleByName);
3423
+ const recommended = candidates[0] ?? null;
3424
+ const category = n.category ?? recommended?.category ?? inferCategory(n.need);
3425
+ return {
3426
+ need: n.need,
3427
+ category,
3428
+ layer: layerFor({ label: recommended?.name ?? n.need, category }),
3429
+ recommended,
3430
+ alternatives: candidates.slice(1),
3431
+ note: recommended ? void 0 : "no tracked package matched this need yet"
3432
+ };
3433
+ });
3434
+ const recommendedNames = new Set(
3435
+ recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
3436
+ );
3437
+ const { slots: pinnedSlots, unresolved: unresolvedPins } = await resolvePins(
3438
+ db,
3439
+ input.using,
3440
+ recommendedNames
3441
+ );
3442
+ const slots = [...pinnedSlots, ...recSlots];
3443
+ const compatibility = await resolveCompat(db, slots);
3444
+ const unmatched = [
3445
+ ...slots.filter((s) => !s.recommended).map((s) => s.need),
3446
+ ...unresolvedPins.map((n) => `${n} (pinned, but not found on npm)`)
3447
+ ];
3448
+ const mermaid = buildMermaid(
3449
+ slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
3450
+ );
3451
+ return {
3452
+ dataAsOf,
3453
+ optimize,
3454
+ source,
3455
+ framework,
3456
+ slots,
3457
+ unmatched,
3458
+ mermaid,
3459
+ note: planNote(source, unmatched.length, optimize, framework),
3460
+ compatibility
3461
+ };
3462
+ }
3463
+ function flagSlotConflicts(picks, conflicts) {
3464
+ const out = /* @__PURE__ */ new Map();
3465
+ for (const c of conflicts) {
3466
+ for (const p of picks) {
3467
+ if (p.name && c.packages.includes(p.name)) {
3468
+ const others = c.packages.filter((n) => n !== p.name);
3469
+ out.set(p.need, [.../* @__PURE__ */ new Set([...out.get(p.need) ?? [], ...others])]);
3470
+ }
3471
+ }
3472
+ }
3473
+ return out;
3474
+ }
3475
+ async function resolveCompat(db, slots) {
3476
+ const eligible = slots.filter((s) => s.recommended);
3477
+ if (eligible.length < 2) return null;
3478
+ try {
3479
+ const allNames = [
3480
+ ...new Set(eligible.flatMap((s) => [s.recommended, ...s.alternatives].map((c) => c.name)))
3481
+ ];
3482
+ const [{ members }, edges] = await Promise.all([
3483
+ assembleMembers(db, allNames),
3484
+ getCompatEdges(db, allNames)
3485
+ ]);
3486
+ const metaByName = new Map(members.map((m) => [m.name, m]));
3487
+ const sandboxConflicts = new Set(
3488
+ edges.filter((e) => e.status === "conflict").map((e) => `${e.packageA}|${e.packageB}`)
3489
+ );
3490
+ const slotCandidates = eligible.map(
3491
+ (s) => [s.recommended, ...s.alternatives].map((c) => metaByName.get(c.name) ?? NO_META(c))
3492
+ );
3493
+ const { selection } = optimizeStack(slotCandidates, sandboxConflicts);
3494
+ eligible.forEach((s, i) => {
3495
+ const idx = selection[i] ?? 0;
3496
+ if (idx <= 0) return;
3497
+ const all = [s.recommended, ...s.alternatives];
3498
+ const chosen = all[idx];
3499
+ if (!chosen) return;
3500
+ s.recommended = chosen;
3501
+ s.alternatives = all.filter((c) => c.name !== chosen.name);
3502
+ s.swappedFrom = all[0].name;
3503
+ });
3504
+ } catch (err) {
3505
+ logger.warn(`plan: compat optimization failed: ${String(err)}`);
3506
+ }
3507
+ const compat = await checkCompat(
3508
+ db,
3509
+ eligible.map((s) => s.recommended.name)
3510
+ ).catch(() => null);
3511
+ if (compat) {
3512
+ const flags = flagSlotConflicts(
3513
+ slots.map((s) => ({ need: s.need, name: s.recommended?.name ?? null })),
3514
+ compat.conflicts
3515
+ );
3516
+ for (const s of slots) {
3517
+ const cw = flags.get(s.need);
3518
+ s.conflictsWith = cw?.length ? cw : void 0;
3519
+ }
3520
+ }
3521
+ return compat;
3522
+ }
3523
+ function planNote(source, unmatched, optimize, framework) {
3524
+ const base = source === "heuristic" ? "Components were extracted from your document with a keyword heuristic (no summary LLM configured) \u2014 coarse; pass a `needs` array or set SUMMARY_API_KEY for sharper decomposition." : source === "llm" ? "Components were extracted from your document by the summary model." : "Components taken from the supplied needs.";
3525
+ const grounding = " Each package is recommended from lurq\u2019s scored index \u2014 a labeled, evidence-backed starting point, not a validated architecture.";
3526
+ const ctx = framework ? ` Anchored to the ${framework} ecosystem so sibling libraries stay coherent across the stack.` : "";
3527
+ const tail = unmatched ? ` ${unmatched} need(s) had no tracked match (listed in \`unmatched\`).` : "";
3528
+ const opt = optimize === "speed" ? " Ranking favored the lightest-bundle option per slot." : "";
3529
+ return base + grounding + ctx + opt + tail;
3530
+ }
3531
+ function dedupeNeeds(needs) {
3532
+ const seen = /* @__PURE__ */ new Map();
3533
+ for (const n of needs) {
3534
+ const key = n.need.trim().toLowerCase();
3535
+ if (!key) continue;
3536
+ if (!seen.has(key)) seen.set(key, { need: n.need.trim(), category: n.category });
3537
+ }
3538
+ return [...seen.values()];
3539
+ }
3540
+ function familyOf(name) {
3541
+ const n = name.toLowerCase();
3542
+ for (const f of FAMILY_TOKENS) if (f.re.test(n)) return f.family;
3543
+ return null;
3544
+ }
3545
+ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
3546
+ const coherence = (c) => {
3547
+ const fam = familyOf(c.name);
3548
+ if (!fam) return 0;
3549
+ return fam === anchorFamily ? 1 : -1;
3550
+ };
3551
+ return [...cands].sort((a, b) => {
3552
+ if (anchorFamily) {
3553
+ const d = coherence(b) - coherence(a);
3554
+ if (d) return d;
3555
+ }
3556
+ if (optimize === "speed") {
3557
+ return (bundleByName.get(a.name) ?? Infinity) - (bundleByName.get(b.name) ?? Infinity);
3558
+ }
3559
+ return 0;
3560
+ });
3561
+ }
3562
+ async function bundleSizes(db, candidates) {
3563
+ const names = [...new Set(candidates.map((c) => c.name))];
3564
+ if (names.length === 0) return /* @__PURE__ */ new Map();
3565
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray3(packages.name, names));
3566
+ return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
3567
+ }
3568
+ async function decompose(document) {
3569
+ const config = getConfig();
3570
+ if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
3571
+ const llm = await decomposeWithLlm(document, config.SUMMARY_API_KEY, config.SUMMARY_MODEL).catch(
3572
+ (err) => {
3573
+ logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
3574
+ return null;
3575
+ }
3576
+ );
3577
+ if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
3578
+ }
3579
+ return { needs: decomposeHeuristic(document), source: "heuristic" };
3580
+ }
3581
+ async function decomposeWithLlm(document, apiKey, model) {
3582
+ const prompt = [
3583
+ "Project description:",
3584
+ document.slice(0, 8e3),
3585
+ "",
3586
+ 'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
3587
+ "One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
3588
+ ].join("\n");
3589
+ const { data } = await httpRequest("https://api.openai.com/v1/chat/completions", {
3590
+ host: "api.openai.com",
3591
+ method: "POST",
3592
+ ttlMs: 24 * 60 * 60 * 1e3,
3593
+ // Hash the FULL document — length + a 64-char prefix collide for same-length
3594
+ // edits or shared templated headers, which would serve a stale decomposition.
3595
+ cacheKey: `openai-plan ${model} ${createHash4("sha1").update(document).digest("hex")}`,
3596
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3597
+ body: JSON.stringify({
3598
+ model,
3599
+ messages: [
3600
+ { role: "system", content: DECOMPOSE_SYSTEM },
3601
+ { role: "user", content: prompt }
3602
+ ],
3603
+ response_format: { type: "json_object" },
3604
+ temperature: 0.2
3605
+ })
3606
+ });
3607
+ const content = data?.choices?.[0]?.message?.content;
3608
+ const parsed = content ? JSON.parse(content) : {};
3609
+ const raw = Array.isArray(parsed?.needs) ? parsed.needs : [];
3610
+ return raw.map((n) => {
3611
+ const need = typeof n?.need === "string" ? n.need.trim() : "";
3612
+ if (!need) return null;
3613
+ const cat = typeof n?.category === "string" && isCategory(n.category) ? n.category : void 0;
3614
+ return { need, category: cat };
3615
+ }).filter(Boolean);
3616
+ }
3617
+ function decomposeHeuristic(document) {
3618
+ const byCategory = /* @__PURE__ */ new Map();
3619
+ for (const rawLine of document.split("\n")) {
3620
+ const line = rawLine.replace(/^[#>*\-\s]+/, "").replace(/^\d+\.\s+/, "").trim();
3621
+ if (line.length < 3) continue;
3622
+ const category = inferCategory(line);
3623
+ if (category && !byCategory.has(category)) {
3624
+ byCategory.set(category, line.slice(0, 120));
3625
+ }
3626
+ }
3627
+ return [...byCategory.entries()].map(([category, need]) => ({ need, category }));
3628
+ }
3629
+ var PER_SLOT, MAX_SLOTS, NO_META, FAMILY_TOKENS, DECOMPOSE_SYSTEM;
3630
+ var init_plan = __esm({
3631
+ "src/mcp/plan.ts"() {
3632
+ "use strict";
3633
+ init_esm_shims();
3634
+ init_config();
3635
+ init_http();
3636
+ init_check();
3637
+ init_members();
3638
+ init_optimize();
3639
+ init_compat();
3640
+ init_logger();
3641
+ init_types();
3642
+ init_schema();
3643
+ init_categoryInference();
3644
+ init_recommend();
3645
+ init_single();
3646
+ init_diagram();
3647
+ init_handlers();
3648
+ PER_SLOT = 3;
3649
+ MAX_SLOTS = 24;
3650
+ NO_META = (c) => ({
3651
+ name: c.name,
3652
+ version: c.latestVersion,
3653
+ peerDependencies: null,
3654
+ peerDependenciesMeta: null,
3655
+ engines: null
3656
+ });
3657
+ FAMILY_TOKENS = [
3658
+ { family: "react", re: /\breact\b|preact/ },
3659
+ { family: "vue", re: /\bvue\b|nuxt/ },
3660
+ { family: "angular", re: /angular/ },
3661
+ { family: "svelte", re: /svelte/ },
3662
+ { family: "solid", re: /\bsolid(-?js)?\b/ }
3663
+ ];
3664
+ DECOMPOSE_SYSTEM = "You break a software project description into the distinct technical components that each need a library. Return ONLY components the project actually requires, grounded in the description. Respond with a JSON object.";
3665
+ }
3666
+ });
3667
+
2141
3668
  // src/mcp/server.ts
2142
3669
  var server_exports = {};
2143
3670
  __export(server_exports, {
2144
3671
  buildMcpServer: () => buildMcpServer,
3672
+ npmName: () => npmName,
2145
3673
  startMcpServer: () => startMcpServer
2146
3674
  });
2147
3675
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -2171,7 +3699,7 @@ function buildMcpServer(db) {
2171
3699
  title: "Evaluate a package",
2172
3700
  description: "Full evidence read for one npm package: scores, signals, advisories, summary, and a usage guide. Fetches & scores on demand if not yet tracked.",
2173
3701
  inputSchema: {
2174
- package: z2.string().min(1).describe("npm package name")
3702
+ package: npmName.describe("npm package name")
2175
3703
  }
2176
3704
  },
2177
3705
  async (args) => json(await handleEvaluate(db, args))
@@ -2182,18 +3710,29 @@ function buildMcpServer(db) {
2182
3710
  title: "Compare packages",
2183
3711
  description: "Side-by-side comparison of 2\u20135 npm packages, ranked by health score.",
2184
3712
  inputSchema: {
2185
- packages: z2.array(z2.string().min(1)).min(2).max(5).describe("2\u20135 npm package names")
3713
+ packages: z2.array(npmName).min(2).max(5).describe("2\u20135 npm package names")
2186
3714
  }
2187
3715
  },
2188
3716
  async (args) => json(await handleCompare(db, args))
2189
3717
  );
3718
+ server.registerTool(
3719
+ "compat",
3720
+ {
3721
+ title: "Check package compatibility",
3722
+ description: "Check whether a set of packages forms a coherent stack: peer-dependency and engine-range compatibility across the whole set (instant, from declared metadata), plus any recorded sandbox-verified conflicts. Returns the exact clashing constraints. Read-only \u2014 does not run installs. Call before committing to a multi-package stack.",
3723
+ inputSchema: {
3724
+ packages: z2.array(npmName).min(2).max(8).describe("2\u20138 npm package names to check together")
3725
+ }
3726
+ },
3727
+ async (args) => json(await handleCompat(db, args))
3728
+ );
2190
3729
  server.registerTool(
2191
3730
  "verify",
2192
3731
  {
2193
3732
  title: "Verify a package",
2194
3733
  description: "Confirm an npm package is real, healthy, and not risky before installing \u2014 guards against hallucinated or typosquatted dependency names. Checks the live registry.",
2195
3734
  inputSchema: {
2196
- package: z2.string().min(1).describe("npm package name to verify")
3735
+ package: npmName.describe("npm package name to verify")
2197
3736
  }
2198
3737
  },
2199
3738
  async (args) => json(await handleVerify(db, args))
@@ -2204,11 +3743,32 @@ function buildMcpServer(db) {
2204
3743
  title: "Reference architecture diagram",
2205
3744
  description: "Emit a reference-architecture Mermaid diagram for a stack you have already chosen (package names). A labeled starting point keyed by layer \u2014 not a validated architecture, and not an architecture designer.",
2206
3745
  inputSchema: {
2207
- stack: z2.array(z2.string()).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
3746
+ stack: z2.array(npmName).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
2208
3747
  }
2209
3748
  },
2210
3749
  async (args) => json(await handleDiagram(db, args))
2211
3750
  );
3751
+ server.registerTool(
3752
+ "plan",
3753
+ {
3754
+ title: "Plan a stack from a program description",
3755
+ description: "Turn a detailed program description (spec/README) or a list of component needs into an evidence-scored build plan: a real, lurq-scored package recommended per component, plus a Mermaid roadmap other agents can parse. Recommends building blocks slot-by-slot from the index \u2014 it does not invent an architecture from a bare prompt.",
3756
+ inputSchema: {
3757
+ document: z2.string().optional().describe("Detailed description of the program (spec/README); lurq decomposes it into components"),
3758
+ needs: z2.array(
3759
+ z2.object({
3760
+ need: z2.string().min(1).describe("A component that needs a library"),
3761
+ category: categoryEnum.optional()
3762
+ })
3763
+ ).optional().describe("Pre-decomposed components (skip if you pass a document)"),
3764
+ using: z2.array(npmName).max(12).optional().describe(
3765
+ "Packages you have already decided on. lurq pins these as fixed slots, recommends only the remaining needs, and checks/optimizes the whole stack around your picks."
3766
+ ),
3767
+ optimize: z2.enum(["speed", "balanced"]).optional().describe("'speed' prefers the lightest-bundle option per slot; default 'balanced'")
3768
+ }
3769
+ },
3770
+ async (args) => json(await handlePlan(db, args))
3771
+ );
2212
3772
  return server;
2213
3773
  }
2214
3774
  async function startMcpServer() {
@@ -2227,7 +3787,7 @@ async function startMcpServer() {
2227
3787
  await server.connect(transport);
2228
3788
  logger.info(`${SERVER_NAME} MCP server v${VERSION} running on stdio.`);
2229
3789
  }
2230
- var categoryEnum, confidenceEnum, constraintsSchema;
3790
+ var categoryEnum, confidenceEnum, npmName, constraintsSchema;
2231
3791
  var init_server = __esm({
2232
3792
  "src/mcp/server.ts"() {
2233
3793
  "use strict";
@@ -2238,8 +3798,10 @@ var init_server = __esm({
2238
3798
  init_logger();
2239
3799
  init_handlers();
2240
3800
  init_diagram();
3801
+ init_plan();
2241
3802
  categoryEnum = z2.enum(CATEGORIES);
2242
- confidenceEnum = z2.enum(["proven", "emerging", "unproven"]);
3803
+ confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
3804
+ npmName = z2.string().trim().min(1).max(214).regex(/^(?:@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/i, "Invalid npm package name");
2243
3805
  constraintsSchema = z2.object({
2244
3806
  runtime: z2.enum(["browser", "node", "both"]).optional(),
2245
3807
  license: z2.string().optional(),
@@ -2249,75 +3811,895 @@ var init_server = __esm({
2249
3811
  }
2250
3812
  });
2251
3813
 
2252
- // src/pipeline/index.ts
2253
- var pipeline_exports = {};
2254
- __export(pipeline_exports, {
2255
- runSync: () => runSync
2256
- });
2257
- var init_pipeline = __esm({
2258
- "src/pipeline/index.ts"() {
3814
+ // src/auth/apiKeys.ts
3815
+ import { createHash as createHash5, randomBytes } from "crypto";
3816
+ import { and as and5, desc as desc3, eq as eq5, isNull } from "drizzle-orm";
3817
+ function hashKey(key) {
3818
+ return createHash5("sha256").update(key).digest("hex");
3819
+ }
3820
+ function generateApiKey() {
3821
+ const body = randomBytes(24).toString("base64url");
3822
+ const key = API_KEY_PREFIX + body;
3823
+ return { key, hash: hashKey(key), prefix: API_KEY_PREFIX + body.slice(0, DISPLAY_BODY) };
3824
+ }
3825
+ async function createKey(db, input = {}) {
3826
+ const { key, hash, prefix } = generateApiKey();
3827
+ const [row] = await db.insert(apiKeys).values({
3828
+ keyHash: hash,
3829
+ prefix,
3830
+ label: input.label,
3831
+ tier: input.tier ?? "free",
3832
+ ownerId: input.ownerId
3833
+ }).returning();
3834
+ return { key, row };
3835
+ }
3836
+ async function lookupActiveKey(db, key) {
3837
+ const hash = hashKey(key);
3838
+ const [row] = await db.select().from(apiKeys).where(and5(eq5(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
3839
+ if (!row) return null;
3840
+ db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, row.id)).then(void 0, () => {
3841
+ });
3842
+ return row;
3843
+ }
3844
+ async function listKeys(db) {
3845
+ return db.select().from(apiKeys).orderBy(desc3(apiKeys.createdAt));
3846
+ }
3847
+ function matchByPrefixOrId(prefixOrId) {
3848
+ const asId = Number(prefixOrId);
3849
+ return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq5(apiKeys.id, asId) : eq5(apiKeys.prefix, prefixOrId);
3850
+ }
3851
+ async function revokeKey(db, prefixOrId) {
3852
+ const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
3853
+ return rows.length;
3854
+ }
3855
+ async function rotateKey(db, prefixOrId) {
3856
+ const [previous] = await db.select().from(apiKeys).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).limit(1);
3857
+ if (!previous) return null;
3858
+ const { key, row } = await createKey(db, {
3859
+ label: previous.label ?? void 0,
3860
+ tier: previous.tier,
3861
+ ownerId: previous.ownerId ?? void 0
3862
+ });
3863
+ await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, previous.id));
3864
+ return { key, row, previous };
3865
+ }
3866
+ var DISPLAY_BODY;
3867
+ var init_apiKeys = __esm({
3868
+ "src/auth/apiKeys.ts"() {
2259
3869
  "use strict";
2260
3870
  init_esm_shims();
2261
- init_sync();
3871
+ init_constants();
3872
+ init_schema();
3873
+ DISPLAY_BODY = 6;
2262
3874
  }
2263
3875
  });
2264
3876
 
2265
- // src/cli/format.ts
2266
- function table(headers, rows) {
2267
- const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
2268
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
2269
- const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
2270
- const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
2271
- for (const r of rows) out.push(line(r));
2272
- return out.join("\n");
2273
- }
2274
- function detail(pairs) {
2275
- const labelWidth = Math.max(...pairs.map(([k]) => k.length));
2276
- return pairs.map(([k, v]) => `${dim((k + ":").padEnd(labelWidth + 1))} ${v}`).join("\n");
2277
- }
2278
- function formatNumber(n) {
2279
- if (n == null) return "\u2014";
2280
- if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
2281
- if (n >= 1e3) return `${Math.round(n / 100) / 10}k`;
2282
- return String(n);
2283
- }
2284
- function formatDate(iso) {
2285
- return iso ? iso.slice(0, 10) : "\u2014";
2286
- }
2287
- function formatPercent(fraction) {
2288
- if (fraction == null) return "\u2014";
2289
- const pct = Math.round(fraction * 1e3) / 10;
2290
- return `${pct > 0 ? "+" : ""}${pct}%`;
3877
+ // src/mcp/http.ts
3878
+ var http_exports = {};
3879
+ __export(http_exports, {
3880
+ startHttpServer: () => startHttpServer
3881
+ });
3882
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3883
+ function rpcError(code, message) {
3884
+ return { jsonrpc: "2.0", error: { code, message }, id: null };
2291
3885
  }
2292
- function confidenceLabel(c) {
2293
- if (c === "proven") return green(c);
2294
- if (c === "emerging") return yellow(c);
2295
- return red(c);
3886
+ async function startHttpServer(opts = {}) {
3887
+ const config = getConfig();
3888
+ const port = opts.port ?? config.PORT;
3889
+ const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
3890
+ const { db } = createDb({ max: 20 });
3891
+ const app = express();
3892
+ app.set("trust proxy", 1);
3893
+ app.use(helmet());
3894
+ app.use(express.json({ limit: "1mb" }));
3895
+ app.get("/healthz", (_req, res) => {
3896
+ res.status(200).json({ status: "ok" });
3897
+ });
3898
+ const ipLimiter = rateLimit({
3899
+ windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
3900
+ limit: config.LURQ_IP_RATE_LIMIT_MAX,
3901
+ standardHeaders: "draft-7",
3902
+ legacyHeaders: false,
3903
+ message: rpcError(-32029, "Rate limit exceeded.")
3904
+ });
3905
+ const auth = async (req, res, next) => {
3906
+ const header = req.headers.authorization;
3907
+ const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
3908
+ if (!token) {
3909
+ res.status(401).json(rpcError(-32001, "Missing API key. Pass Authorization: Bearer <key>."));
3910
+ return;
3911
+ }
3912
+ try {
3913
+ const row = await lookupActiveKey(db, token);
3914
+ if (!row) {
3915
+ res.status(401).json(rpcError(-32001, "Invalid or revoked API key."));
3916
+ return;
3917
+ }
3918
+ req.lurqKey = row;
3919
+ next();
3920
+ } catch (err) {
3921
+ logger.error("auth lookup failed:", err instanceof Error ? err.message : String(err));
3922
+ res.status(500).json(rpcError(-32603, "Internal error."));
3923
+ }
3924
+ };
3925
+ const keyLimiter = rateLimit({
3926
+ windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
3927
+ limit: config.LURQ_RATE_LIMIT_MAX,
3928
+ standardHeaders: "draft-7",
3929
+ legacyHeaders: false,
3930
+ // Key on the resolved API key's unique row id (always present — auth runs
3931
+ // first). The display `prefix` is only 6 chars of body, so distinct keys
3932
+ // can collide on it and share a quota; the id cannot. The IP fallback uses
3933
+ // express-rate-limit's ipKeyGenerator so IPv6 addresses are normalized
3934
+ // correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
3935
+ keyGenerator: (req) => {
3936
+ const id = req.lurqKey?.id;
3937
+ return id != null ? `key:${id}` : ipKeyGenerator(req.ip ?? "0.0.0.0");
3938
+ },
3939
+ message: rpcError(-32029, "Rate limit exceeded.")
3940
+ });
3941
+ app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
3942
+ const server = buildMcpServer(db);
3943
+ const transport = new StreamableHTTPServerTransport({
3944
+ sessionIdGenerator: void 0,
3945
+ enableJsonResponse: true
3946
+ });
3947
+ res.on("close", () => {
3948
+ void transport.close();
3949
+ void server.close();
3950
+ });
3951
+ try {
3952
+ await server.connect(transport);
3953
+ await transport.handleRequest(req, res, req.body);
3954
+ } catch (err) {
3955
+ logger.error("mcp request failed:", err instanceof Error ? err.message : String(err));
3956
+ if (!res.headersSent) res.status(500).json(rpcError(-32603, "Internal error"));
3957
+ }
3958
+ });
3959
+ app.all("/mcp", (_req, res) => {
3960
+ res.status(405).json(rpcError(-32e3, "Method not allowed."));
3961
+ });
3962
+ app.listen(port, () => {
3963
+ logger.info(`lurq HTTP MCP server listening on :${port}/mcp`);
3964
+ });
2296
3965
  }
2297
- var ESC, wrap, bold, dim, red, green, yellow, ANSI_RE, width;
2298
- var init_format = __esm({
2299
- "src/cli/format.ts"() {
3966
+ var init_http2 = __esm({
3967
+ "src/mcp/http.ts"() {
2300
3968
  "use strict";
2301
3969
  init_esm_shims();
2302
- ESC = "\x1B[";
2303
- wrap = (code, s) => `${ESC}${code}m${s}${ESC}0m`;
2304
- bold = (s) => wrap("1", s);
2305
- dim = (s) => wrap("2", s);
2306
- red = (s) => wrap("31", s);
2307
- green = (s) => wrap("32", s);
2308
- yellow = (s) => wrap("33", s);
2309
- ANSI_RE = /\[[0-9;]*m/g;
2310
- width = (s) => s.replace(ANSI_RE, "").length;
3970
+ init_config();
3971
+ init_logger();
3972
+ init_apiKeys();
3973
+ init_client();
3974
+ init_server();
2311
3975
  }
2312
3976
  });
2313
3977
 
2314
- // src/cli/commands.ts
2315
- var commands_exports = {};
2316
- __export(commands_exports, {
2317
- runCompare: () => runCompare,
2318
- runEvaluate: () => runEvaluate,
3978
+ // src/pipeline/rescore.ts
3979
+ import { isNotNull as isNotNull4 } from "drizzle-orm";
3980
+ import { eq as eq6 } from "drizzle-orm";
3981
+ async function runRescore() {
3982
+ const weights = loadWeights();
3983
+ const handle = createDb({ max: 4 });
3984
+ try {
3985
+ const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull4(packages.scoreBreakdown));
3986
+ let updated = 0;
3987
+ for (const row of rows) {
3988
+ if (!row.breakdown) continue;
3989
+ const health = computeHealthScore(row.breakdown, weights.health);
3990
+ if (health !== row.healthScore) {
3991
+ await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq6(packages.id, row.id));
3992
+ updated++;
3993
+ }
3994
+ }
3995
+ logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
3996
+ if (updated > 0) await invalidateCache();
3997
+ return { seen: rows.length, updated };
3998
+ } finally {
3999
+ await handle.close();
4000
+ }
4001
+ }
4002
+ var init_rescore = __esm({
4003
+ "src/pipeline/rescore.ts"() {
4004
+ "use strict";
4005
+ init_esm_shims();
4006
+ init_cache();
4007
+ init_logger();
4008
+ init_client();
4009
+ init_schema();
4010
+ init_scoring();
4011
+ init_weights();
4012
+ }
4013
+ });
4014
+
4015
+ // src/db/discovery.ts
4016
+ import { eq as eq7 } from "drizzle-orm";
4017
+ async function getKnownNames(db) {
4018
+ const [tracked, queued] = await Promise.all([
4019
+ db.select({ name: packages.name }).from(packages),
4020
+ db.select({ name: discoveryQueue.name }).from(discoveryQueue)
4021
+ ]);
4022
+ return /* @__PURE__ */ new Set([...tracked.map((r) => r.name), ...queued.map((r) => r.name)]);
4023
+ }
4024
+ async function enqueueCandidates(db, candidates) {
4025
+ if (candidates.length === 0) return 0;
4026
+ const rows = candidates.map((c) => ({ name: c.name, discoveredVia: c.via }));
4027
+ const inserted = await db.insert(discoveryQueue).values(rows).onConflictDoNothing({ target: discoveryQueue.name }).returning({ id: discoveryQueue.id });
4028
+ return inserted.length;
4029
+ }
4030
+ async function getPendingCandidates(db, limit) {
4031
+ return db.select().from(discoveryQueue).where(eq7(discoveryQueue.status, "pending")).limit(limit);
4032
+ }
4033
+ async function setDiscoveryStatus(db, name, data) {
4034
+ await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq7(discoveryQueue.name, name));
4035
+ }
4036
+ var init_discovery = __esm({
4037
+ "src/db/discovery.ts"() {
4038
+ "use strict";
4039
+ init_esm_shims();
4040
+ init_schema();
4041
+ }
4042
+ });
4043
+
4044
+ // src/pipeline/discovery.ts
4045
+ import { isNotNull as isNotNull5 } from "drizzle-orm";
4046
+ function selectCandidates(raw, known) {
4047
+ const seen = new Set(known);
4048
+ const out = [];
4049
+ for (const c of raw) {
4050
+ const name = c.name?.trim();
4051
+ if (!name || seen.has(name)) continue;
4052
+ seen.add(name);
4053
+ out.push({ name, via: c.via });
4054
+ }
4055
+ return out;
4056
+ }
4057
+ function passesGate(preScore) {
4058
+ return preScore !== null && preScore >= DISCOVERY.minPreScore;
4059
+ }
4060
+ async function preScorePackage(name, fetchImpl) {
4061
+ try {
4062
+ const registry = await fetchNpmRegistry(name, fetchImpl);
4063
+ const signals = {
4064
+ name,
4065
+ registry,
4066
+ downloads: null,
4067
+ github: null,
4068
+ depsDev: null,
4069
+ bundle: null,
4070
+ errors: []
4071
+ };
4072
+ return computeQuality(toScoringInput(signals, null));
4073
+ } catch {
4074
+ return null;
4075
+ }
4076
+ }
4077
+ async function graphChannel(db) {
4078
+ const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull5(packages.latestVersion));
4079
+ const out = [];
4080
+ for (const t of tracked) {
4081
+ if (!t.version) continue;
4082
+ const deps = (await fetchDependencyNames(t.name, t.version)).slice(
4083
+ 0,
4084
+ DISCOVERY.graphNeighborsPerSeed
4085
+ );
4086
+ for (const name of deps) out.push({ name, via: "dependency-graph" });
4087
+ }
4088
+ return out;
4089
+ }
4090
+ async function searchChannel() {
4091
+ const out = [];
4092
+ for (const category of CATEGORIES) {
4093
+ if (category === "other") continue;
4094
+ const hits = await searchNpm(`keywords:${category}`, DISCOVERY.searchSizePerCategory);
4095
+ for (const hit of hits) {
4096
+ const via = isRecent(hit.date) ? "recent" : "category-search";
4097
+ out.push({ name: hit.name, via });
4098
+ }
4099
+ }
4100
+ return out;
4101
+ }
4102
+ function isRecent(date) {
4103
+ if (!date) return false;
4104
+ const ms = Date.parse(date);
4105
+ if (Number.isNaN(ms)) return false;
4106
+ return Date.now() - ms <= 90 * 24 * 60 * 60 * 1e3;
4107
+ }
4108
+ async function runDiscovery(opts = {}) {
4109
+ const cap = opts.perRunCap ?? DISCOVERY.perRunCap;
4110
+ const handle = createDb({ max: 6 });
4111
+ try {
4112
+ logger.info("Discovery: gathering candidates from graph + search channels\u2026");
4113
+ const [graph, search] = await Promise.all([graphChannel(handle.db), searchChannel()]);
4114
+ const known = await getKnownNames(handle.db);
4115
+ const fresh = selectCandidates([...graph, ...search], known);
4116
+ const enqueued = await enqueueCandidates(handle.db, fresh);
4117
+ logger.info(
4118
+ `Discovery: ${graph.length} graph + ${search.length} search candidates \u2192 ${enqueued} new queued.`
4119
+ );
4120
+ const pending = await getPendingCandidates(handle.db, cap * 4);
4121
+ const scored = [];
4122
+ for (const cand of pending) {
4123
+ const preScore = await preScorePackage(cand.name);
4124
+ await setDiscoveryStatus(handle.db, cand.name, {
4125
+ status: passesGate(preScore) ? "pending" : "rejected",
4126
+ preScore: preScore ?? null
4127
+ });
4128
+ if (passesGate(preScore)) scored.push({ name: cand.name, preScore });
4129
+ }
4130
+ logger.info(`Discovery: gated ${pending.length}; ${scored.length} cleared the quality bar.`);
4131
+ scored.sort((a, b) => b.preScore - a.preScore);
4132
+ const toIngest = scored.slice(0, cap);
4133
+ const deferred = scored.slice(cap);
4134
+ if (deferred.length > 0) {
4135
+ logger.info(
4136
+ `Discovery: per-run cap ${cap} reached \u2014 ${deferred.length} eligible candidate(s) deferred to the next run: ${deferred.map((d) => d.name).join(", ")}`
4137
+ );
4138
+ }
4139
+ let ingested = 0;
4140
+ if (!opts.dryRun) {
4141
+ for (const cand of toIngest) {
4142
+ try {
4143
+ await syncOnePackage(handle.db, cand.name);
4144
+ await setDiscoveryStatus(handle.db, cand.name, { status: "ingested" });
4145
+ ingested++;
4146
+ } catch (err) {
4147
+ logger.warn(`Discovery: failed to ingest ${cand.name}: ${err.message}`);
4148
+ }
4149
+ }
4150
+ }
4151
+ logger.info(`Discovery: ingested ${ingested}/${toIngest.length} candidate(s).`);
4152
+ return {
4153
+ enqueued,
4154
+ gated: pending.length,
4155
+ passed: scored.length,
4156
+ ingested,
4157
+ droppedToNextRun: deferred.length
4158
+ };
4159
+ } finally {
4160
+ await handle.close();
4161
+ }
4162
+ }
4163
+ var init_discovery2 = __esm({
4164
+ "src/pipeline/discovery.ts"() {
4165
+ "use strict";
4166
+ init_esm_shims();
4167
+ init_logger();
4168
+ init_types();
4169
+ init_client();
4170
+ init_discovery();
4171
+ init_schema();
4172
+ init_depsDev();
4173
+ init_npmRegistry();
4174
+ init_npmSearch();
4175
+ init_scoring();
4176
+ init_weights();
4177
+ init_single();
4178
+ }
4179
+ });
4180
+
4181
+ // src/pipeline/index.ts
4182
+ var pipeline_exports = {};
4183
+ __export(pipeline_exports, {
4184
+ runDiscovery: () => runDiscovery,
4185
+ runRescore: () => runRescore,
4186
+ runSync: () => runSync
4187
+ });
4188
+ var init_pipeline = __esm({
4189
+ "src/pipeline/index.ts"() {
4190
+ "use strict";
4191
+ init_esm_shims();
4192
+ init_sync();
4193
+ init_rescore();
4194
+ init_discovery2();
4195
+ }
4196
+ });
4197
+
4198
+ // src/cli/format.ts
4199
+ function table(headers, rows) {
4200
+ const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
4201
+ const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
4202
+ const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
4203
+ const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
4204
+ for (const r of rows) out.push(line(r));
4205
+ return out.join("\n");
4206
+ }
4207
+ function detail(pairs) {
4208
+ const labelWidth = Math.max(...pairs.map(([k]) => k.length));
4209
+ return pairs.map(([k, v]) => `${dim((k + ":").padEnd(labelWidth + 1))} ${v}`).join("\n");
4210
+ }
4211
+ function formatNumber(n) {
4212
+ if (n == null) return "\u2014";
4213
+ if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
4214
+ if (n >= 1e3) return `${Math.round(n / 100) / 10}k`;
4215
+ return String(n);
4216
+ }
4217
+ function formatDate(iso) {
4218
+ return iso ? iso.slice(0, 10) : "\u2014";
4219
+ }
4220
+ function formatPercent(fraction) {
4221
+ if (fraction == null) return "\u2014";
4222
+ const pct = Math.round(fraction * 1e3) / 10;
4223
+ return `${pct > 0 ? "+" : ""}${pct}%`;
4224
+ }
4225
+ function confidenceLabel(c) {
4226
+ if (c === "proven") return green(c);
4227
+ if (c === "emerging") return yellow(c);
4228
+ if (c === "promising") return green(c);
4229
+ return red(c);
4230
+ }
4231
+ var ESC, wrap, bold, dim, red, green, yellow, ANSI_RE, width;
4232
+ var init_format = __esm({
4233
+ "src/cli/format.ts"() {
4234
+ "use strict";
4235
+ init_esm_shims();
4236
+ ESC = "\x1B[";
4237
+ wrap = (code, s) => `${ESC}${code}m${s}${ESC}0m`;
4238
+ bold = (s) => wrap("1", s);
4239
+ dim = (s) => wrap("2", s);
4240
+ red = (s) => wrap("31", s);
4241
+ green = (s) => wrap("32", s);
4242
+ yellow = (s) => wrap("33", s);
4243
+ ANSI_RE = /\[[0-9;]*m/g;
4244
+ width = (s) => s.replace(ANSI_RE, "").length;
4245
+ }
4246
+ });
4247
+
4248
+ // src/cli/planView.ts
4249
+ var planView_exports = {};
4250
+ __export(planView_exports, {
4251
+ renderPlanHtml: () => renderPlanHtml
4252
+ });
4253
+ function esc(s) {
4254
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4255
+ }
4256
+ function slotRows(plan) {
4257
+ return plan.slots.map((s) => {
4258
+ const rec = s.recommended;
4259
+ const name = rec ? rec.repoUrl ? `<a href="${esc(rec.repoUrl)}" target="_blank" rel="noreferrer">${esc(rec.name)}</a>` : esc(rec.name) : '<span class="muted">\u2014 no match \u2014</span>';
4260
+ const alts = s.alternatives.map((a) => esc(a.name)).join(", ") || '<span class="muted">\u2014</span>';
4261
+ return `<tr>
4262
+ <td>${esc(s.need)}</td>
4263
+ <td><span class="layer">${esc(s.layer)}</span></td>
4264
+ <td class="pkg">${name}</td>
4265
+ <td class="num">${rec ? rec.healthScore : "\u2014"}</td>
4266
+ <td>${rec ? esc(rec.confidence) : "\u2014"}</td>
4267
+ <td class="alts">${alts}</td>
4268
+ </tr>`;
4269
+ }).join("\n");
4270
+ }
4271
+ function renderPlanHtml(plan) {
4272
+ return `<!doctype html>
4273
+ <html lang="en">
4274
+ <head>
4275
+ <meta charset="utf-8" />
4276
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4277
+ <title>lurq plan \u2014 roadmap</title>
4278
+ <style>
4279
+ :root { color-scheme: dark; }
4280
+ * { box-sizing: border-box; }
4281
+ body { margin: 0; font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
4282
+ background: #0b0b0f; color: #e7e7ea; padding: 2.5rem clamp(1rem, 5vw, 4rem); }
4283
+ h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
4284
+ h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: .08em; color: #9a9aa6; margin: 2.5rem 0 .75rem; }
4285
+ .sub { color: #9a9aa6; margin: 0 0 1rem; max-width: 70ch; }
4286
+ .diagram { background: #141419; border: 1px solid #26262e; border-radius: 14px; padding: 1.5rem; overflow: auto; }
4287
+ table { width: 100%; border-collapse: collapse; margin-top: .5rem; }
4288
+ th, td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid #1e1e25; vertical-align: top; }
4289
+ th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #9a9aa6; }
4290
+ .pkg a { color: #d98cff; text-decoration: none; } .pkg a:hover { text-decoration: underline; }
4291
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
4292
+ .muted { color: #6a6a76; } .alts { color: #b8b8c2; font-size: .92rem; }
4293
+ .layer { font-size: .8rem; color: #8ad; background: #15202b; border-radius: 6px; padding: .1rem .45rem; }
4294
+ footer { margin-top: 2.5rem; color: #6a6a76; font-size: .85rem; }
4295
+ </style>
4296
+ </head>
4297
+ <body>
4298
+ <h1>lurq plan <span class="muted">\xB7 ${esc(plan.optimize)} \xB7 ${esc(plan.source)}${plan.framework ? ` \xB7 ${esc(plan.framework)} ecosystem` : ""}</span></h1>
4299
+ <p class="sub">${esc(plan.note)}</p>
4300
+
4301
+ <h2>Roadmap</h2>
4302
+ <div class="diagram"><pre class="mermaid">${esc(plan.mermaid)}</pre></div>
4303
+
4304
+ <h2>Components</h2>
4305
+ <table>
4306
+ <thead><tr><th>Component</th><th>Layer</th><th>Recommended</th><th>Health</th><th>Confidence</th><th>Alternatives</th></tr></thead>
4307
+ <tbody>
4308
+ ${slotRows(plan)}
4309
+ </tbody>
4310
+ </table>
4311
+
4312
+ <footer>data as of ${esc(plan.dataAsOf)} \xB7 generated by lurq</footer>
4313
+
4314
+ <script type="module">
4315
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
4316
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
4317
+ </script>
4318
+ </body>
4319
+ </html>
4320
+ `;
4321
+ }
4322
+ var init_planView = __esm({
4323
+ "src/cli/planView.ts"() {
4324
+ "use strict";
4325
+ init_esm_shims();
4326
+ }
4327
+ });
4328
+
4329
+ // src/db/watch.ts
4330
+ import { eq as eq8 } from "drizzle-orm";
4331
+ async function getWatchCursor(db, id) {
4332
+ const rows = await db.select({ seq: watchState.seq }).from(watchState).where(eq8(watchState.id, id)).limit(1);
4333
+ return rows[0]?.seq ?? null;
4334
+ }
4335
+ async function setWatchCursor(db, id, seq) {
4336
+ await db.insert(watchState).values({ id, seq, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({ target: watchState.id, set: { seq, updatedAt: /* @__PURE__ */ new Date() } });
4337
+ }
4338
+ var init_watch = __esm({
4339
+ "src/db/watch.ts"() {
4340
+ "use strict";
4341
+ init_esm_shims();
4342
+ init_schema();
4343
+ }
4344
+ });
4345
+
4346
+ // src/pipeline/watch.ts
4347
+ var watch_exports = {};
4348
+ __export(watch_exports, {
4349
+ parseChangeLine: () => parseChangeLine,
4350
+ watchNpmChanges: () => watchNpmChanges
4351
+ });
4352
+ function parseChangeLine(line) {
4353
+ const trimmed = line.trim();
4354
+ if (!trimmed) return null;
4355
+ try {
4356
+ const obj = JSON.parse(trimmed);
4357
+ if (typeof obj?.id !== "string" || obj.seq == null) return null;
4358
+ return { seq: obj.seq, id: obj.id, deleted: Boolean(obj.deleted) };
4359
+ } catch {
4360
+ return null;
4361
+ }
4362
+ }
4363
+ async function watchNpmChanges(db, opts = {}) {
4364
+ const { signal } = opts;
4365
+ let backoff2 = 1e3;
4366
+ while (!signal?.aborted) {
4367
+ let tracked = new Set(await getAllPackageNames(db));
4368
+ let trackedAt = Date.now();
4369
+ const since = await getWatchCursor(db, FEED_ID) ?? opts.since ?? "now";
4370
+ const url = `${FEED_URL}?feed=continuous&since=${encodeURIComponent(since)}&heartbeat=${HEARTBEAT_MS}`;
4371
+ logger.info(`watch: connecting from seq=${since} (${tracked.size} tracked packages)`);
4372
+ try {
4373
+ const res = await fetch(url, { signal });
4374
+ if (!res.ok || !res.body) throw new Error(`feed responded ${res.status}`);
4375
+ backoff2 = 1e3;
4376
+ let sinceCheckpoint = 0;
4377
+ for await (const line of ndjsonLines(res.body, signal)) {
4378
+ const change = parseChangeLine(line);
4379
+ if (!change) continue;
4380
+ const seq = String(change.seq);
4381
+ sinceCheckpoint++;
4382
+ if (Date.now() - trackedAt > TRACKED_REFRESH_MS) {
4383
+ tracked = new Set(await getAllPackageNames(db));
4384
+ trackedAt = Date.now();
4385
+ }
4386
+ if (!change.deleted && tracked.has(change.id)) {
4387
+ logger.info(`watch: re-syncing ${change.id} (seq=${seq})`);
4388
+ await syncOnePackage(db, change.id).catch(
4389
+ (err) => logger.warn(`watch: re-sync failed for ${change.id}: ${String(err)}`)
4390
+ );
4391
+ await setWatchCursor(db, FEED_ID, seq);
4392
+ sinceCheckpoint = 0;
4393
+ } else if (sinceCheckpoint >= CHECKPOINT_EVERY) {
4394
+ await setWatchCursor(db, FEED_ID, seq);
4395
+ sinceCheckpoint = 0;
4396
+ }
4397
+ }
4398
+ logger.info("watch: feed stream ended; reconnecting");
4399
+ } catch (err) {
4400
+ if (signal?.aborted) break;
4401
+ logger.warn(`watch: ${String(err)} \u2014 retrying in ${backoff2}ms`);
4402
+ await sleep(backoff2, signal);
4403
+ backoff2 = Math.min(backoff2 * 2, MAX_BACKOFF_MS);
4404
+ }
4405
+ }
4406
+ }
4407
+ async function* ndjsonLines(body, signal) {
4408
+ const reader = body.getReader();
4409
+ const decoder = new TextDecoder();
4410
+ let buffer = "";
4411
+ try {
4412
+ while (!signal?.aborted) {
4413
+ const { value, done } = await reader.read();
4414
+ if (done) break;
4415
+ buffer += decoder.decode(value, { stream: true });
4416
+ let nl;
4417
+ while ((nl = buffer.indexOf("\n")) >= 0) {
4418
+ yield buffer.slice(0, nl);
4419
+ buffer = buffer.slice(nl + 1);
4420
+ }
4421
+ }
4422
+ } finally {
4423
+ reader.releaseLock();
4424
+ }
4425
+ }
4426
+ function sleep(ms, signal) {
4427
+ return new Promise((resolve) => {
4428
+ const timer = setTimeout(resolve, ms);
4429
+ signal?.addEventListener(
4430
+ "abort",
4431
+ () => {
4432
+ clearTimeout(timer);
4433
+ resolve();
4434
+ },
4435
+ { once: true }
4436
+ );
4437
+ });
4438
+ }
4439
+ var FEED_ID, FEED_URL, HEARTBEAT_MS, TRACKED_REFRESH_MS, CHECKPOINT_EVERY, MAX_BACKOFF_MS;
4440
+ var init_watch2 = __esm({
4441
+ "src/pipeline/watch.ts"() {
4442
+ "use strict";
4443
+ init_esm_shims();
4444
+ init_logger();
4445
+ init_packages();
4446
+ init_watch();
4447
+ init_single();
4448
+ FEED_ID = "npm-changes";
4449
+ FEED_URL = "https://replicate.npmjs.com/_changes";
4450
+ HEARTBEAT_MS = 3e4;
4451
+ TRACKED_REFRESH_MS = 5 * 6e4;
4452
+ CHECKPOINT_EVERY = 200;
4453
+ MAX_BACKOFF_MS = 3e4;
4454
+ }
4455
+ });
4456
+
4457
+ // src/sandbox/types.ts
4458
+ var DEFAULT_TARGET;
4459
+ var init_types2 = __esm({
4460
+ "src/sandbox/types.ts"() {
4461
+ "use strict";
4462
+ init_esm_shims();
4463
+ DEFAULT_TARGET = {
4464
+ node: "20",
4465
+ moduleSystem: "cjs"
4466
+ };
4467
+ }
4468
+ });
4469
+
4470
+ // src/sandbox/local.ts
4471
+ import { execFile } from "child_process";
4472
+ import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
4473
+ import { tmpdir } from "os";
4474
+ import { join as join3 } from "path";
4475
+ import { promisify } from "util";
4476
+ function npmInstallArgs(specs, opts) {
4477
+ const bad = specs.find((s) => s.startsWith("-"));
4478
+ if (bad) throw new Error(`Invalid package spec: ${bad}`);
4479
+ const args = ["install", ...specs, "--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
4480
+ if (!opts.allowScripts) args.push("--ignore-scripts");
4481
+ return args;
4482
+ }
4483
+ function smokeScript(pkg, moduleSystem) {
4484
+ return moduleSystem === "esm" ? ["--input-type=module", "-e", `await import(${JSON.stringify(pkg)})`] : ["-e", `require(${JSON.stringify(pkg)})`];
4485
+ }
4486
+ function condense(s) {
4487
+ return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX);
4488
+ }
4489
+ function stderrOf(err) {
4490
+ if (err && typeof err === "object") {
4491
+ const e = err;
4492
+ if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
4493
+ if (typeof e.message === "string") return e.message;
4494
+ }
4495
+ return String(err);
4496
+ }
4497
+ var execFileAsync, INSTALL_TIMEOUT_MS, SMOKE_TIMEOUT_MS, ERROR_MAX, toSpec, LocalSandbox;
4498
+ var init_local = __esm({
4499
+ "src/sandbox/local.ts"() {
4500
+ "use strict";
4501
+ init_esm_shims();
4502
+ init_types2();
4503
+ execFileAsync = promisify(execFile);
4504
+ INSTALL_TIMEOUT_MS = 12e4;
4505
+ SMOKE_TIMEOUT_MS = 3e4;
4506
+ ERROR_MAX = 500;
4507
+ toSpec = (p) => p.version ? `${p.name}@${p.version}` : p.name;
4508
+ LocalSandbox = class {
4509
+ name = "local";
4510
+ async verify(pkg, version, opts = {}) {
4511
+ const set = await this.verifySet([{ name: pkg, version }], opts);
4512
+ return {
4513
+ driver: this.name,
4514
+ moduleSystem: set.moduleSystem,
4515
+ installed: set.installed,
4516
+ imported: set.loaded[0]?.loaded ?? null,
4517
+ ranScripts: opts.allowScripts ?? false,
4518
+ durationMs: set.durationMs,
4519
+ error: set.error
4520
+ };
4521
+ }
4522
+ async verifySet(packages2, opts = {}) {
4523
+ const target = opts.target ?? DEFAULT_TARGET;
4524
+ const allowScripts = opts.allowScripts ?? false;
4525
+ const specs = packages2.map(toSpec);
4526
+ const dir = await mkdtemp(join3(tmpdir(), "lurq-sandbox-"));
4527
+ const started = Date.now();
4528
+ const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
4529
+ let installed = false;
4530
+ let error = null;
4531
+ try {
4532
+ await writeFile2(
4533
+ join3(dir, "package.json"),
4534
+ JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
4535
+ );
4536
+ await execFileAsync("npm", npmInstallArgs(specs, { allowScripts }), {
4537
+ cwd: dir,
4538
+ timeout: opts.timeoutMs ?? INSTALL_TIMEOUT_MS,
4539
+ signal: opts.signal
4540
+ });
4541
+ installed = true;
4542
+ for (let i = 0; i < packages2.length; i++) {
4543
+ try {
4544
+ await execFileAsync("node", smokeScript(packages2[i].name, target.moduleSystem), {
4545
+ cwd: dir,
4546
+ timeout: SMOKE_TIMEOUT_MS,
4547
+ signal: opts.signal
4548
+ });
4549
+ loaded[i].loaded = true;
4550
+ } catch (err) {
4551
+ loaded[i].loaded = false;
4552
+ if (!error) error = condense(stderrOf(err));
4553
+ }
4554
+ }
4555
+ } catch (err) {
4556
+ error = condense(stderrOf(err));
4557
+ } finally {
4558
+ await rm(dir, { recursive: true, force: true }).catch(() => {
4559
+ });
4560
+ }
4561
+ return {
4562
+ driver: this.name,
4563
+ moduleSystem: target.moduleSystem,
4564
+ installed,
4565
+ loaded,
4566
+ durationMs: Date.now() - started,
4567
+ error
4568
+ };
4569
+ }
4570
+ };
4571
+ }
4572
+ });
4573
+
4574
+ // src/sandbox/index.ts
4575
+ function getSandbox() {
4576
+ return new LocalSandbox();
4577
+ }
4578
+ var init_sandbox = __esm({
4579
+ "src/sandbox/index.ts"() {
4580
+ "use strict";
4581
+ init_esm_shims();
4582
+ init_local();
4583
+ init_types2();
4584
+ init_local();
4585
+ }
4586
+ });
4587
+
4588
+ // src/pipeline/sandbox.ts
4589
+ var sandbox_exports = {};
4590
+ __export(sandbox_exports, {
4591
+ verifyPackageInSandbox: () => verifyPackageInSandbox
4592
+ });
4593
+ async function verifyPackageInSandbox(db, pkg, version, opts = {}) {
4594
+ const result = await getSandbox().verify(pkg, version, opts);
4595
+ await storeVerificationRun(db, {
4596
+ packageName: pkg,
4597
+ version: version ?? "latest",
4598
+ driver: result.driver,
4599
+ moduleSystem: result.moduleSystem,
4600
+ installed: result.installed,
4601
+ imported: result.imported,
4602
+ ranScripts: result.ranScripts,
4603
+ durationMs: result.durationMs,
4604
+ error: result.error,
4605
+ ranAt: /* @__PURE__ */ new Date()
4606
+ }).catch(() => {
4607
+ });
4608
+ return result;
4609
+ }
4610
+ var init_sandbox2 = __esm({
4611
+ "src/pipeline/sandbox.ts"() {
4612
+ "use strict";
4613
+ init_esm_shims();
4614
+ init_verification();
4615
+ init_sandbox();
4616
+ }
4617
+ });
4618
+
4619
+ // src/pipeline/compat.ts
4620
+ var compat_exports = {};
4621
+ __export(compat_exports, {
4622
+ deriveCompatEdges: () => deriveCompatEdges,
4623
+ verifyCompatibility: () => verifyCompatibility
4624
+ });
4625
+ function deriveCompatEdges(resolved, result) {
4626
+ const allLoaded = result.loaded.every((l) => l.loaded === true);
4627
+ const edges = [];
4628
+ if (result.installed && allLoaded) {
4629
+ for (let i = 0; i < resolved.length; i++) {
4630
+ for (let j = i + 1; j < resolved.length; j++) {
4631
+ edges.push({
4632
+ a: resolved[i].name,
4633
+ aVersion: resolved[i].version,
4634
+ b: resolved[j].name,
4635
+ bVersion: resolved[j].version,
4636
+ status: "compatible"
4637
+ });
4638
+ }
4639
+ }
4640
+ } else if (resolved.length === 2) {
4641
+ edges.push({
4642
+ a: resolved[0].name,
4643
+ aVersion: resolved[0].version,
4644
+ b: resolved[1].name,
4645
+ bVersion: resolved[1].version,
4646
+ status: "conflict"
4647
+ });
4648
+ }
4649
+ return edges;
4650
+ }
4651
+ async function verifyCompatibility(db, packages2, opts = {}) {
4652
+ const resolved = await Promise.all(
4653
+ packages2.map(async (name) => ({
4654
+ name,
4655
+ version: (await getPackageByName(db, name))?.latestVersion ?? "latest"
4656
+ }))
4657
+ );
4658
+ const result = await getSandbox().verifySet(
4659
+ resolved.map((r) => ({ name: r.name, version: r.version === "latest" ? null : r.version })),
4660
+ { allowScripts: opts.allowScripts }
4661
+ );
4662
+ const edges = deriveCompatEdges(resolved, result);
4663
+ for (const e of edges) {
4664
+ const pair = canonicalPair(
4665
+ { name: e.a, version: e.aVersion },
4666
+ { name: e.b, version: e.bVersion }
4667
+ );
4668
+ await upsertCompatEdge(db, {
4669
+ ...pair,
4670
+ status: e.status,
4671
+ driver: result.driver,
4672
+ ranAt: /* @__PURE__ */ new Date()
4673
+ }).catch(() => {
4674
+ });
4675
+ }
4676
+ const failed = !result.installed || !result.loaded.every((l) => l.loaded === true);
4677
+ return { result, edges, unattributedConflict: failed && edges.length === 0 };
4678
+ }
4679
+ var init_compat2 = __esm({
4680
+ "src/pipeline/compat.ts"() {
4681
+ "use strict";
4682
+ init_esm_shims();
4683
+ init_compat();
4684
+ init_packages();
4685
+ init_sandbox();
4686
+ }
4687
+ });
4688
+
4689
+ // src/cli/commands.ts
4690
+ var commands_exports = {};
4691
+ __export(commands_exports, {
4692
+ runCompare: () => runCompare,
4693
+ runCompat: () => runCompat,
4694
+ runEditWeights: () => runEditWeights,
4695
+ runEvaluate: () => runEvaluate,
4696
+ runPlan: () => runPlan,
2319
4697
  runRecommend: () => runRecommend,
2320
- runVerify: () => runVerify
4698
+ runSandbox: () => runSandbox,
4699
+ runVerify: () => runVerify,
4700
+ runVersions: () => runVersions,
4701
+ runWatch: () => runWatch,
4702
+ runWeights: () => runWeights
2321
4703
  });
2322
4704
  async function withDb(fn) {
2323
4705
  requireConfig(["DATABASE_URL"]);
@@ -2345,10 +4727,11 @@ async function runRecommend(need, opts) {
2345
4727
  }
2346
4728
  console.log(
2347
4729
  table(
2348
- ["Package", "Health", "Confidence", "Weekly", "Latest", "Category"],
4730
+ ["Package", "Health", "Quality", "Confidence", "Weekly", "Latest", "Category"],
2349
4731
  res.candidates.map((c) => [
2350
4732
  c.name,
2351
4733
  String(c.healthScore),
4734
+ c.qualityScore != null ? String(c.qualityScore) : "\u2014",
2352
4735
  confidenceLabel(c.confidence),
2353
4736
  formatNumber(c.weeklyDownloads),
2354
4737
  c.latestVersion ?? "\u2014",
@@ -2373,9 +4756,10 @@ async function runEvaluate(pkg, opts) {
2373
4756
  console.log(
2374
4757
  detail([
2375
4758
  ["health", `${res.healthScore} ${confidenceLabel(res.confidence)}`],
4759
+ ["quality", res.qualityScore != null ? String(res.qualityScore) : "\u2014"],
2376
4760
  [
2377
4761
  "breakdown",
2378
- `maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"}`
4762
+ `maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"} \xB7 qual ${b.quality ?? "\u2014"}`
2379
4763
  ],
2380
4764
  ["version", res.latestVersion ?? "\u2014"],
2381
4765
  ["weekly dl", `${formatNumber(res.weeklyDownloads)} (${formatPercent(res.downloadGrowth90d)} 90d)`],
@@ -2387,6 +4771,12 @@ async function runEvaluate(pkg, opts) {
2387
4771
  ["repo", res.repoUrl ?? "\u2014"]
2388
4772
  ])
2389
4773
  );
4774
+ if (res.buildVerified) {
4775
+ const bv = res.buildVerified;
4776
+ const state = !bv.installed ? red("install failed") : bv.loaded === false ? yellow("installs, load failed") : green("installs and loads");
4777
+ console.log(`
4778
+ sandbox: ${state} ${dim(`${bv.version} \xB7 ${bv.driver}`)}`);
4779
+ }
2390
4780
  if (res.summary) console.log("\n" + res.summary);
2391
4781
  if (res.usageGuide) {
2392
4782
  const g = res.usageGuide;
@@ -2424,8 +4814,148 @@ async function runCompare(pkgs, opts) {
2424
4814
  ])
2425
4815
  )
2426
4816
  );
2427
- if (res.missing?.length) console.log(dim(`
2428
- not found: ${res.missing.join(", ")}`));
4817
+ if (res.missing?.length) console.log(dim(`
4818
+ not found: ${res.missing.join(", ")}`));
4819
+ console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
4820
+ });
4821
+ }
4822
+ function runWeights(opts = {}) {
4823
+ const w = loadWeights();
4824
+ const active = activeWeightsPath();
4825
+ if (opts.json) {
4826
+ console.log(JSON.stringify({ ...w, source: active?.source ?? "defaults" }, null, 2));
4827
+ return;
4828
+ }
4829
+ const pct = (n) => n.toFixed(2);
4830
+ console.log(bold("Two axes, blended for default sort:"));
4831
+ console.log(` composite = (1\u2212\u03BB)\xB7health + \u03BB\xB7quality \u03BB = ${pct(w.composite.lambda)}
4832
+ `);
4833
+ console.log(bold("Health (proven-ness) \u2014 weighted sum of 4 components:"));
4834
+ console.log(
4835
+ detail([
4836
+ ["maintenance", `${pct(w.health.maintenance)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.maintenance)}`],
4837
+ ["adoption", `${pct(w.health.adoption)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.adoption)}`],
4838
+ ["reliability", `${pct(w.health.reliability)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.reliability)}`],
4839
+ ["efficiency", `${pct(w.health.efficiency)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.efficiency)}`]
4840
+ ])
4841
+ );
4842
+ console.log("\n" + bold("Quality (intrinsic, adoption-independent):"));
4843
+ console.log(" " + dim(Object.keys(QUALITY_WEIGHTS).join(", ")));
4844
+ console.log("\n" + bold("Confidence thresholds:"));
4845
+ console.log(
4846
+ detail([
4847
+ ["proven", `\u2265 ${formatNumber(CONFIDENCE.proven.minWeeklyDownloads)} weekly dl, \u2265 ${CONFIDENCE.proven.minAgeMonths}mo old`],
4848
+ ["emerging", `\u2265 ${formatNumber(CONFIDENCE.emerging.minWeeklyDownloads)} weekly dl OR \u2265 ${CONFIDENCE.emerging.strongGrowth * 100}% 90d growth`],
4849
+ ["promising", `\u2265 ${CONFIDENCE.promising.minQuality} quality score (adoption-independent)`]
4850
+ ])
4851
+ );
4852
+ console.log(dim(`
4853
+ Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
4854
+ }
4855
+ async function runEditWeights(opts) {
4856
+ const { invalidateCache: invalidateCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
4857
+ if (opts.reset) {
4858
+ const removed = resetWeights();
4859
+ console.log(removed.length ? `Removed overrides:
4860
+ ${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
4861
+ if (removed.length) await invalidateCache2();
4862
+ return;
4863
+ }
4864
+ if (opts.explain) {
4865
+ const key = opts.explain;
4866
+ const text2 = WEIGHT_EXPLANATIONS[key];
4867
+ if (!text2) {
4868
+ throw new Error(`No explanation for "${key}". Known: ${Object.keys(WEIGHT_EXPLANATIONS).join(", ")}.`);
4869
+ }
4870
+ console.log(`${bold(key)} \u2014 ${text2}`);
4871
+ return;
4872
+ }
4873
+ if (opts.set && opts.set.length > 0) {
4874
+ const next = applyOverrides(loadWeights(), opts.set);
4875
+ const { weights, normalized } = validateWeights(next);
4876
+ const path2 = saveWeights(weights, opts.project ? "project" : "user");
4877
+ await invalidateCache2();
4878
+ console.log(`Saved overrides to ${path2}`);
4879
+ if (normalized) {
4880
+ console.log(
4881
+ 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)}`
4882
+ );
4883
+ }
4884
+ console.log(dim("\nRun `lurq rescore` to apply the new health weights to the stored index."));
4885
+ return;
4886
+ }
4887
+ console.log(dim(`No changes. Settable keys: ${settableKeys().join(", ")}.
4888
+ `));
4889
+ runWeights(opts);
4890
+ }
4891
+ async function openInBrowser(target) {
4892
+ const { spawn } = await import("child_process");
4893
+ const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", target]] : ["xdg-open", [target]];
4894
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
4895
+ }
4896
+ async function runPlan(file, opts) {
4897
+ const { readFileSync: readFileSync4 } = await import("fs");
4898
+ let document;
4899
+ try {
4900
+ document = readFileSync4(file, "utf8");
4901
+ } catch {
4902
+ throw new Error(`Could not read "${file}".`);
4903
+ }
4904
+ if (opts.optimize && opts.optimize !== "speed" && opts.optimize !== "balanced") {
4905
+ throw new Error("--optimize must be 'speed' or 'balanced'.");
4906
+ }
4907
+ await withDb(async (db) => {
4908
+ const { handlePlan: handlePlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
4909
+ const res = await handlePlan2(db, {
4910
+ document,
4911
+ optimize: opts.optimize
4912
+ });
4913
+ if (opts.json) return console.log(JSON.stringify(res, null, 2));
4914
+ if (!("slots" in res)) {
4915
+ console.log(res.note);
4916
+ return;
4917
+ }
4918
+ if (opts.html || opts.open) {
4919
+ const { writeFileSync: writeFileSync3 } = await import("fs");
4920
+ const { tmpdir: tmpdir2 } = await import("os");
4921
+ const { join: join5 } = await import("path");
4922
+ const { renderPlanHtml: renderPlanHtml2 } = await Promise.resolve().then(() => (init_planView(), planView_exports));
4923
+ const out = opts.html ?? join5(tmpdir2(), `lurq-plan-${Date.now()}.html`);
4924
+ writeFileSync3(out, renderPlanHtml2(res), "utf8");
4925
+ console.log(`Roadmap written to ${out}`);
4926
+ if (opts.open) await openInBrowser(out);
4927
+ }
4928
+ console.log(
4929
+ table(
4930
+ ["Component", "Layer", "Recommended", "Health", "Confidence", "Alternatives"],
4931
+ res.slots.map((s) => [
4932
+ s.need.length > 32 ? s.need.slice(0, 31) + "\u2026" : s.need,
4933
+ s.layer,
4934
+ s.recommended ? `${s.recommended.name}@${s.recommended.latestVersion ?? "?"}` : dim("\u2014"),
4935
+ s.recommended ? String(s.recommended.healthScore) : "\u2014",
4936
+ s.recommended ? confidenceLabel(s.recommended.confidence) : "\u2014",
4937
+ s.alternatives.map((a) => a.name).join(", ") || "\u2014"
4938
+ ])
4939
+ )
4940
+ );
4941
+ if (res.unmatched.length) console.log(yellow(`
4942
+ No match for: ${res.unmatched.join(", ")}`));
4943
+ if (res.compatibility) {
4944
+ const c = res.compatibility;
4945
+ const col = c.overall === "compatible" ? green : c.overall === "conflict" ? red : dim;
4946
+ console.log("\n" + bold("Compatibility: ") + col(c.overall));
4947
+ for (const s of res.slots) {
4948
+ if (s.swappedFrom && s.recommended) {
4949
+ console.log(green(` \u2713 swapped ${s.swappedFrom} \u2192 ${s.recommended.name} for compatibility`));
4950
+ }
4951
+ }
4952
+ for (const cf of c.conflicts) console.log(red(` \u2717 ${cf.detail} (no compatible alternative)`));
4953
+ if (c.unverified.length) console.log(dim(` unverified: ${c.unverified.join(", ")}`));
4954
+ }
4955
+ console.log("\n" + bold("Roadmap (Mermaid):"));
4956
+ console.log(res.mermaid);
4957
+ console.log(dim(`
4958
+ ${res.note}`));
2429
4959
  console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
2430
4960
  });
2431
4961
  }
@@ -2433,7 +4963,8 @@ async function runVerify(pkg, opts) {
2433
4963
  await withDb(async (db) => {
2434
4964
  const res = await handleVerify(db, { package: pkg });
2435
4965
  if (opts.json) return console.log(JSON.stringify(res, null, 2));
2436
- const verdict = !res.exists ? red("\u2717 NOT FOUND on npm") : res.deprecated || res.archived || res.advisoryCount > 0 ? yellow("\u26A0 exists, but risky") : green("\u2713 looks safe");
4966
+ const riskColor = res.risk === "high" ? red : res.risk === "medium" ? yellow : green;
4967
+ const verdict = !res.exists ? red("\u2717 NOT FOUND on npm") : res.risk === "high" ? red("\u2717 high supply-chain risk") : res.risk === "medium" ? yellow("\u26A0 exists, but risky") : green("\u2713 looks safe");
2437
4968
  console.log(`${bold(pkg)} ${verdict}`);
2438
4969
  console.log(
2439
4970
  detail([
@@ -2441,11 +4972,107 @@ async function runVerify(pkg, opts) {
2441
4972
  ["weekly dl", formatNumber(res.weeklyDownloads)],
2442
4973
  ["confidence", res.confidence ? confidenceLabel(res.confidence) : "\u2014"],
2443
4974
  ["advisories", String(res.advisoryCount)],
4975
+ ["risk", riskColor(res.risk)],
2444
4976
  ["risk flags", res.riskFlags.length ? yellow(res.riskFlags.join(", ")) : "none"]
2445
4977
  ])
2446
4978
  );
2447
4979
  });
2448
4980
  }
4981
+ async function runVersions(pkg, opts) {
4982
+ const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10) || 30) : 30;
4983
+ await withDb(async (db) => {
4984
+ const versions = await getPackageVersions(db, pkg, limit);
4985
+ if (opts.json) return console.log(JSON.stringify(versions, null, 2));
4986
+ if (versions.length === 0) {
4987
+ console.log(`No stored versions for ${bold(pkg)}. Run \`lurq sync --package ${pkg}\` first.`);
4988
+ return;
4989
+ }
4990
+ console.log(bold(pkg));
4991
+ console.log(
4992
+ table(
4993
+ ["Version", "Published"],
4994
+ versions.map((v) => [
4995
+ v.version,
4996
+ v.publishedAt ? v.publishedAt.toISOString().slice(0, 10) : "\u2014"
4997
+ ])
4998
+ )
4999
+ );
5000
+ });
5001
+ }
5002
+ async function runWatch() {
5003
+ const { watchNpmChanges: watchNpmChanges2 } = await Promise.resolve().then(() => (init_watch2(), watch_exports));
5004
+ await withDb(async (db) => {
5005
+ const controller = new AbortController();
5006
+ const stop = () => controller.abort();
5007
+ process.once("SIGINT", stop);
5008
+ process.once("SIGTERM", stop);
5009
+ console.log(dim("watching npm for releases of tracked packages \u2014 Ctrl-C to stop"));
5010
+ try {
5011
+ await watchNpmChanges2(db, { signal: controller.signal });
5012
+ } finally {
5013
+ process.off("SIGINT", stop);
5014
+ process.off("SIGTERM", stop);
5015
+ }
5016
+ });
5017
+ }
5018
+ async function runSandbox(pkg, version, opts) {
5019
+ if (opts.allowScripts) {
5020
+ console.error(
5021
+ yellow("warning: running install scripts and loading the package locally without isolation")
5022
+ );
5023
+ }
5024
+ const { verifyPackageInSandbox: verifyPackageInSandbox2 } = await Promise.resolve().then(() => (init_sandbox2(), sandbox_exports));
5025
+ await withDb(async (db) => {
5026
+ const result = await verifyPackageInSandbox2(db, pkg, version ?? null, {
5027
+ target: { node: "20", moduleSystem: opts.esm ? "esm" : "cjs" },
5028
+ allowScripts: opts.allowScripts
5029
+ });
5030
+ if (opts.json) return console.log(JSON.stringify(result, null, 2));
5031
+ const ok = result.installed && result.imported !== false;
5032
+ const label = version ? `${pkg}@${version}` : pkg;
5033
+ const verdict = ok ? green("\u2713 installs and loads") : red("\u2717 failed");
5034
+ console.log(`${bold(label)} ${verdict} ${dim(`(${result.durationMs}ms \xB7 ${result.driver})`)}`);
5035
+ console.log(
5036
+ detail([
5037
+ ["installed", result.installed ? "yes" : "no"],
5038
+ ["loaded", result.imported === null ? "\u2014" : result.imported ? "yes" : "no"],
5039
+ ["module", result.moduleSystem],
5040
+ ["scripts", result.ranScripts ? "ran" : "skipped"],
5041
+ ["error", result.error ?? "none"]
5042
+ ])
5043
+ );
5044
+ });
5045
+ }
5046
+ async function runCompat(pkgs, opts) {
5047
+ await withDb(async (db) => {
5048
+ if (opts.run) {
5049
+ console.error(
5050
+ yellow("co-installing in the sandbox (loads package code locally without isolation)")
5051
+ );
5052
+ const { verifyCompatibility: verifyCompatibility2 } = await Promise.resolve().then(() => (init_compat2(), compat_exports));
5053
+ await verifyCompatibility2(db, pkgs);
5054
+ }
5055
+ const { handleCompat: handleCompat2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
5056
+ const res = await handleCompat2(db, { packages: pkgs });
5057
+ if (opts.json) return console.log(JSON.stringify(res, null, 2));
5058
+ const color = res.overall === "compatible" ? green : res.overall === "conflict" ? red : dim;
5059
+ console.log(`${bold(res.packages.join(" + "))} ${color(res.overall)}`);
5060
+ if (res.conflicts.length) {
5061
+ console.log(
5062
+ table(
5063
+ ["Source", "Detail"],
5064
+ res.conflicts.map((c) => [c.source, c.detail])
5065
+ )
5066
+ );
5067
+ } else if (res.overall === "compatible") {
5068
+ console.log(dim("no peer-dependency or engine conflicts across the set"));
5069
+ }
5070
+ if (res.unverified.length) {
5071
+ console.log(dim(`
5072
+ unverified (no metadata): ${res.unverified.join(", ")}`));
5073
+ }
5074
+ });
5075
+ }
2449
5076
  var init_commands = __esm({
2450
5077
  "src/cli/commands.ts"() {
2451
5078
  "use strict";
@@ -2453,58 +5080,35 @@ var init_commands = __esm({
2453
5080
  init_config();
2454
5081
  init_types();
2455
5082
  init_client();
5083
+ init_packages();
2456
5084
  init_handlers();
5085
+ init_weights();
5086
+ init_weights();
2457
5087
  init_format();
2458
5088
  }
2459
5089
  });
2460
5090
 
2461
- // src/core/paths.ts
2462
- import { existsSync as existsSync2 } from "fs";
2463
- import { dirname as dirname2, join as join2 } from "path";
2464
- import { fileURLToPath as fileURLToPath2 } from "url";
2465
- function packageRoot() {
2466
- if (cachedRoot) return cachedRoot;
2467
- let dir = dirname2(fileURLToPath2(import.meta.url));
2468
- for (; ; ) {
2469
- if (existsSync2(join2(dir, "package.json"))) {
2470
- cachedRoot = dir;
2471
- return dir;
2472
- }
2473
- const parent = dirname2(dir);
2474
- if (parent === dir) break;
2475
- dir = parent;
2476
- }
2477
- cachedRoot = process.cwd();
2478
- return cachedRoot;
2479
- }
2480
- function migrationsDir() {
2481
- return join2(packageRoot(), "drizzle");
2482
- }
2483
- function seedJsonPath() {
2484
- return join2(packageRoot(), "src", "data", "seed.json");
2485
- }
2486
- var cachedRoot;
2487
- var init_paths = __esm({
2488
- "src/core/paths.ts"() {
2489
- "use strict";
2490
- init_esm_shims();
2491
- }
2492
- });
2493
-
2494
5091
  // src/cli/installSkill.ts
2495
5092
  var installSkill_exports = {};
2496
5093
  __export(installSkill_exports, {
2497
5094
  SUPPORTED_AGENTS: () => SUPPORTED_AGENTS,
5095
+ agentSpecs: () => agentSpecs,
5096
+ buildRemoteServerEntry: () => buildRemoteServerEntry,
5097
+ buildRemoteTomlBlock: () => buildRemoteTomlBlock,
2498
5098
  buildServerEntry: () => buildServerEntry,
2499
5099
  buildTomlBlock: () => buildTomlBlock,
5100
+ installAgent: () => installAgent,
5101
+ installInstructionsFile: () => installInstructionsFile,
5102
+ printInstallReport: () => printInstallReport,
5103
+ resolveAgents: () => resolveAgents,
2500
5104
  runInstallSkill: () => runInstallSkill
2501
5105
  });
2502
- import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
5106
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2503
5107
  import { copyFileSync } from "fs";
2504
- import { homedir as homedir2 } from "os";
2505
- import { dirname as dirname3, join as join3 } from "path";
5108
+ import { homedir as homedir3 } from "os";
5109
+ import { dirname as dirname4, join as join4 } from "path";
2506
5110
  function home(...p) {
2507
- return join3(homedir2(), ...p);
5111
+ return join4(homedir3(), ...p);
2508
5112
  }
2509
5113
  function agentSpecs() {
2510
5114
  return [
@@ -2513,35 +5117,35 @@ function agentSpecs() {
2513
5117
  label: "Claude Code",
2514
5118
  format: "mcpServers",
2515
5119
  path: home(".claude.json"),
2516
- detected: existsSync3(home(".claude.json")) || existsSync3(home(".claude"))
5120
+ detected: existsSync4(home(".claude.json")) || existsSync4(home(".claude"))
2517
5121
  },
2518
5122
  {
2519
5123
  id: "cursor",
2520
5124
  label: "Cursor",
2521
5125
  format: "mcpServers",
2522
5126
  path: home(".cursor", "mcp.json"),
2523
- detected: existsSync3(home(".cursor"))
5127
+ detected: existsSync4(home(".cursor"))
2524
5128
  },
2525
5129
  {
2526
5130
  id: "windsurf",
2527
5131
  label: "Windsurf",
2528
5132
  format: "mcpServers",
2529
5133
  path: home(".codeium", "windsurf", "mcp_config.json"),
2530
- detected: existsSync3(home(".codeium"))
5134
+ detected: existsSync4(home(".codeium"))
2531
5135
  },
2532
5136
  {
2533
5137
  id: "copilot",
2534
5138
  label: "VS Code / GitHub Copilot",
2535
5139
  format: "servers",
2536
5140
  path: home("Library", "Application Support", "Code", "User", "mcp.json"),
2537
- detected: existsSync3(home("Library", "Application Support", "Code", "User"))
5141
+ detected: existsSync4(home("Library", "Application Support", "Code", "User"))
2538
5142
  },
2539
5143
  {
2540
5144
  id: "codex",
2541
5145
  label: "OpenAI Codex CLI",
2542
5146
  format: "toml",
2543
5147
  path: home(".codex", "config.toml"),
2544
- detected: existsSync3(home(".codex"))
5148
+ detected: existsSync4(home(".codex"))
2545
5149
  }
2546
5150
  ];
2547
5151
  }
@@ -2554,14 +5158,14 @@ function collectEnv() {
2554
5158
  return env;
2555
5159
  }
2556
5160
  function readJsonObject(path2) {
2557
- if (!existsSync3(path2)) return {};
2558
- const text2 = readFileSync(path2, "utf8").trim();
5161
+ if (!existsSync4(path2)) return {};
5162
+ const text2 = readFileSync2(path2, "utf8").trim();
2559
5163
  if (!text2) return {};
2560
5164
  return JSON.parse(text2);
2561
5165
  }
2562
5166
  function writeJson(path2, obj) {
2563
- mkdirSync(dirname3(path2), { recursive: true });
2564
- writeFileSync(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
5167
+ mkdirSync2(dirname4(path2), { recursive: true });
5168
+ writeFileSync2(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
2565
5169
  }
2566
5170
  function buildServerEntry(env, withType) {
2567
5171
  const entry = { command: "npx", args: ["-y", PACKAGE_NAME, "serve"] };
@@ -2569,13 +5173,18 @@ function buildServerEntry(env, withType) {
2569
5173
  if (Object.keys(env).length) entry.env = env;
2570
5174
  return entry;
2571
5175
  }
2572
- function installJson(spec, env) {
2573
- const key = spec.format === "servers" ? "servers" : "mcpServers";
2574
- const config = readJsonObject(spec.path);
2575
- if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
2576
- config[key].lurq = buildServerEntry(env, spec.format === "servers");
2577
- writeJson(spec.path, config);
2578
- return { agent: spec.id, path: spec.path, status: "installed" };
5176
+ function buildRemoteServerEntry(agentId, opts) {
5177
+ const headers = { Authorization: `Bearer ${opts.apiKey}` };
5178
+ switch (agentId) {
5179
+ case "cursor":
5180
+ return { url: opts.url, headers };
5181
+ case "windsurf":
5182
+ return { serverUrl: opts.url, headers };
5183
+ case "claude-code":
5184
+ case "copilot":
5185
+ default:
5186
+ return { type: "http", url: opts.url, headers };
5187
+ }
2579
5188
  }
2580
5189
  function buildTomlBlock(env) {
2581
5190
  const lines = ["[mcp_servers.lurq]", 'command = "npx"', `args = ["-y", "${PACKAGE_NAME}", "serve"]`];
@@ -2585,8 +5194,23 @@ function buildTomlBlock(env) {
2585
5194
  }
2586
5195
  return lines.join("\n") + "\n";
2587
5196
  }
2588
- function installToml(spec, env) {
2589
- const existing = existsSync3(spec.path) ? readFileSync(spec.path, "utf8") : "";
5197
+ function buildRemoteTomlBlock(opts) {
5198
+ return [
5199
+ "[mcp_servers.lurq]",
5200
+ `url = ${JSON.stringify(opts.url)}`,
5201
+ `http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.apiKey}`)} }`
5202
+ ].join("\n") + "\n";
5203
+ }
5204
+ function installJsonEntry(spec, entry) {
5205
+ const key = spec.format === "servers" ? "servers" : "mcpServers";
5206
+ const config = readJsonObject(spec.path);
5207
+ if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
5208
+ config[key].lurq = entry;
5209
+ writeJson(spec.path, config);
5210
+ return { agent: spec.id, path: spec.path, status: "installed" };
5211
+ }
5212
+ function installTomlBlock(spec, block) {
5213
+ const existing = existsSync4(spec.path) ? readFileSync2(spec.path, "utf8") : "";
2590
5214
  if (existing.includes("[mcp_servers.lurq]")) {
2591
5215
  return {
2592
5216
  agent: spec.id,
@@ -2595,59 +5219,48 @@ function installToml(spec, env) {
2595
5219
  message: "lurq already present; edit manually to change it."
2596
5220
  };
2597
5221
  }
2598
- mkdirSync(dirname3(spec.path), { recursive: true });
5222
+ mkdirSync2(dirname4(spec.path), { recursive: true });
2599
5223
  const sep = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
2600
- writeFileSync(spec.path, existing + sep + buildTomlBlock(env), "utf8");
5224
+ writeFileSync2(spec.path, existing + sep + block, "utf8");
2601
5225
  return { agent: spec.id, path: spec.path, status: "installed" };
2602
5226
  }
5227
+ function installAgent(spec, mode) {
5228
+ try {
5229
+ if (spec.format === "toml") {
5230
+ const block = mode.kind === "remote" ? buildRemoteTomlBlock(mode) : buildTomlBlock(mode.env);
5231
+ return installTomlBlock(spec, block);
5232
+ }
5233
+ const entry = mode.kind === "remote" ? buildRemoteServerEntry(spec.id, mode) : buildServerEntry(mode.env, spec.format === "servers");
5234
+ return installJsonEntry(spec, entry);
5235
+ } catch (err) {
5236
+ return {
5237
+ agent: spec.id,
5238
+ path: spec.path,
5239
+ status: "error",
5240
+ message: err instanceof Error ? err.message : String(err)
5241
+ };
5242
+ }
5243
+ }
2603
5244
  function installInstructionsFile() {
2604
- const src = join3(packageRoot(), "templates", "skill-instructions.md");
2605
- if (!existsSync3(src)) return null;
5245
+ const src = join4(packageRoot(), "templates", "skill-instructions.md");
5246
+ if (!existsSync4(src)) return null;
2606
5247
  const destDir = home(".lurq");
2607
- const dest = join3(destDir, "skill-instructions.md");
2608
- mkdirSync(destDir, { recursive: true });
5248
+ const dest = join4(destDir, "skill-instructions.md");
5249
+ mkdirSync2(destDir, { recursive: true });
2609
5250
  copyFileSync(src, dest);
2610
5251
  return dest;
2611
5252
  }
2612
- async function runInstallSkill(opts) {
2613
- const target = opts.agent ?? "claude-code";
5253
+ function resolveAgents(target) {
2614
5254
  const specs = agentSpecs();
2615
- const env = collectEnv();
2616
- let selected;
2617
- if (target === "all") {
2618
- selected = specs.filter((s) => s.detected);
2619
- if (selected.length === 0) {
2620
- console.log("No supported agents detected on this machine.");
2621
- return;
2622
- }
2623
- } else {
2624
- const spec = specs.find((s) => s.id === target);
2625
- if (!spec) {
2626
- throw new Error(
2627
- `Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`
2628
- );
2629
- }
2630
- selected = [spec];
2631
- }
2632
- if (!env.DATABASE_URL) {
2633
- logger.warn(
2634
- "DATABASE_URL is not set in the current environment \u2014 the installed server entry will have no DATABASE_URL. Set it (in .env) and re-run, or edit the config."
2635
- );
2636
- }
2637
- const results = [];
2638
- for (const spec of selected) {
2639
- try {
2640
- results.push(spec.format === "toml" ? installToml(spec, env) : installJson(spec, env));
2641
- } catch (err) {
2642
- results.push({
2643
- agent: spec.id,
2644
- path: spec.path,
2645
- status: "error",
2646
- message: err instanceof Error ? err.message : String(err)
2647
- });
2648
- }
5255
+ if (target === "all") return specs.filter((s) => s.detected);
5256
+ const spec = specs.find((s) => s.id === target);
5257
+ if (!spec) {
5258
+ throw new Error(`Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`);
2649
5259
  }
2650
- const instructionsPath = installInstructionsFile();
5260
+ return [spec];
5261
+ }
5262
+ function printInstallReport(results, instructionsPath, mode) {
5263
+ const specs = agentSpecs();
2651
5264
  console.log("lurq MCP server registration:");
2652
5265
  for (const r of results) {
2653
5266
  const spec = specs.find((s) => s.id === r.agent);
@@ -2657,9 +5270,43 @@ async function runInstallSkill(opts) {
2657
5270
  if (instructionsPath) console.log(`
2658
5271
  Skill instructions written to ${instructionsPath}`);
2659
5272
  console.log("\nNext steps:");
2660
- console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
2661
- console.log(" 2. Restart the agent so it picks up the new MCP server.");
2662
- console.log(" 3. Ask it to recommend a library \u2014 it should call lurq.");
5273
+ if (mode.kind === "local") {
5274
+ console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
5275
+ console.log(" 2. Restart the agent so it picks up the new MCP server.");
5276
+ } else {
5277
+ console.log(" 1. Restart the agent so it picks up the new MCP server.");
5278
+ }
5279
+ console.log(" \u2022 Ask it to recommend a library \u2014 it should call lurq.");
5280
+ }
5281
+ async function runInstallSkill(opts) {
5282
+ const selected = resolveAgents(opts.agent ?? "claude-code");
5283
+ if (selected.length === 0) {
5284
+ console.log("No supported agents detected on this machine.");
5285
+ return;
5286
+ }
5287
+ const remote = !opts.local;
5288
+ let mode;
5289
+ if (remote) {
5290
+ const apiKey = opts.apiKey ?? process.env.LURQ_API_KEY;
5291
+ if (!apiKey) {
5292
+ throw new Error(
5293
+ "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."
5294
+ );
5295
+ }
5296
+ const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
5297
+ mode = { kind: "remote", url, apiKey };
5298
+ } else {
5299
+ const env = collectEnv();
5300
+ if (!env.DATABASE_URL) {
5301
+ logger.warn(
5302
+ "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."
5303
+ );
5304
+ }
5305
+ mode = { kind: "local", env };
5306
+ }
5307
+ const results = selected.map((spec) => installAgent(spec, mode));
5308
+ const instructionsPath = installInstructionsFile();
5309
+ printInstallReport(results, instructionsPath, mode);
2663
5310
  }
2664
5311
  var ENV_KEYS, SUPPORTED_AGENTS;
2665
5312
  var init_installSkill = __esm({
@@ -2683,12 +5330,279 @@ var init_installSkill = __esm({
2683
5330
  }
2684
5331
  });
2685
5332
 
5333
+ // src/cli/install.ts
5334
+ var install_exports = {};
5335
+ __export(install_exports, {
5336
+ runInstallWizard: () => runInstallWizard
5337
+ });
5338
+ async function validateKey(url, apiKey) {
5339
+ try {
5340
+ const res = await fetch(url, {
5341
+ method: "POST",
5342
+ headers: {
5343
+ "Content-Type": "application/json",
5344
+ Accept: "application/json, text/event-stream",
5345
+ Authorization: `Bearer ${apiKey}`
5346
+ },
5347
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
5348
+ });
5349
+ if (res.ok) return "valid";
5350
+ if (res.status === 401 || res.status === 403) return "invalid";
5351
+ return "unreachable";
5352
+ } catch {
5353
+ return "unreachable";
5354
+ }
5355
+ }
5356
+ async function runInstallWizard(opts) {
5357
+ const interactive = !opts.yes;
5358
+ const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
5359
+ let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
5360
+ if (interactive) {
5361
+ const { input, checkbox, confirm, select } = await import("@inquirer/prompts");
5362
+ console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
5363
+ if (!apiKey) {
5364
+ console.log(` Need a key? Get one at ${GET_KEY_URL}
5365
+ `);
5366
+ apiKey = (await input({
5367
+ message: "Paste your lurq API key",
5368
+ validate: (v) => v.trim().startsWith("lurq_") ? true : "Keys look like lurq_live_\u2026 "
5369
+ })).trim();
5370
+ }
5371
+ process.stdout.write(" Validating key\u2026 ");
5372
+ const check = await validateKey(url, apiKey);
5373
+ console.log(
5374
+ check === "valid" ? "ok" : check === "invalid" ? "rejected" : "could not reach endpoint"
5375
+ );
5376
+ if (check !== "valid") {
5377
+ const proceed = await confirm({
5378
+ message: check === "invalid" ? `That key was rejected (401) by ${url}. Continue anyway?` : `Couldn't reach ${url} to validate the key. Continue anyway?`,
5379
+ default: false
5380
+ });
5381
+ if (!proceed) {
5382
+ console.log("Aborted. No config was changed.");
5383
+ return;
5384
+ }
5385
+ }
5386
+ if (opts.agent) {
5387
+ await finish(resolveAgents(opts.agent), { url, apiKey });
5388
+ return;
5389
+ }
5390
+ const specs = agentSpecs();
5391
+ const detected = specs.filter((s) => s.detected);
5392
+ const primary = detected.find((s) => s.id === "claude-code") ?? detected[0];
5393
+ if (primary) {
5394
+ const others = detected.length - 1;
5395
+ const choice = await select({
5396
+ message: others > 0 ? `Looks like you have ${detected.map((s) => s.label).join(", ")}. Connect lurq to them?` : `Looks like you have ${primary.label}. Connect lurq to it?`,
5397
+ default: "yes",
5398
+ choices: [
5399
+ {
5400
+ name: others > 0 ? `Yes \u2014 set up all ${detected.length} detected agents` : `Yes \u2014 set up ${primary.label} for me`,
5401
+ value: "yes"
5402
+ },
5403
+ { name: "No \u2014 let me choose which agent(s)", value: "other" },
5404
+ { name: "Cancel \u2014 change nothing", value: "cancel" }
5405
+ ]
5406
+ });
5407
+ if (choice === "cancel") {
5408
+ console.log("No problem \u2014 nothing was changed. Run `lurq install` again anytime.");
5409
+ return;
5410
+ }
5411
+ if (choice === "yes") {
5412
+ await finish(detected, { url, apiKey });
5413
+ return;
5414
+ }
5415
+ }
5416
+ const ids = await checkbox({
5417
+ message: "Which assistant(s) should I configure?",
5418
+ choices: specs.map((s) => ({
5419
+ name: `${s.label}${s.detected ? " (detected)" : ""}`,
5420
+ value: s.id,
5421
+ checked: s.detected
5422
+ }))
5423
+ });
5424
+ await finish(specs.filter((s) => ids.includes(s.id)), { url, apiKey });
5425
+ return;
5426
+ }
5427
+ if (!apiKey) {
5428
+ throw new Error(
5429
+ "No API key. Pass --api-key <key> or set LURQ_API_KEY (or drop --yes to be prompted)."
5430
+ );
5431
+ }
5432
+ const selected = opts.agent ? resolveAgents(opts.agent) : agentSpecs().filter((s) => s.detected);
5433
+ await finish(selected, { url, apiKey });
5434
+ }
5435
+ async function finish(selected, remote) {
5436
+ if (selected.length === 0) {
5437
+ console.log(
5438
+ "\nNo agents selected or detected. Re-run with --agent <id>, or install an assistant first."
5439
+ );
5440
+ return;
5441
+ }
5442
+ const mode = { kind: "remote", ...remote };
5443
+ const results = selected.map((s) => installAgent(s, mode));
5444
+ const instructionsPath = installInstructionsFile();
5445
+ console.log("");
5446
+ printInstallReport(results, instructionsPath, mode);
5447
+ }
5448
+ var GET_KEY_URL;
5449
+ var init_install = __esm({
5450
+ "src/cli/install.ts"() {
5451
+ "use strict";
5452
+ init_esm_shims();
5453
+ init_constants();
5454
+ init_installSkill();
5455
+ GET_KEY_URL = "https://lurq.run";
5456
+ }
5457
+ });
5458
+
5459
+ // src/cli/keys.ts
5460
+ var keys_exports = {};
5461
+ __export(keys_exports, {
5462
+ runKeysCreate: () => runKeysCreate,
5463
+ runKeysList: () => runKeysList,
5464
+ runKeysRevoke: () => runKeysRevoke,
5465
+ runKeysRotate: () => runKeysRotate
5466
+ });
5467
+ function waitForEnter(prompt) {
5468
+ return new Promise((resolve) => {
5469
+ process.stdout.write(prompt);
5470
+ process.stdin.resume();
5471
+ process.stdin.once("data", () => {
5472
+ process.stdin.pause();
5473
+ resolve();
5474
+ });
5475
+ });
5476
+ }
5477
+ async function presentNewKey(key, row, opts) {
5478
+ if (opts.json) {
5479
+ console.log(
5480
+ JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label, ...opts.extraJson })
5481
+ );
5482
+ return;
5483
+ }
5484
+ const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
5485
+ const block = [bold(opts.header ?? "API key created."), "", ` ${green(key)}`, "", dim(meta)];
5486
+ console.log(block.join("\n"));
5487
+ if (process.stdout.isTTY && process.stdin.isTTY) {
5488
+ await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
5489
+ process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
5490
+ console.log(
5491
+ dim(`New key (prefix ${row.prefix}) erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
5492
+ );
5493
+ } else {
5494
+ console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
5495
+ }
5496
+ }
5497
+ async function runKeysCreate(opts) {
5498
+ requireConfig(["DATABASE_URL"]);
5499
+ const { db, close } = createDb({ max: 1 });
5500
+ try {
5501
+ const { key, row } = await createKey(db, { label: opts.label, tier: opts.tier });
5502
+ await presentNewKey(key, row, opts);
5503
+ } finally {
5504
+ await close();
5505
+ }
5506
+ }
5507
+ async function runKeysRotate(prefixOrId, opts) {
5508
+ requireConfig(["DATABASE_URL"]);
5509
+ const { db, close } = createDb({ max: 1 });
5510
+ try {
5511
+ const result = await rotateKey(db, prefixOrId);
5512
+ if (!result) {
5513
+ logger.warn(`No active key matched "${prefixOrId}".`);
5514
+ process.exitCode = 1;
5515
+ return;
5516
+ }
5517
+ await presentNewKey(result.key, result.row, {
5518
+ json: opts.json,
5519
+ header: `API key rotated \u2014 replaces ${result.previous.prefix} (now revoked).`,
5520
+ extraJson: { replaced: result.previous.prefix }
5521
+ });
5522
+ } finally {
5523
+ await close();
5524
+ }
5525
+ }
5526
+ async function runKeysList(opts) {
5527
+ requireConfig(["DATABASE_URL"]);
5528
+ const { db, close } = createDb({ max: 1 });
5529
+ try {
5530
+ const rows = await listKeys(db);
5531
+ if (opts.json) {
5532
+ console.log(
5533
+ JSON.stringify(
5534
+ rows.map((r) => ({
5535
+ id: r.id,
5536
+ prefix: r.prefix,
5537
+ label: r.label,
5538
+ tier: r.tier,
5539
+ ownerId: r.ownerId,
5540
+ createdAt: r.createdAt,
5541
+ lastUsedAt: r.lastUsedAt,
5542
+ revokedAt: r.revokedAt
5543
+ })),
5544
+ null,
5545
+ 2
5546
+ )
5547
+ );
5548
+ return;
5549
+ }
5550
+ if (rows.length === 0) {
5551
+ console.log("No API keys yet. Create one with `lurq keys create --label <name>`.");
5552
+ return;
5553
+ }
5554
+ console.log(
5555
+ table(
5556
+ ["prefix", "label", "tier", "created", "last used", "status"],
5557
+ rows.map((r) => [
5558
+ r.prefix,
5559
+ r.label ?? "\u2014",
5560
+ r.tier,
5561
+ isoDay(r.createdAt),
5562
+ isoDay(r.lastUsedAt),
5563
+ r.revokedAt ? "revoked" : "active"
5564
+ ])
5565
+ )
5566
+ );
5567
+ } finally {
5568
+ await close();
5569
+ }
5570
+ }
5571
+ async function runKeysRevoke(prefixOrId) {
5572
+ requireConfig(["DATABASE_URL"]);
5573
+ const { db, close } = createDb({ max: 1 });
5574
+ try {
5575
+ const n = await revokeKey(db, prefixOrId);
5576
+ if (n === 0) {
5577
+ logger.warn(`No active key matched "${prefixOrId}".`);
5578
+ process.exitCode = 1;
5579
+ return;
5580
+ }
5581
+ console.log(`Revoked ${n} key(s) matching "${prefixOrId}".`);
5582
+ } finally {
5583
+ await close();
5584
+ }
5585
+ }
5586
+ var isoDay;
5587
+ var init_keys = __esm({
5588
+ "src/cli/keys.ts"() {
5589
+ "use strict";
5590
+ init_esm_shims();
5591
+ init_config();
5592
+ init_logger();
5593
+ init_apiKeys();
5594
+ init_client();
5595
+ init_format();
5596
+ isoDay = (d) => d ? d.toISOString().slice(0, 10) : "\u2014";
5597
+ }
5598
+ });
5599
+
2686
5600
  // src/db/seed.ts
2687
- import { readFileSync as readFileSync2 } from "fs";
5601
+ import { readFileSync as readFileSync3 } from "fs";
2688
5602
  import { sql as sql5 } from "drizzle-orm";
2689
5603
  import { z as z3 } from "zod";
2690
5604
  function loadSeedFile(path2 = seedJsonPath()) {
2691
- const raw = JSON.parse(readFileSync2(path2, "utf8"));
5605
+ const raw = JSON.parse(readFileSync3(path2, "utf8"));
2692
5606
  const parsed = SeedFileSchema.parse(raw);
2693
5607
  const byName = /* @__PURE__ */ new Map();
2694
5608
  for (const entry of parsed) {
@@ -2732,7 +5646,13 @@ __export(migrate_exports, {
2732
5646
  });
2733
5647
  import { migrate } from "drizzle-orm/postgres-js/migrator";
2734
5648
  async function ensureVectorExtension(handle) {
2735
- await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
5649
+ try {
5650
+ await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
5651
+ } catch (err) {
5652
+ logger.warn(
5653
+ `CREATE EXTENSION vector did not run cleanly (continuing to migrations): ${err instanceof Error ? err.message : String(err)}`
5654
+ );
5655
+ }
2736
5656
  }
2737
5657
  async function runMigrate() {
2738
5658
  const handle = createDb({ max: 1 });
@@ -2791,13 +5711,17 @@ function buildProgram() {
2791
5711
  const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
2792
5712
  await startMcpServer2();
2793
5713
  });
5714
+ 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) => {
5715
+ const { startHttpServer: startHttpServer2 } = await Promise.resolve().then(() => (init_http2(), http_exports));
5716
+ await startHttpServer2({ port: opts.port });
5717
+ });
2794
5718
  program.command("sync").description("run ingestion: refresh scores for the seed list (or one package)").option("--full", "force a full re-sync, ignoring cache TTLs").option("--package <name>", "sync a single package by name").option("--json", "output the run summary as JSON").action(async (opts) => {
2795
5719
  const { runSync: runSync2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
2796
5720
  const summary = await runSync2({ full: opts.full, packageName: opts.package });
2797
5721
  if (opts.json) console.log(JSON.stringify(summary, null, 2));
2798
5722
  if (summary.status === "failed") process.exitCode = 1;
2799
5723
  });
2800
- program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
5724
+ program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | promising | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
2801
5725
  const { runRecommend: runRecommend2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
2802
5726
  await runRecommend2(need, opts);
2803
5727
  });
@@ -2813,14 +5737,83 @@ function buildProgram() {
2813
5737
  const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
2814
5738
  await runVerify2(pkg, opts);
2815
5739
  });
2816
- program.command("install-skill").description("register lurq as an MCP server in supported AI assistants").option(
5740
+ program.command("versions").argument("<package>", "npm package name").description("show the stored version timeline for a package").option("--json", "output JSON instead of a table").option("-n, --limit <n>", "how many versions to show (default 30)").action(async (pkg, opts) => {
5741
+ const { runVersions: runVersions2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5742
+ await runVersions2(pkg, opts);
5743
+ });
5744
+ program.command("plan").argument("<file>", "path to a markdown file describing your program").description("turn a program description into an evidence-scored package plan + roadmap").option("--optimize <mode>", "ranking bias: 'speed' (lightest bundle) or 'balanced'").option("--html <path>", "write the roadmap as a self-contained HTML visualization").option("--open", "render the roadmap to HTML and open it in your browser").option("--json", "output the full plan as JSON").action(async (file, opts) => {
5745
+ const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5746
+ await runPlan2(file, opts);
5747
+ });
5748
+ 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) => {
5749
+ const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5750
+ runWeights2(opts);
5751
+ });
5752
+ 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) => {
5753
+ const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5754
+ await runEditWeights2(opts);
5755
+ });
5756
+ 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) => {
5757
+ const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
5758
+ requireConfig2(["DATABASE_URL"]);
5759
+ const { runDiscovery: runDiscovery2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
5760
+ const summary = await runDiscovery2({ perRunCap: opts.cap, dryRun: opts.dryRun });
5761
+ if (opts.json) console.log(JSON.stringify(summary, null, 2));
5762
+ });
5763
+ 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) => {
5764
+ const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
5765
+ requireConfig2(["DATABASE_URL"]);
5766
+ const { runRescore: runRescore2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
5767
+ const summary = await runRescore2();
5768
+ if (opts.json) console.log(JSON.stringify(summary, null, 2));
5769
+ });
5770
+ program.command("watch").description(
5771
+ "operator-side: follow the npm changes feed, re-syncing tracked packages on new releases"
5772
+ ).action(async () => {
5773
+ const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5774
+ await runWatch2();
5775
+ });
5776
+ program.command("sandbox").argument("<package>", "npm package name").argument("[version]", "specific version (default: latest)").description(
5777
+ "operator-side: install + smoke-load a package in a sandbox to verify it actually works"
5778
+ ).option("--esm", "load via ESM import instead of CJS require").option("--allow-scripts", "run install scripts (UNSAFE without VM isolation)").option("--json", "output JSON").action(
5779
+ async (pkg, version, opts) => {
5780
+ const { runSandbox: runSandbox2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5781
+ await runSandbox2(pkg, version, opts);
5782
+ }
5783
+ );
5784
+ program.command("compat").argument("<packages...>", "npm package names to check together").description("check pairwise compatibility of packages from the sandbox matrix").option("--run", "operator-side: co-install them in the sandbox first (UNSAFE without VM isolation)").option("--json", "output JSON").action(async (pkgs, opts) => {
5785
+ const { runCompat: runCompat2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5786
+ await runCompat2(pkgs, opts);
5787
+ });
5788
+ 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) => {
5789
+ const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
5790
+ await runInstallWizard2(opts);
5791
+ });
5792
+ program.command("install-skill").description("register lurq as an MCP server in supported AI assistants (scriptable)").option(
2817
5793
  "--agent <agent>",
2818
5794
  "claude-code | cursor | copilot | windsurf | codex | all",
2819
5795
  "claude-code"
2820
- ).action(async (opts) => {
5796
+ ).option("--api-key <key>", "hosted API key (remote install; default mode)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--local", "self-host: write a local stdio entry using your own DATABASE_URL").action(async (opts) => {
2821
5797
  const { runInstallSkill: runInstallSkill2 } = await Promise.resolve().then(() => (init_installSkill(), installSkill_exports));
2822
5798
  await runInstallSkill2(opts);
2823
5799
  });
5800
+ const keys = program.command("keys").description("manage API keys for the hosted service (operator; needs DATABASE_URL)");
5801
+ 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) => {
5802
+ const { runKeysCreate: runKeysCreate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
5803
+ await runKeysCreate2(opts);
5804
+ });
5805
+ keys.command("list").description("list issued API keys (hashes are never shown)").option("--json", "output as JSON").action(async (opts) => {
5806
+ const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
5807
+ await runKeysList2(opts);
5808
+ });
5809
+ keys.command("rotate").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id to replace").description("issue a replacement key (same label/tier) and revoke the old one").option("--json", "print the new key as JSON and skip the interactive erase (for scripts)").action(async (prefixOrId, opts) => {
5810
+ const { runKeysRotate: runKeysRotate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
5811
+ await runKeysRotate2(prefixOrId, opts);
5812
+ });
5813
+ keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
5814
+ const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
5815
+ await runKeysRevoke2(prefixOrId);
5816
+ });
2824
5817
  const db = program.command("db").description("database management");
2825
5818
  db.command("migrate").description("apply database migrations and load the seed list").action(async () => {
2826
5819
  const { runMigrate: runMigrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
@@ -2840,9 +5833,11 @@ function buildProgram() {
2840
5833
  return program;
2841
5834
  }
2842
5835
  export {
5836
+ API_KEY_PREFIX,
2843
5837
  CACHE_TTL,
2844
5838
  CATEGORIES,
2845
5839
  ConfigError,
5840
+ DEFAULT_ENDPOINT,
2846
5841
  EMBEDDING_DIM,
2847
5842
  FRONTEND_CATEGORIES,
2848
5843
  PACKAGE_NAME,