lurqrun 0.0.2 → 0.0.4

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
@@ -77,7 +77,7 @@ var init_constants = __esm({
77
77
  init_esm_shims();
78
78
  SERVER_NAME = "lurq";
79
79
  PACKAGE_NAME = "lurqrun";
80
- VERSION = "0.0.2";
80
+ VERSION = "0.0.4";
81
81
  DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
82
82
  API_KEY_PREFIX = "lurq_live_";
83
83
  EMBEDDING_DIM = 1536;
@@ -145,12 +145,18 @@ var init_config = __esm({
145
145
  EnvSchema = z.object({
146
146
  DATABASE_URL: z.string().min(1).optional(),
147
147
  GITHUB_TOKEN: z.string().min(1).optional(),
148
+ // 'openai' here means "OpenAI-compatible" — any provider exposing /v1/embeddings
149
+ // + Bearer auth (OpenAI, Together, Fireworks, HF TEI, …). Point *_BASE_URL at it.
148
150
  EMBEDDING_PROVIDER: z.enum(["openai", "local"]).default("openai"),
149
151
  EMBEDDING_API_KEY: z.string().min(1).optional(),
150
152
  EMBEDDING_MODEL: z.string().min(1).default("text-embedding-3-small"),
153
+ EMBEDDING_BASE_URL: z.string().url().default("https://api.openai.com/v1"),
154
+ // 'openai' means "OpenAI-compatible /v1/chat/completions": OpenAI, Groq, Together,
155
+ // Fireworks, xAI (Grok), etc. Swap provider by setting SUMMARY_BASE_URL + key + model.
151
156
  SUMMARY_PROVIDER: z.enum(["openai", "none"]).default("openai"),
152
157
  SUMMARY_API_KEY: z.string().min(1).optional(),
153
158
  SUMMARY_MODEL: z.string().min(1).default("gpt-4o-mini"),
159
+ SUMMARY_BASE_URL: z.string().url().default("https://api.openai.com/v1"),
154
160
  LURQ_SYNC_CONCURRENCY: z.coerce.number().int().positive().max(50).default(5),
155
161
  LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
156
162
  // Hosted HTTP service (`serve-http`). Server-side only.
@@ -161,9 +167,22 @@ var init_config = __esm({
161
167
  LURQ_IP_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(240),
162
168
  /** Rate-limit window, milliseconds (applies to both limiters). */
163
169
  LURQ_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(6e4),
170
+ /** Bearer token guarding `/metrics`. Unset → the endpoint is disabled (404). */
171
+ LURQ_METRICS_TOKEN: z.string().min(1).optional(),
172
+ /** Shared secret for self-serve key issuance (`POST /keys`). The Clerk-
173
+ * authenticated web app presents it to mint a key for a signed-in user. Unset
174
+ * → the endpoint is disabled (404). Keep it server-side, never in the client. */
175
+ LURQ_ISSUER_SECRET: z.string().min(1).optional(),
164
176
  // Client-side (install wizard / CLI talking to a remote endpoint).
165
177
  LURQ_ENDPOINT: z.string().url().optional(),
166
- LURQ_API_KEY: z.string().min(1).optional()
178
+ LURQ_API_KEY: z.string().min(1).optional(),
179
+ // Sandbox verification. With E2B_API_KEY set, package install + smoke-load
180
+ // runs in an isolated E2B cloud sandbox (safe for UNTRUSTED packages);
181
+ // without it, the local child-process driver is used (trusted packages only).
182
+ E2B_API_KEY: z.string().min(1).optional(),
183
+ // E2B template to launch. Must provide node + npm on PATH; omit for E2B's
184
+ // default. Provision a Node-versioned template here for reproducible runs.
185
+ E2B_TEMPLATE: z.string().min(1).optional()
167
186
  });
168
187
  ConfigError = class extends Error {
169
188
  constructor(message) {
@@ -213,10 +232,15 @@ var init_logger = __esm({
213
232
  var schema_exports = {};
214
233
  __export(schema_exports, {
215
234
  apiKeys: () => apiKeys,
235
+ compatEdges: () => compatEdges,
216
236
  discoveryQueue: () => discoveryQueue,
237
+ packageVersions: () => packageVersions,
217
238
  packages: () => packages,
239
+ recommendationOutcomes: () => recommendationOutcomes,
218
240
  seedPackages: () => seedPackages,
219
- syncRuns: () => syncRuns
241
+ syncRuns: () => syncRuns,
242
+ verificationRuns: () => verificationRuns,
243
+ watchState: () => watchState
220
244
  });
221
245
  import { sql } from "drizzle-orm";
222
246
  import {
@@ -227,13 +251,15 @@ import {
227
251
  integer,
228
252
  jsonb,
229
253
  pgTable,
254
+ primaryKey,
230
255
  real,
231
256
  serial,
232
257
  text,
233
258
  timestamp,
259
+ uniqueIndex,
234
260
  vector
235
261
  } from "drizzle-orm/pg-core";
236
- var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys;
262
+ var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges, recommendationOutcomes;
237
263
  var init_schema = __esm({
238
264
  "src/db/schema.ts"() {
239
265
  "use strict";
@@ -270,7 +296,6 @@ var init_schema = __esm({
270
296
  // Adoption signals
271
297
  weeklyDownloads: bigint("weekly_downloads", { mode: "number" }),
272
298
  downloadGrowth90d: real("download_growth_90d"),
273
- dependentsCount: integer("dependents_count"),
274
299
  stars: integer("stars"),
275
300
  openIssues: integer("open_issues"),
276
301
  closedIssues: integer("closed_issues"),
@@ -278,6 +303,11 @@ var init_schema = __esm({
278
303
  scorecard: real("scorecard"),
279
304
  bundleMinGzipKb: real("bundle_min_gzip_kb"),
280
305
  advisories: jsonb("advisories").$type(),
306
+ // Compatibility metadata (Tier-1): declared peer-deps + engines of the
307
+ // latest version, so a whole-stack peer-range check is one indexed query.
308
+ peerDependencies: jsonb("peer_dependencies").$type(),
309
+ peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
310
+ engines: jsonb("engines").$type(),
281
311
  // Computed outputs
282
312
  healthScore: integer("health_score"),
283
313
  /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
@@ -287,6 +317,10 @@ var init_schema = __esm({
287
317
  scoreBreakdown: jsonb("score_breakdown").$type(),
288
318
  usageGuide: jsonb("usage_guide").$type(),
289
319
  embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
320
+ // Identity of the vector space `embedding` was produced in (e.g.
321
+ // `openai:text-embedding-3-small`, `local`). Vector search filters on the
322
+ // active provider so switching models can't compare incompatible spaces.
323
+ embeddingProvider: text("embedding_provider"),
290
324
  // Lexical search vector (§3): name weighted highest (A), then category (B),
291
325
  // then summary/description (C). Generated + STORED so it stays in sync with
292
326
  // the row automatically; indexed with GIN for fast `@@` matching.
@@ -353,6 +387,83 @@ var init_schema = __esm({
353
387
  },
354
388
  (table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
355
389
  );
390
+ packageVersions = pgTable(
391
+ "package_versions",
392
+ {
393
+ packageName: text("package_name").notNull(),
394
+ version: text("version").notNull(),
395
+ publishedAt: ts("published_at")
396
+ },
397
+ (table2) => [
398
+ primaryKey({ columns: [table2.packageName, table2.version] }),
399
+ index("package_versions_name_published_idx").on(table2.packageName, table2.publishedAt)
400
+ ]
401
+ );
402
+ watchState = pgTable("watch_state", {
403
+ id: text("id").primaryKey(),
404
+ seq: text("seq").notNull(),
405
+ updatedAt: ts("updated_at")
406
+ });
407
+ verificationRuns = pgTable(
408
+ "verification_runs",
409
+ {
410
+ id: serial("id").primaryKey(),
411
+ packageName: text("package_name").notNull(),
412
+ version: text("version").notNull(),
413
+ driver: text("driver").notNull(),
414
+ moduleSystem: text("module_system").notNull(),
415
+ installed: boolean("installed").notNull(),
416
+ imported: boolean("imported"),
417
+ ranScripts: boolean("ran_scripts").notNull().default(false),
418
+ durationMs: integer("duration_ms"),
419
+ error: text("error"),
420
+ ranAt: ts("ran_at")
421
+ },
422
+ (table2) => [index("verification_runs_pkg_idx").on(table2.packageName, table2.version)]
423
+ );
424
+ compatEdges = pgTable(
425
+ "compat_edges",
426
+ {
427
+ id: serial("id").primaryKey(),
428
+ packageA: text("package_a").notNull(),
429
+ versionA: text("version_a").notNull(),
430
+ packageB: text("package_b").notNull(),
431
+ versionB: text("version_b").notNull(),
432
+ status: text("status").$type().notNull(),
433
+ driver: text("driver").notNull(),
434
+ ranAt: ts("ran_at")
435
+ },
436
+ (table2) => [
437
+ uniqueIndex("compat_edges_pair_idx").on(
438
+ table2.packageA,
439
+ table2.versionA,
440
+ table2.packageB,
441
+ table2.versionB
442
+ )
443
+ ]
444
+ );
445
+ recommendationOutcomes = pgTable(
446
+ "recommendation_outcomes",
447
+ {
448
+ id: serial("id").primaryKey(),
449
+ /** The org this outcome belongs to (api_keys.owner_id). Null for anonymous /
450
+ * operator-issued keys. This is what turns the flywheel from a global blob
451
+ * into a per-org asset — "what did *this* org succeed with." Server-injected
452
+ * from the authenticated key, never caller-supplied. */
453
+ ownerId: text("owner_id"),
454
+ packageName: text("package_name").notNull(),
455
+ accepted: boolean("accepted").notNull(),
456
+ buildSignal: text("build_signal").$type(),
457
+ /** The original need text the recommendation was for — ties outcome back to
458
+ * the ask. Optional, length-capped at the trust boundary; never source code. */
459
+ need: text("need"),
460
+ createdAt: ts("created_at").notNull().defaultNow()
461
+ },
462
+ (table2) => [
463
+ index("recommendation_outcomes_pkg_idx").on(table2.packageName),
464
+ index("recommendation_outcomes_owner_idx").on(table2.ownerId)
465
+ ]
466
+ );
356
467
  }
357
468
  });
358
469
 
@@ -361,10 +472,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
361
472
  import postgres from "postgres";
362
473
  function createDb(opts = {}) {
363
474
  const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
364
- const sql8 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
475
+ const sql6 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
365
476
  } });
366
- const db = drizzle(sql8, { schema: schema_exports });
367
- return { db, sql: sql8, close: () => sql8.end() };
477
+ const db = drizzle(sql6, { schema: schema_exports });
478
+ return { db, sql: sql6, close: () => sql6.end() };
368
479
  }
369
480
  var init_client = __esm({
370
481
  "src/db/client.ts"() {
@@ -375,6 +486,131 @@ var init_client = __esm({
375
486
  }
376
487
  });
377
488
 
489
+ // src/core/cache.ts
490
+ var cache_exports = {};
491
+ __export(cache_exports, {
492
+ cached: () => cached2,
493
+ invalidateCache: () => invalidateCache
494
+ });
495
+ function ttlSeconds() {
496
+ const n = Number(process.env.LURQ_CACHE_TTL_SEC);
497
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_TTL_SEC;
498
+ }
499
+ async function getClient() {
500
+ if (!process.env.REDIS_URL) return null;
501
+ if (!clientPromise) {
502
+ clientPromise = (async () => {
503
+ try {
504
+ const { default: Redis } = await import("ioredis");
505
+ const client = new Redis(process.env.REDIS_URL, {
506
+ maxRetriesPerRequest: 1,
507
+ enableOfflineQueue: false,
508
+ lazyConnect: false,
509
+ // Railway's private network (redis.railway.internal) is IPv6-only and
510
+ // ioredis defaults to IPv4 — family:0 lets it resolve either stack, so
511
+ // both the private URL and a public/Upstash URL work unchanged.
512
+ family: 0
513
+ });
514
+ client.on("error", (err) => logger.warn(`redis: ${err.message}`));
515
+ logger.info("Response cache enabled (REDIS_URL set).");
516
+ return client;
517
+ } catch (err) {
518
+ logger.warn(`redis disabled (init failed): ${err.message}`);
519
+ return null;
520
+ }
521
+ })();
522
+ }
523
+ return clientPromise;
524
+ }
525
+ async function namespaceVersion(client) {
526
+ if (versionMemo && Date.now() - versionMemo.at < VERSION_MEMO_MS) return versionMemo.value;
527
+ const value = await client.get(VERSION_KEY).catch(() => null) ?? "0";
528
+ versionMemo = { value, at: Date.now() };
529
+ return value;
530
+ }
531
+ async function cached2(namespace, key, compute, opts = {}) {
532
+ const client = await getClient();
533
+ if (!client) return compute();
534
+ let fullKey;
535
+ try {
536
+ const version = await namespaceVersion(client);
537
+ fullKey = `lurq:${namespace}:${version}:${key}`;
538
+ const hit = await client.get(fullKey);
539
+ if (hit != null) return JSON.parse(hit);
540
+ } catch (err) {
541
+ logger.warn(`redis read failed, bypassing cache: ${err.message}`);
542
+ return compute();
543
+ }
544
+ const value = await compute();
545
+ if (!opts.skipCache?.(value)) {
546
+ client.set(fullKey, JSON.stringify(value), "EX", ttlSeconds()).catch(() => {
547
+ });
548
+ }
549
+ return value;
550
+ }
551
+ async function invalidateCache() {
552
+ const client = await getClient();
553
+ if (!client) return;
554
+ try {
555
+ await client.incr(VERSION_KEY);
556
+ versionMemo = null;
557
+ } catch (err) {
558
+ logger.warn(`redis invalidate failed: ${err.message}`);
559
+ }
560
+ }
561
+ var VERSION_KEY, DEFAULT_TTL_SEC, VERSION_MEMO_MS, clientPromise, versionMemo;
562
+ var init_cache = __esm({
563
+ "src/core/cache.ts"() {
564
+ "use strict";
565
+ init_esm_shims();
566
+ init_logger();
567
+ VERSION_KEY = "lurq:cachever";
568
+ DEFAULT_TTL_SEC = 3600;
569
+ VERSION_MEMO_MS = 1e4;
570
+ clientPromise = null;
571
+ versionMemo = null;
572
+ }
573
+ });
574
+
575
+ // src/db/compat.ts
576
+ import { and, inArray } from "drizzle-orm";
577
+ async function getCompatMetadata(db, names) {
578
+ if (names.length === 0) return [];
579
+ return db.select({
580
+ name: packages.name,
581
+ latestVersion: packages.latestVersion,
582
+ peerDependencies: packages.peerDependencies,
583
+ peerDependenciesMeta: packages.peerDependenciesMeta,
584
+ engines: packages.engines
585
+ }).from(packages).where(inArray(packages.name, names));
586
+ }
587
+ function canonicalPair(a, b) {
588
+ const [low, high] = a.name <= b.name ? [a, b] : [b, a];
589
+ return { packageA: low.name, versionA: low.version, packageB: high.name, versionB: high.version };
590
+ }
591
+ async function upsertCompatEdge(db, edge) {
592
+ await db.insert(compatEdges).values(edge).onConflictDoUpdate({
593
+ target: [
594
+ compatEdges.packageA,
595
+ compatEdges.versionA,
596
+ compatEdges.packageB,
597
+ compatEdges.versionB
598
+ ],
599
+ set: { status: edge.status, driver: edge.driver, ranAt: edge.ranAt }
600
+ });
601
+ }
602
+ async function getCompatEdges(db, names) {
603
+ if (names.length === 0) return [];
604
+ return db.select().from(compatEdges).where(and(inArray(compatEdges.packageA, names), inArray(compatEdges.packageB, names)));
605
+ }
606
+ var init_compat = __esm({
607
+ "src/db/compat.ts"() {
608
+ "use strict";
609
+ init_esm_shims();
610
+ init_schema();
611
+ }
612
+ });
613
+
378
614
  // src/core/http.ts
379
615
  import { createHash } from "crypto";
380
616
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -439,9 +675,9 @@ async function httpRequest(url, opts) {
439
675
  } = opts;
440
676
  const key = opts.cacheKey ?? `${method} ${url} ${body ?? ""}`;
441
677
  if (ttlMs > 0 && !bypassCacheRead) {
442
- const cached2 = await readCache(key, ttlMs);
443
- if (cached2) {
444
- return { status: cached2.status, data: decode(cached2.body, accept), fromCache: true };
678
+ const cached3 = await readCache(key, ttlMs);
679
+ if (cached3) {
680
+ return { status: cached3.status, data: decode(cached3.body, accept), fromCache: true };
445
681
  }
446
682
  }
447
683
  const limiter = limiterFor(host);
@@ -626,9 +862,45 @@ function parseNpmRegistry(json2) {
626
862
  hasTypes: detectTypes(name, latestManifest),
627
863
  hasTestScript: detectTestScript(latestManifest),
628
864
  directDependenciesCount: countDeps(latestManifest?.dependencies),
629
- hasProvenance: detectProvenance(latestManifest)
865
+ hasProvenance: detectProvenance(latestManifest),
866
+ hasInstallScripts: detectInstallScripts(latestManifest),
867
+ peerDependencies: parseDepMap(latestManifest?.peerDependencies),
868
+ peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
869
+ engines: parseDepMap(latestManifest?.engines),
870
+ versionTimeline: parseVersionTimeline(json2)
630
871
  };
631
872
  }
873
+ function parseDepMap(value) {
874
+ if (!value || typeof value !== "object") return null;
875
+ const out = {};
876
+ for (const [name, range] of Object.entries(value)) {
877
+ if (typeof range === "string" && range.trim() !== "") out[name] = range;
878
+ }
879
+ return Object.keys(out).length ? out : null;
880
+ }
881
+ function parsePeerMeta(value) {
882
+ if (!value || typeof value !== "object") return null;
883
+ const out = {};
884
+ for (const [name, meta] of Object.entries(value)) {
885
+ if (meta && typeof meta === "object" && "optional" in meta) {
886
+ out[name] = { optional: Boolean(meta.optional) };
887
+ }
888
+ }
889
+ return Object.keys(out).length ? out : null;
890
+ }
891
+ function detectInstallScripts(manifest) {
892
+ const scripts = manifest?.scripts;
893
+ if (!scripts || typeof scripts !== "object") return false;
894
+ return ["preinstall", "install", "postinstall"].some(
895
+ (hook) => typeof scripts[hook] === "string" && scripts[hook].trim() !== ""
896
+ );
897
+ }
898
+ function parseVersionTimeline(json2) {
899
+ const versions = json2?.versions;
900
+ if (!versions || typeof versions !== "object") return [];
901
+ const time = json2?.time ?? {};
902
+ return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
903
+ }
632
904
  function parseKeywords(value) {
633
905
  if (!Array.isArray(value)) return [];
634
906
  return value.filter((k) => typeof k === "string");
@@ -733,7 +1005,7 @@ function parseDownloadGrowth(json2) {
733
1005
  const priorAvg = sum(prior) / prior.length;
734
1006
  const recentAvg = sum(recent) / recent.length;
735
1007
  if (priorAvg <= 0) return recentAvg > 0 ? 1 : 0;
736
- return (recentAvg - priorAvg) / priorAvg;
1008
+ return Math.round((recentAvg - priorAvg) / priorAvg * 1e3) / 1e3;
737
1009
  }
738
1010
  function ymd(date) {
739
1011
  return date.toISOString().slice(0, 10);
@@ -1025,6 +1297,336 @@ var init_sources = __esm({
1025
1297
  }
1026
1298
  });
1027
1299
 
1300
+ // src/compat/members.ts
1301
+ async function assembleMembers(db, names) {
1302
+ const tracked = new Map((await getCompatMetadata(db, names)).map((r) => [r.name, r]));
1303
+ const members = [];
1304
+ const unverified = [];
1305
+ await Promise.all(
1306
+ names.map(async (name) => {
1307
+ const row = tracked.get(name);
1308
+ if (row) {
1309
+ members.push({
1310
+ name,
1311
+ version: row.latestVersion,
1312
+ peerDependencies: row.peerDependencies,
1313
+ peerDependenciesMeta: row.peerDependenciesMeta,
1314
+ engines: row.engines
1315
+ });
1316
+ return;
1317
+ }
1318
+ const reg = await fetchNpmRegistry(name).catch(() => null);
1319
+ if (reg) {
1320
+ members.push({
1321
+ name,
1322
+ version: reg.latestVersion,
1323
+ peerDependencies: reg.peerDependencies,
1324
+ peerDependenciesMeta: reg.peerDependenciesMeta,
1325
+ engines: reg.engines
1326
+ });
1327
+ } else {
1328
+ unverified.push(name);
1329
+ }
1330
+ })
1331
+ );
1332
+ return { members, unverified };
1333
+ }
1334
+ var init_members = __esm({
1335
+ "src/compat/members.ts"() {
1336
+ "use strict";
1337
+ init_esm_shims();
1338
+ init_compat();
1339
+ init_sources();
1340
+ }
1341
+ });
1342
+
1343
+ // src/compat/peerCompat.ts
1344
+ import semver from "semver";
1345
+ function satisfiesRange(version, range) {
1346
+ if (!semver.validRange(range)) return null;
1347
+ const v = semver.valid(version) ? version : semver.coerce(version)?.version;
1348
+ if (!v) return null;
1349
+ return semver.satisfies(v, range, { includePrerelease: true });
1350
+ }
1351
+ function rangesIntersect(a, b) {
1352
+ if (!semver.validRange(a) || !semver.validRange(b)) return null;
1353
+ try {
1354
+ return semver.intersects(a, b, { includePrerelease: true });
1355
+ } catch {
1356
+ return null;
1357
+ }
1358
+ }
1359
+ function resolveArchitectureCompat(members) {
1360
+ const conflicts = [];
1361
+ const pinned = /* @__PURE__ */ new Map();
1362
+ for (const m of members) if (m.version) pinned.set(m.name, m.version);
1363
+ const constraints = [];
1364
+ for (const m of members) {
1365
+ if (!m.peerDependencies) continue;
1366
+ for (const [peer, range] of Object.entries(m.peerDependencies)) {
1367
+ constraints.push({
1368
+ requirer: m.name,
1369
+ peer,
1370
+ range,
1371
+ optional: Boolean(m.peerDependenciesMeta?.[peer]?.optional)
1372
+ });
1373
+ }
1374
+ }
1375
+ for (const c of constraints) {
1376
+ const pv = pinned.get(c.peer);
1377
+ if (pv && satisfiesRange(pv, c.range) === false) {
1378
+ conflicts.push({
1379
+ source: "peer-deps",
1380
+ packages: [c.requirer, c.peer],
1381
+ detail: `${c.requirer} needs peer ${c.peer}@${c.range}, but the stack uses ${c.peer}@${pv}`
1382
+ });
1383
+ }
1384
+ }
1385
+ const byPeer = /* @__PURE__ */ new Map();
1386
+ for (const c of constraints) {
1387
+ if (pinned.has(c.peer) || c.optional) continue;
1388
+ const arr = byPeer.get(c.peer);
1389
+ if (arr) arr.push(c);
1390
+ else byPeer.set(c.peer, [c]);
1391
+ }
1392
+ for (const [peer, cs] of byPeer) {
1393
+ for (let i = 0; i < cs.length; i++) {
1394
+ for (let j = i + 1; j < cs.length; j++) {
1395
+ if (cs[i].range !== cs[j].range && rangesIntersect(cs[i].range, cs[j].range) === false) {
1396
+ conflicts.push({
1397
+ source: "peer-deps",
1398
+ packages: [cs[i].requirer, cs[j].requirer],
1399
+ detail: `${cs[i].requirer} needs ${peer}@${cs[i].range} but ${cs[j].requirer} needs ${peer}@${cs[j].range} \u2014 no overlapping version`
1400
+ });
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ const nodeReqs = members.map((m) => ({ name: m.name, range: m.engines?.node })).filter((r) => typeof r.range === "string");
1406
+ for (let i = 0; i < nodeReqs.length; i++) {
1407
+ for (let j = i + 1; j < nodeReqs.length; j++) {
1408
+ if (rangesIntersect(nodeReqs[i].range, nodeReqs[j].range) === false) {
1409
+ conflicts.push({
1410
+ source: "engines",
1411
+ packages: [nodeReqs[i].name, nodeReqs[j].name],
1412
+ detail: `${nodeReqs[i].name} needs node ${nodeReqs[i].range} but ${nodeReqs[j].name} needs node ${nodeReqs[j].range} \u2014 no overlap`
1413
+ });
1414
+ }
1415
+ }
1416
+ }
1417
+ return conflicts;
1418
+ }
1419
+ var init_peerCompat = __esm({
1420
+ "src/compat/peerCompat.ts"() {
1421
+ "use strict";
1422
+ init_esm_shims();
1423
+ }
1424
+ });
1425
+
1426
+ // src/compat/check.ts
1427
+ async function checkCompat(db, packages2) {
1428
+ const names = [...new Set(packages2)];
1429
+ const { members, unverified } = await assembleMembers(db, names);
1430
+ const conflicts = resolveArchitectureCompat(members);
1431
+ for (const edge of await getCompatEdges(db, names)) {
1432
+ if (edge.status === "conflict") {
1433
+ conflicts.push({
1434
+ source: "sandbox",
1435
+ packages: [edge.packageA, edge.packageB],
1436
+ detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
1437
+ });
1438
+ }
1439
+ }
1440
+ const overall = conflicts.length ? "conflict" : unverified.length ? "unknown" : "compatible";
1441
+ return {
1442
+ packages: names,
1443
+ overall,
1444
+ conflicts,
1445
+ unverified,
1446
+ checked: members.map((m) => ({ name: m.name, version: m.version }))
1447
+ };
1448
+ }
1449
+ var init_check = __esm({
1450
+ "src/compat/check.ts"() {
1451
+ "use strict";
1452
+ init_esm_shims();
1453
+ init_compat();
1454
+ init_members();
1455
+ init_peerCompat();
1456
+ }
1457
+ });
1458
+
1459
+ // src/db/packages.ts
1460
+ import { desc, eq, isNotNull } from "drizzle-orm";
1461
+ async function getSeedTargets(db) {
1462
+ const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
1463
+ return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
1464
+ }
1465
+ async function getPackageByName(db, name) {
1466
+ const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
1467
+ return rows[0] ?? null;
1468
+ }
1469
+ async function getAllPackageNames(db) {
1470
+ const rows = await db.select({ name: packages.name }).from(packages);
1471
+ return rows.map((r) => r.name);
1472
+ }
1473
+ async function upsertPackageVersions(db, name, versions) {
1474
+ if (versions.length === 0) return;
1475
+ for (let i = 0; i < versions.length; i += VERSION_CHUNK) {
1476
+ const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
1477
+ packageName: name,
1478
+ version: v.version,
1479
+ publishedAt: v.publishedAt
1480
+ }));
1481
+ await db.insert(packageVersions).values(rows).onConflictDoNothing();
1482
+ }
1483
+ }
1484
+ async function getPackageVersions(db, name, limit = 50) {
1485
+ const rows = await db.select({ version: packageVersions.version, publishedAt: packageVersions.publishedAt }).from(packageVersions).where(eq(packageVersions.packageName, name)).orderBy(desc(packageVersions.publishedAt)).limit(limit);
1486
+ return rows.map((r) => ({ version: r.version, publishedAt: r.publishedAt }));
1487
+ }
1488
+ async function getTopPackageNames(db, limit = 1e3) {
1489
+ const rows = await db.select({ name: packages.name }).from(packages).where(isNotNull(packages.weeklyDownloads)).orderBy(desc(packages.weeklyDownloads)).limit(limit);
1490
+ return rows.map((r) => r.name);
1491
+ }
1492
+ async function ensureSeedEntry(db, name, category) {
1493
+ await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
1494
+ }
1495
+ async function upsertPackage(db, row) {
1496
+ const { name: _name, createdAt: _createdAt, ...mutable } = row;
1497
+ await db.insert(packages).values(row).onConflictDoUpdate({
1498
+ target: packages.name,
1499
+ set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
1500
+ });
1501
+ }
1502
+ async function startSyncRun(db) {
1503
+ const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
1504
+ return row.id;
1505
+ }
1506
+ async function finishSyncRun(db, id, data) {
1507
+ await db.update(syncRuns).set({
1508
+ finishedAt: /* @__PURE__ */ new Date(),
1509
+ packagesSeen: data.packagesSeen,
1510
+ packagesUpdated: data.packagesUpdated,
1511
+ errors: data.errors,
1512
+ status: data.status
1513
+ }).where(eq(syncRuns.id, id));
1514
+ }
1515
+ var VERSION_CHUNK;
1516
+ var init_packages = __esm({
1517
+ "src/db/packages.ts"() {
1518
+ "use strict";
1519
+ init_esm_shims();
1520
+ init_schema();
1521
+ VERSION_CHUNK = 500;
1522
+ }
1523
+ });
1524
+
1525
+ // src/db/verification.ts
1526
+ import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
1527
+ async function storeVerificationRun(db, run) {
1528
+ await db.insert(verificationRuns).values(run);
1529
+ }
1530
+ async function getLatestVerificationByName(db, packageName) {
1531
+ const rows = await db.select().from(verificationRuns).where(eq2(verificationRuns.packageName, packageName)).orderBy(desc2(verificationRuns.ranAt)).limit(1);
1532
+ return rows[0] ?? null;
1533
+ }
1534
+ var init_verification = __esm({
1535
+ "src/db/verification.ts"() {
1536
+ "use strict";
1537
+ init_esm_shims();
1538
+ init_schema();
1539
+ }
1540
+ });
1541
+
1542
+ // src/db/outcomes.ts
1543
+ async function recordOutcome(db, outcome) {
1544
+ await db.insert(recommendationOutcomes).values(outcome);
1545
+ }
1546
+ var init_outcomes = __esm({
1547
+ "src/db/outcomes.ts"() {
1548
+ "use strict";
1549
+ init_esm_shims();
1550
+ init_schema();
1551
+ }
1552
+ });
1553
+
1554
+ // src/data/successors.json
1555
+ var successors_default;
1556
+ var init_successors = __esm({
1557
+ "src/data/successors.json"() {
1558
+ successors_default = {
1559
+ moment: {
1560
+ replacedBy: "dayjs",
1561
+ reason: "In maintenance mode since 2020 (the maintainers recommend alternatives). dayjs is a ~2KB immutable library with a near-identical API."
1562
+ },
1563
+ request: {
1564
+ replacedBy: "got",
1565
+ reason: "Deprecated in 2020 and no longer maintained. Use the built-in fetch, or got for a full-featured HTTP client."
1566
+ },
1567
+ "request-promise": {
1568
+ replacedBy: "got",
1569
+ reason: "Deprecated alongside request. got returns promises natively."
1570
+ },
1571
+ "node-sass": {
1572
+ replacedBy: "sass",
1573
+ reason: "Deprecated; bindings break on new Node versions. sass is the Dart implementation (sass-embedded for speed)."
1574
+ },
1575
+ tslint: {
1576
+ replacedBy: "typescript-eslint",
1577
+ reason: "Deprecated in 2019 in favor of ESLint. Use typescript-eslint to lint TypeScript."
1578
+ },
1579
+ enzyme: {
1580
+ replacedBy: "@testing-library/react",
1581
+ reason: "Unmaintained with no official React 17+/18 adapter. React Testing Library is the current standard."
1582
+ },
1583
+ protractor: {
1584
+ replacedBy: "@playwright/test",
1585
+ reason: "Reached end-of-life in 2023. Use Playwright (or Cypress) for browser E2E."
1586
+ },
1587
+ faker: {
1588
+ replacedBy: "@faker-js/faker",
1589
+ reason: "The original package was sabotaged and deprecated. @faker-js/faker is the community-maintained fork."
1590
+ },
1591
+ bower: {
1592
+ replacedBy: "npm",
1593
+ reason: "Deprecated; front-end packages ship on npm now. Use npm (or your package manager) directly."
1594
+ },
1595
+ "popper.js": {
1596
+ replacedBy: "@popperjs/core",
1597
+ reason: "v1 is deprecated. @popperjs/core is the maintained v2 line."
1598
+ },
1599
+ "create-react-app": {
1600
+ replacedBy: "vite",
1601
+ reason: "No longer recommended by the React team and effectively unmaintained. Vite is the current scaffolding standard."
1602
+ },
1603
+ "babel-eslint": {
1604
+ replacedBy: "@babel/eslint-parser",
1605
+ reason: "Deprecated and renamed. Use @babel/eslint-parser."
1606
+ },
1607
+ istanbul: {
1608
+ replacedBy: "nyc",
1609
+ reason: "The istanbul package is legacy; nyc is its maintained CLI (or c8 for native V8 coverage)."
1610
+ }
1611
+ };
1612
+ }
1613
+ });
1614
+
1615
+ // src/core/successors.ts
1616
+ function lookupSuccessor(name) {
1617
+ const hit = MAP[name.toLowerCase()];
1618
+ return hit ? { name: hit.replacedBy, reason: hit.reason } : null;
1619
+ }
1620
+ var MAP;
1621
+ var init_successors2 = __esm({
1622
+ "src/core/successors.ts"() {
1623
+ "use strict";
1624
+ init_esm_shims();
1625
+ init_successors();
1626
+ MAP = successors_default;
1627
+ }
1628
+ });
1629
+
1028
1630
  // src/ingestion/sources/githubReadme.ts
1029
1631
  async function fetchGithubReadme(owner, repo, fetchImpl) {
1030
1632
  for (const file of CANDIDATES) {
@@ -1091,7 +1693,12 @@ function str(value) {
1091
1693
  function createSummaryProvider(fetchImpl) {
1092
1694
  const config = getConfig();
1093
1695
  if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
1094
- return new OpenAISummaryProvider(config.SUMMARY_API_KEY, config.SUMMARY_MODEL, fetchImpl);
1696
+ return new OpenAISummaryProvider(
1697
+ config.SUMMARY_API_KEY,
1698
+ config.SUMMARY_MODEL,
1699
+ config.SUMMARY_BASE_URL,
1700
+ fetchImpl
1701
+ );
1095
1702
  }
1096
1703
  return new FallbackSummaryProvider();
1097
1704
  }
@@ -1163,15 +1770,19 @@ var init_summarize = __esm({
1163
1770
  };
1164
1771
  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.";
1165
1772
  OpenAISummaryProvider = class {
1166
- constructor(apiKey, model, fetchImpl) {
1773
+ constructor(apiKey, model, baseUrl, fetchImpl) {
1167
1774
  this.apiKey = apiKey;
1168
1775
  this.model = model;
1169
1776
  this.fetchImpl = fetchImpl;
1777
+ this.endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`;
1778
+ this.host = new URL(baseUrl).host;
1170
1779
  }
1171
1780
  apiKey;
1172
1781
  model;
1173
1782
  fetchImpl;
1174
1783
  kind = "openai";
1784
+ endpoint;
1785
+ host;
1175
1786
  async generate(input) {
1176
1787
  try {
1177
1788
  const body = JSON.stringify({
@@ -1183,8 +1794,8 @@ var init_summarize = __esm({
1183
1794
  response_format: { type: "json_object" },
1184
1795
  temperature: 0.2
1185
1796
  });
1186
- const { data } = await httpRequest("https://api.openai.com/v1/chat/completions", {
1187
- host: "api.openai.com",
1797
+ const { data } = await httpRequest(this.endpoint, {
1798
+ host: this.host,
1188
1799
  method: "POST",
1189
1800
  ttlMs: 30 * 24 * 60 * 60 * 1e3,
1190
1801
  // cache summaries 30d to control cost
@@ -1371,10 +1982,10 @@ function activeWeightsPath() {
1371
1982
  function loadWeights() {
1372
1983
  if (cachedWeights) return cachedWeights;
1373
1984
  let merged = structuredClone(DEFAULT_WEIGHTS);
1374
- const active = activeWeightsPath();
1375
- if (active) {
1985
+ const active2 = activeWeightsPath();
1986
+ if (active2) {
1376
1987
  try {
1377
- const fromFile = JSON.parse(readFileSync(active.path, "utf8"));
1988
+ const fromFile = JSON.parse(readFileSync(active2.path, "utf8"));
1378
1989
  merged = mergeWeights(merged, fromFile);
1379
1990
  } catch {
1380
1991
  }
@@ -1422,10 +2033,10 @@ function settableKeys() {
1422
2033
  function applyOverrides(base, sets) {
1423
2034
  const next = structuredClone(base);
1424
2035
  for (const entry of sets) {
1425
- const eq7 = entry.indexOf("=");
1426
- if (eq7 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
1427
- const key = entry.slice(0, eq7).trim();
1428
- const value = Number(entry.slice(eq7 + 1).trim());
2036
+ const eq9 = entry.indexOf("=");
2037
+ if (eq9 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
2038
+ const key = entry.slice(0, eq9).trim();
2039
+ const value = Number(entry.slice(eq9 + 1).trim());
1429
2040
  const apply = SETTABLE[key];
1430
2041
  if (!apply) {
1431
2042
  throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
@@ -1589,8 +2200,6 @@ function toScoringInput(signals, category) {
1589
2200
  weeklyDownloads: downloads?.weeklyDownloads ?? null,
1590
2201
  downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
1591
2202
  stars: github?.stars ?? null,
1592
- dependentsCount: null,
1593
- // not collected in v1 (see spec R1 note)
1594
2203
  firstPublishedAt: registry?.firstPublishedAt ?? null,
1595
2204
  lastReleaseAt: github?.lastReleaseAt ?? registry?.lastReleaseAt ?? null,
1596
2205
  releasesLast12mo: github?.releasesLast12mo ?? null,
@@ -1746,7 +2355,8 @@ function computeConfidence(input, now, qualityScore = null) {
1746
2355
  if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
1747
2356
  return "emerging";
1748
2357
  }
1749
- const promisingReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
2358
+ const promisingRecencyMonths = lastReleaseMonths ?? ageMonths;
2359
+ const promisingReleaseOk = promisingRecencyMonths !== null && promisingRecencyMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
1750
2360
  if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
1751
2361
  return "promising";
1752
2362
  }
@@ -1840,7 +2450,12 @@ function localEmbed(text2, dim2 = EMBEDDING_DIM) {
1840
2450
  function createEmbeddingProvider(fetchImpl) {
1841
2451
  const config = getConfig();
1842
2452
  if (config.EMBEDDING_PROVIDER === "openai" && config.EMBEDDING_API_KEY) {
1843
- return new OpenAIEmbeddingProvider(config.EMBEDDING_API_KEY, config.EMBEDDING_MODEL, fetchImpl);
2453
+ return new OpenAIEmbeddingProvider(
2454
+ config.EMBEDDING_API_KEY,
2455
+ config.EMBEDDING_MODEL,
2456
+ config.EMBEDDING_BASE_URL,
2457
+ fetchImpl
2458
+ );
1844
2459
  }
1845
2460
  if (config.EMBEDDING_PROVIDER === "openai" && !config.EMBEDDING_API_KEY) {
1846
2461
  logger.warn("EMBEDDING_API_KEY not set \u2014 falling back to the local embedder.");
@@ -1859,29 +2474,36 @@ var init_embeddings = __esm({
1859
2474
  LocalEmbeddingProvider = class {
1860
2475
  kind = "local";
1861
2476
  dimensions = EMBEDDING_DIM;
2477
+ id = "local";
1862
2478
  async embed(texts) {
1863
2479
  return texts.map((t) => localEmbed(t));
1864
2480
  }
1865
2481
  };
1866
2482
  OPENAI_BATCH = 96;
1867
2483
  OpenAIEmbeddingProvider = class {
1868
- constructor(apiKey, model, fetchImpl) {
2484
+ constructor(apiKey, model, baseUrl, fetchImpl) {
1869
2485
  this.apiKey = apiKey;
1870
2486
  this.model = model;
1871
2487
  this.fetchImpl = fetchImpl;
2488
+ this.id = `openai:${model}`;
2489
+ this.endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
2490
+ this.host = new URL(baseUrl).host;
1872
2491
  }
1873
2492
  apiKey;
1874
2493
  model;
1875
2494
  fetchImpl;
1876
2495
  kind = "openai";
1877
2496
  dimensions = EMBEDDING_DIM;
2497
+ id;
2498
+ endpoint;
2499
+ host;
1878
2500
  async embed(texts) {
1879
2501
  const out = [];
1880
2502
  for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
1881
2503
  const batch = texts.slice(i, i + OPENAI_BATCH);
1882
- const body = JSON.stringify({ model: this.model, input: batch });
1883
- const { data } = await httpRequest("https://api.openai.com/v1/embeddings", {
1884
- host: "api.openai.com",
2504
+ const body = JSON.stringify({ model: this.model, input: batch, dimensions: EMBEDDING_DIM });
2505
+ const { data } = await httpRequest(this.endpoint, {
2506
+ host: this.host,
1885
2507
  method: "POST",
1886
2508
  ttlMs: 30 * 24 * 60 * 60 * 1e3,
1887
2509
  // cache embeddings 30d to control cost
@@ -1892,7 +2514,14 @@ var init_embeddings = __esm({
1892
2514
  });
1893
2515
  const vectors = data?.data ?? [];
1894
2516
  vectors.sort((a, b) => a.index - b.index);
1895
- for (const v of vectors) out.push(v.embedding);
2517
+ for (const v of vectors) {
2518
+ if (v.embedding.length !== EMBEDDING_DIM) {
2519
+ throw new Error(
2520
+ `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.`
2521
+ );
2522
+ }
2523
+ out.push(v.embedding);
2524
+ }
1896
2525
  }
1897
2526
  return out;
1898
2527
  }
@@ -1900,47 +2529,6 @@ var init_embeddings = __esm({
1900
2529
  }
1901
2530
  });
1902
2531
 
1903
- // src/db/packages.ts
1904
- import { eq, sql as sql2 } from "drizzle-orm";
1905
- async function getSeedTargets(db) {
1906
- const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
1907
- return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
1908
- }
1909
- async function getPackageByName(db, name) {
1910
- const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
1911
- return rows[0] ?? null;
1912
- }
1913
- async function ensureSeedEntry(db, name, category) {
1914
- await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
1915
- }
1916
- async function upsertPackage(db, row) {
1917
- const { name: _name, createdAt: _createdAt, ...mutable } = row;
1918
- await db.insert(packages).values(row).onConflictDoUpdate({
1919
- target: packages.name,
1920
- set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
1921
- });
1922
- }
1923
- async function startSyncRun(db) {
1924
- const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
1925
- return row.id;
1926
- }
1927
- async function finishSyncRun(db, id, data) {
1928
- await db.update(syncRuns).set({
1929
- finishedAt: /* @__PURE__ */ new Date(),
1930
- packagesSeen: data.packagesSeen,
1931
- packagesUpdated: data.packagesUpdated,
1932
- errors: data.errors,
1933
- status: data.status
1934
- }).where(eq(syncRuns.id, id));
1935
- }
1936
- var init_packages = __esm({
1937
- "src/db/packages.ts"() {
1938
- "use strict";
1939
- init_esm_shims();
1940
- init_schema();
1941
- }
1942
- });
1943
-
1944
2532
  // src/core/concurrency.ts
1945
2533
  async function pMap(items, mapper, concurrency) {
1946
2534
  const results = new Array(items.length);
@@ -1970,6 +2558,11 @@ async function runSync(opts = {}) {
1970
2558
  const handle = createDb({ max: Math.max(4, config.LURQ_SYNC_CONCURRENCY) });
1971
2559
  const provider = createSummaryProvider();
1972
2560
  logger.info(`Summary provider: ${provider.kind}`);
2561
+ if (!config.GITHUB_TOKEN) {
2562
+ logger.warn(
2563
+ "GITHUB_TOKEN not set \u2014 GitHub signals (stars, issues, release cadence) will be skipped, degrading maintenance/adoption scores. Set it for accurate scoring."
2564
+ );
2565
+ }
1973
2566
  const runId = await startSyncRun(handle.db);
1974
2567
  const allErrors = [];
1975
2568
  try {
@@ -2068,6 +2661,7 @@ async function runSync(opts = {}) {
2068
2661
  healthScore,
2069
2662
  qualityScore: c.quality,
2070
2663
  embedding: embeddings[i] ?? null,
2664
+ embeddingProvider: embProvider.id,
2071
2665
  now
2072
2666
  })
2073
2667
  );
@@ -2081,6 +2675,7 @@ async function runSync(opts = {}) {
2081
2675
  status
2082
2676
  });
2083
2677
  logger.info(`Sync ${status}: ${updated}/${targets.length} updated, ${allErrors.length} source errors.`);
2678
+ if (updated > 0) await invalidateCache();
2084
2679
  return { seen: targets.length, updated, errors: allErrors.length, status };
2085
2680
  } catch (err) {
2086
2681
  await finishSyncRun(handle.db, runId, {
@@ -2140,19 +2735,22 @@ function assemblePackageRow(p) {
2140
2735
  lastReleaseAt: p.input.lastReleaseAt,
2141
2736
  weeklyDownloads: p.input.weeklyDownloads,
2142
2737
  downloadGrowth90d: p.input.downloadGrowth90d,
2143
- dependentsCount: p.input.dependentsCount,
2144
2738
  stars: p.input.stars,
2145
2739
  openIssues: p.input.openIssues,
2146
2740
  closedIssues: p.input.closedIssues,
2147
2741
  scorecard: p.input.scorecard,
2148
2742
  bundleMinGzipKb: p.input.bundleMinGzipKb,
2149
2743
  advisories: p.input.advisories,
2744
+ peerDependencies: r?.peerDependencies ?? null,
2745
+ peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
2746
+ engines: r?.engines ?? null,
2150
2747
  healthScore: p.healthScore,
2151
2748
  qualityScore: p.qualityScore,
2152
2749
  confidence: p.confidence,
2153
2750
  scoreBreakdown: p.breakdown,
2154
2751
  usageGuide: p.usageGuide,
2155
2752
  embedding: p.embedding,
2753
+ embeddingProvider: p.embedding ? p.embeddingProvider : null,
2156
2754
  dataAsOf: p.now
2157
2755
  };
2158
2756
  }
@@ -2160,6 +2758,7 @@ var init_sync = __esm({
2160
2758
  "src/pipeline/sync.ts"() {
2161
2759
  "use strict";
2162
2760
  init_esm_shims();
2761
+ init_cache();
2163
2762
  init_config();
2164
2763
  init_concurrency();
2165
2764
  init_http();
@@ -2176,17 +2775,68 @@ var init_sync = __esm({
2176
2775
  }
2177
2776
  });
2178
2777
 
2179
- // src/pipeline/single.ts
2180
- import { and, eq as eq2, isNotNull, sql as sql3 } from "drizzle-orm";
2181
- async function getSeedCategory(db, name) {
2182
- const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq2(seedPackages.name, name)).limit(1);
2183
- return row?.category ?? null;
2778
+ // src/pipeline/ingestQueue.ts
2779
+ function enqueueIngest(db, name) {
2780
+ if (queuedNames.has(name) || inFlight.has(name)) return;
2781
+ if (pending.length >= MAX_PENDING) {
2782
+ logger.warn(`ingest queue full (${MAX_PENDING}); dropping on-demand request for ${name}`);
2783
+ return;
2784
+ }
2785
+ queuedNames.add(name);
2786
+ pending.push(name);
2787
+ pump(db);
2788
+ }
2789
+ function pump(db) {
2790
+ while (active < MAX_CONCURRENT && pending.length > 0) {
2791
+ const name = pending.shift();
2792
+ queuedNames.delete(name);
2793
+ inFlight.add(name);
2794
+ active += 1;
2795
+ void ingestOne(db, name).finally(() => {
2796
+ inFlight.delete(name);
2797
+ active -= 1;
2798
+ pump(db);
2799
+ });
2800
+ }
2184
2801
  }
2185
- async function getCategoryMedianBundle(db, category) {
2186
- const [row] = await db.select({
2187
- m: sql3`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
2188
- }).from(packages).where(and(eq2(packages.category, category), isNotNull(packages.bundleMinGzipKb)));
2189
- return row?.m ?? null;
2802
+ async function ingestOne(db, name) {
2803
+ try {
2804
+ const row = await syncOnePackage(db, name);
2805
+ if (row.confidence && row.confidence !== "unproven") {
2806
+ await ensureSeedEntry(db, name, row.category);
2807
+ }
2808
+ } catch (err) {
2809
+ logger.warn(`on-demand ingest failed for ${name}: ${String(err)}`);
2810
+ }
2811
+ }
2812
+ var MAX_CONCURRENT, MAX_PENDING, pending, inFlight, queuedNames, active;
2813
+ var init_ingestQueue = __esm({
2814
+ "src/pipeline/ingestQueue.ts"() {
2815
+ "use strict";
2816
+ init_esm_shims();
2817
+ init_logger();
2818
+ init_packages();
2819
+ init_single();
2820
+ MAX_CONCURRENT = 3;
2821
+ MAX_PENDING = 500;
2822
+ pending = [];
2823
+ inFlight = /* @__PURE__ */ new Set();
2824
+ queuedNames = /* @__PURE__ */ new Set();
2825
+ active = 0;
2826
+ }
2827
+ });
2828
+
2829
+ // src/pipeline/single.ts
2830
+ import { and as and3, eq as eq3, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
2831
+ async function getSeedCategory(db, name) {
2832
+ const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq3(seedPackages.name, name)).limit(1);
2833
+ return row?.category ?? null;
2834
+ }
2835
+ async function getCategoryMedianBundle(db, category) {
2836
+ const [row] = await db.select({
2837
+ m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
2838
+ }).from(packages).where(and3(eq3(packages.category, category), isNotNull2(packages.bundleMinGzipKb)));
2839
+ return row?.m ?? null;
2190
2840
  }
2191
2841
  async function syncOnePackage(db, name, opts = {}) {
2192
2842
  const config = getConfig();
@@ -2225,7 +2875,8 @@ async function syncOnePackage(db, name, opts = {}) {
2225
2875
  };
2226
2876
  const healthScore = computeHealthScore(breakdown);
2227
2877
  const confidence = computeConfidence(input, now, quality);
2228
- const [embedding] = await createEmbeddingProvider().embed([
2878
+ const embProvider = createEmbeddingProvider();
2879
+ const [embedding] = await embProvider.embed([
2229
2880
  buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
2230
2881
  ]);
2231
2882
  await upsertPackage(
@@ -2243,9 +2894,14 @@ async function syncOnePackage(db, name, opts = {}) {
2243
2894
  healthScore,
2244
2895
  qualityScore: quality,
2245
2896
  embedding: embedding ?? null,
2897
+ embeddingProvider: embProvider.id,
2246
2898
  now
2247
2899
  })
2248
2900
  );
2901
+ await upsertPackageVersions(db, name, signals.registry?.versionTimeline ?? []).catch(
2902
+ () => {
2903
+ }
2904
+ );
2249
2905
  return await getPackageByName(db, name);
2250
2906
  }
2251
2907
  async function getOrFetchPackage(db, name) {
@@ -2253,9 +2909,8 @@ async function getOrFetchPackage(db, name) {
2253
2909
  if (existing) return { row: existing, wasTracked: true, existsOnNpm: true };
2254
2910
  const exists = await npmPackageExists(name);
2255
2911
  if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
2256
- const row = await syncOnePackage(db, name);
2257
- await ensureSeedEntry(db, name, row.category);
2258
- return { row, wasTracked: false, existsOnNpm: true };
2912
+ enqueueIngest(db, name);
2913
+ return { row: null, wasTracked: false, existsOnNpm: true, queued: true };
2259
2914
  }
2260
2915
  var init_single = __esm({
2261
2916
  "src/pipeline/single.ts"() {
@@ -2271,20 +2926,21 @@ var init_single = __esm({
2271
2926
  init_packages();
2272
2927
  init_schema();
2273
2928
  init_sync();
2929
+ init_ingestQueue();
2274
2930
  }
2275
2931
  });
2276
2932
 
2277
2933
  // src/search/recommend.ts
2278
- import { and as and2, cosineDistance, eq as eq3, isNotNull as isNotNull2, lte, sql as sql4 } from "drizzle-orm";
2934
+ import { and as and4, cosineDistance, eq as eq4, isNotNull as isNotNull3, lte, sql as sql3 } from "drizzle-orm";
2279
2935
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
2280
2936
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
2281
2937
  const [queryVec] = await provider.embed([opts.need]);
2282
2938
  if (!queryVec) return [];
2283
2939
  const category = opts.category ?? inferCategory(opts.need);
2284
2940
  const pool = Math.max(limit * 5, 25);
2285
- let fused = await hybridSearch(db, queryVec, opts.need, opts.constraints, category, pool);
2941
+ let fused = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, category, pool);
2286
2942
  if (category && fused.length < limit) {
2287
- const broad = await hybridSearch(db, queryVec, opts.need, opts.constraints, null, pool);
2943
+ const broad = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, null, pool);
2288
2944
  const seen = new Set(fused.map((f) => f.row.name));
2289
2945
  fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
2290
2946
  }
@@ -2298,9 +2954,9 @@ async function recommend(db, opts, provider = createEmbeddingProvider()) {
2298
2954
  }).sort((a, b) => b.score - a.score).slice(0, limit);
2299
2955
  return ranked.map(({ row }) => toCandidate(row));
2300
2956
  }
2301
- async function hybridSearch(db, queryVec, need, constraints, category, pool) {
2957
+ async function hybridSearch(db, queryVec, providerId, need, constraints, category, pool) {
2302
2958
  const [vectorRows, lexicalRows] = await Promise.all([
2303
- runVectorQuery(db, queryVec, constraints, category, pool),
2959
+ runVectorQuery(db, queryVec, providerId, constraints, category, pool),
2304
2960
  runLexicalQuery(db, need, constraints, category, pool)
2305
2961
  ]);
2306
2962
  return rrfFuse([vectorRows, lexicalRows]);
@@ -2319,8 +2975,8 @@ function rrfFuse(lists, k = RRF_K) {
2319
2975
  }
2320
2976
  function buildConditions(constraints, category) {
2321
2977
  const conditions = [];
2322
- if (category) conditions.push(eq3(packages.category, category));
2323
- if (constraints?.license) conditions.push(eq3(packages.license, constraints.license));
2978
+ if (category) conditions.push(eq4(packages.category, category));
2979
+ if (constraints?.license) conditions.push(eq4(packages.license, constraints.license));
2324
2980
  if (constraints?.maxBundleKb !== void 0) {
2325
2981
  conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
2326
2982
  }
@@ -2329,21 +2985,25 @@ function buildConditions(constraints, category) {
2329
2985
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
2330
2986
  );
2331
2987
  conditions.push(
2332
- sql4`${packages.confidence} in ${sql4.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
2988
+ sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
2333
2989
  );
2334
2990
  }
2335
2991
  return conditions;
2336
2992
  }
2337
- async function runVectorQuery(db, queryVec, constraints, category, pool) {
2993
+ async function runVectorQuery(db, queryVec, providerId, constraints, category, pool) {
2338
2994
  const distance = cosineDistance(packages.embedding, queryVec);
2339
- const conditions = [isNotNull2(packages.embedding), ...buildConditions(constraints, category)];
2340
- return db.select(ROW_COLUMNS).from(packages).where(and2(...conditions)).orderBy(distance).limit(pool);
2995
+ const conditions = [
2996
+ isNotNull3(packages.embedding),
2997
+ eq4(packages.embeddingProvider, providerId),
2998
+ ...buildConditions(constraints, category)
2999
+ ];
3000
+ return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(distance).limit(pool);
2341
3001
  }
2342
3002
  async function runLexicalQuery(db, need, constraints, category, pool) {
2343
- const tsquery = sql4`websearch_to_tsquery('english', ${need})`;
2344
- const rank = sql4`ts_rank(${packages.searchVector}, ${tsquery})`;
2345
- const conditions = [sql4`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
2346
- return db.select(ROW_COLUMNS).from(packages).where(and2(...conditions)).orderBy(sql4`${rank} desc`).limit(pool);
3003
+ const tsquery = sql3`websearch_to_tsquery('english', ${need})`;
3004
+ const rank = sql3`ts_rank(${packages.searchVector}, ${tsquery})`;
3005
+ const conditions = [sql3`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
3006
+ return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(sql3`${rank} desc`).limit(pool);
2347
3007
  }
2348
3008
  function toCandidate(row) {
2349
3009
  return {
@@ -2404,12 +3064,316 @@ var init_recommend = __esm({
2404
3064
  }
2405
3065
  });
2406
3066
 
3067
+ // src/security/risk.ts
3068
+ function assessRisk(i) {
3069
+ const malwarePattern = i.installScripts && i.brandNew && i.lowTrust;
3070
+ if (i.typosquat || i.hasCriticalOrHighAdvisory || malwarePattern) return "high";
3071
+ if (i.deprecatedOrArchived || i.installScripts && (i.lowTrust || i.brandNew) || i.lowTrust && i.flags.includes("single-maintainer")) {
3072
+ return "medium";
3073
+ }
3074
+ return "low";
3075
+ }
3076
+ var init_risk = __esm({
3077
+ "src/security/risk.ts"() {
3078
+ "use strict";
3079
+ init_esm_shims();
3080
+ }
3081
+ });
3082
+
3083
+ // src/data/popular-packages.json
3084
+ var popular_packages_default;
3085
+ var init_popular_packages = __esm({
3086
+ "src/data/popular-packages.json"() {
3087
+ popular_packages_default = [
3088
+ "react",
3089
+ "react-dom",
3090
+ "react-router",
3091
+ "react-router-dom",
3092
+ "next",
3093
+ "vue",
3094
+ "vue-router",
3095
+ "pinia",
3096
+ "vuex",
3097
+ "svelte",
3098
+ "solid-js",
3099
+ "preact",
3100
+ "angular",
3101
+ "@angular/core",
3102
+ "jquery",
3103
+ "lodash",
3104
+ "underscore",
3105
+ "ramda",
3106
+ "immer",
3107
+ "rxjs",
3108
+ "express",
3109
+ "koa",
3110
+ "fastify",
3111
+ "@nestjs/core",
3112
+ "hapi",
3113
+ "connect",
3114
+ "body-parser",
3115
+ "cookie-parser",
3116
+ "cors",
3117
+ "helmet",
3118
+ "morgan",
3119
+ "compression",
3120
+ "express-session",
3121
+ "express-rate-limit",
3122
+ "multer",
3123
+ "passport",
3124
+ "jsonwebtoken",
3125
+ "bcrypt",
3126
+ "bcryptjs",
3127
+ "axios",
3128
+ "node-fetch",
3129
+ "got",
3130
+ "undici",
3131
+ "superagent",
3132
+ "request",
3133
+ "cheerio",
3134
+ "puppeteer",
3135
+ "playwright",
3136
+ "cypress",
3137
+ "jsdom",
3138
+ "ws",
3139
+ "socket.io",
3140
+ "socket.io-client",
3141
+ "graphql",
3142
+ "@apollo/client",
3143
+ "apollo-server",
3144
+ "dataloader",
3145
+ "mongoose",
3146
+ "mongodb",
3147
+ "pg",
3148
+ "mysql2",
3149
+ "sequelize",
3150
+ "prisma",
3151
+ "knex",
3152
+ "drizzle-orm",
3153
+ "typeorm",
3154
+ "redis",
3155
+ "ioredis",
3156
+ "sqlite3",
3157
+ "better-sqlite3",
3158
+ "typescript",
3159
+ "eslint",
3160
+ "prettier",
3161
+ "tslint",
3162
+ "webpack",
3163
+ "vite",
3164
+ "rollup",
3165
+ "esbuild",
3166
+ "parcel",
3167
+ "@babel/core",
3168
+ "babel-core",
3169
+ "babel-loader",
3170
+ "ts-node",
3171
+ "tsx",
3172
+ "tsup",
3173
+ "nodemon",
3174
+ "concurrently",
3175
+ "npm-run-all",
3176
+ "jest",
3177
+ "vitest",
3178
+ "mocha",
3179
+ "chai",
3180
+ "sinon",
3181
+ "ava",
3182
+ "tape",
3183
+ "supertest",
3184
+ "@testing-library/react",
3185
+ "enzyme",
3186
+ "chalk",
3187
+ "colors",
3188
+ "kleur",
3189
+ "picocolors",
3190
+ "ansi-colors",
3191
+ "commander",
3192
+ "yargs",
3193
+ "meow",
3194
+ "minimist",
3195
+ "inquirer",
3196
+ "prompts",
3197
+ "ora",
3198
+ "boxen",
3199
+ "figlet",
3200
+ "cli-progress",
3201
+ "debug",
3202
+ "dotenv",
3203
+ "cross-env",
3204
+ "rimraf",
3205
+ "glob",
3206
+ "fast-glob",
3207
+ "chokidar",
3208
+ "fs-extra",
3209
+ "execa",
3210
+ "cross-spawn",
3211
+ "shelljs",
3212
+ "which",
3213
+ "semver",
3214
+ "uuid",
3215
+ "nanoid",
3216
+ "ulid",
3217
+ "moment",
3218
+ "dayjs",
3219
+ "date-fns",
3220
+ "luxon",
3221
+ "zod",
3222
+ "yup",
3223
+ "joi",
3224
+ "ajv",
3225
+ "validator",
3226
+ "class-validator",
3227
+ "winston",
3228
+ "pino",
3229
+ "bunyan",
3230
+ "nodemailer",
3231
+ "node-cron",
3232
+ "bull",
3233
+ "bullmq",
3234
+ "kafkajs",
3235
+ "amqplib",
3236
+ "sharp",
3237
+ "jimp",
3238
+ "archiver",
3239
+ "tar",
3240
+ "adm-zip",
3241
+ "qs",
3242
+ "query-string",
3243
+ "form-data",
3244
+ "mime-types",
3245
+ "http-errors",
3246
+ "classnames",
3247
+ "clsx",
3248
+ "styled-components",
3249
+ "@emotion/react",
3250
+ "tailwindcss",
3251
+ "postcss",
3252
+ "autoprefixer",
3253
+ "sass",
3254
+ "less",
3255
+ "framer-motion",
3256
+ "three",
3257
+ "d3",
3258
+ "chart.js",
3259
+ "recharts",
3260
+ "gsap",
3261
+ "swiper",
3262
+ "leaflet",
3263
+ "mapbox-gl",
3264
+ "formik",
3265
+ "react-hook-form",
3266
+ "redux",
3267
+ "@reduxjs/toolkit",
3268
+ "zustand",
3269
+ "jotai",
3270
+ "mobx",
3271
+ "bootstrap",
3272
+ "@popperjs/core",
3273
+ "aws-sdk",
3274
+ "@aws-sdk/client-s3",
3275
+ "firebase",
3276
+ "firebase-admin",
3277
+ "stripe",
3278
+ "twilio",
3279
+ "openai",
3280
+ "@anthropic-ai/sdk",
3281
+ "googleapis",
3282
+ "bignumber.js",
3283
+ "decimal.js",
3284
+ "mathjs",
3285
+ "slugify",
3286
+ "faker",
3287
+ "@faker-js/faker",
3288
+ "puppeteer-core"
3289
+ ];
3290
+ }
3291
+ });
3292
+
3293
+ // src/security/typosquat.ts
3294
+ function typosquatCorpus(trackedTopNames) {
3295
+ return [.../* @__PURE__ */ new Set([...POPULAR_BASELINE, ...trackedTopNames])];
3296
+ }
3297
+ function bareName(name) {
3298
+ const slash = name.indexOf("/");
3299
+ return (slash >= 0 ? name.slice(slash + 1) : name).toLowerCase();
3300
+ }
3301
+ function editDistance(a, b) {
3302
+ const m = a.length;
3303
+ const n = b.length;
3304
+ if (m === 0) return n;
3305
+ if (n === 0) return m;
3306
+ let prevPrev = new Array(n + 1).fill(0);
3307
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
3308
+ for (let i = 1; i <= m; i++) {
3309
+ const cur = new Array(n + 1).fill(0);
3310
+ cur[0] = i;
3311
+ for (let j = 1; j <= n; j++) {
3312
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3313
+ let val = Math.min(
3314
+ (prev[j] ?? 0) + 1,
3315
+ (cur[j - 1] ?? 0) + 1,
3316
+ (prev[j - 1] ?? 0) + cost
3317
+ );
3318
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
3319
+ val = Math.min(val, (prevPrev[j - 2] ?? 0) + 1);
3320
+ }
3321
+ cur[j] = val;
3322
+ }
3323
+ prevPrev = prev;
3324
+ prev = cur;
3325
+ }
3326
+ return prev[n] ?? 0;
3327
+ }
3328
+ function detectTyposquat(name, popular, maxDistance = 2) {
3329
+ const target = bareName(name);
3330
+ if (target.length < 4) return null;
3331
+ if (popular.some((p) => p.toLowerCase() === name.toLowerCase())) return null;
3332
+ let best = null;
3333
+ for (const p of popular) {
3334
+ const cand = bareName(p);
3335
+ if (cand === target) continue;
3336
+ if (Math.abs(cand.length - target.length) > maxDistance) continue;
3337
+ const dist = editDistance(target, cand);
3338
+ if (dist >= 1 && dist <= maxDistance && (!best || dist < best.distance)) {
3339
+ best = { target: p, distance: dist };
3340
+ if (dist === 1) break;
3341
+ }
3342
+ }
3343
+ return best;
3344
+ }
3345
+ var POPULAR_BASELINE;
3346
+ var init_typosquat = __esm({
3347
+ "src/security/typosquat.ts"() {
3348
+ "use strict";
3349
+ init_esm_shims();
3350
+ init_popular_packages();
3351
+ POPULAR_BASELINE = popular_packages_default;
3352
+ }
3353
+ });
3354
+
2407
3355
  // src/mcp/handlers.ts
2408
- import { sql as sql5 } from "drizzle-orm";
3356
+ var handlers_exports = {};
3357
+ __export(handlers_exports, {
3358
+ handleCompare: () => handleCompare,
3359
+ handleCompat: () => handleCompat,
3360
+ handleEvaluate: () => handleEvaluate,
3361
+ handleRecommend: () => handleRecommend,
3362
+ handleReportOutcome: () => handleReportOutcome,
3363
+ handleVerify: () => handleVerify,
3364
+ latestDataAsOf: () => latestDataAsOf,
3365
+ rowToEvaluate: () => rowToEvaluate
3366
+ });
3367
+ import { createHash as createHash3 } from "crypto";
3368
+ import { sql as sql4 } from "drizzle-orm";
2409
3369
  function isStale(dataAsOf) {
2410
3370
  if (!dataAsOf) return true;
2411
3371
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
2412
3372
  }
3373
+ function refreshStale(out) {
3374
+ out.stale = isStale(out.dataAsOf ? new Date(out.dataAsOf) : null) || void 0;
3375
+ return out;
3376
+ }
2413
3377
  function withinDays(date, days) {
2414
3378
  return date ? Date.now() - date.getTime() <= days * DAY_MS2 : false;
2415
3379
  }
@@ -2438,7 +3402,6 @@ function rowToEvaluate(row) {
2438
3402
  lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
2439
3403
  weeklyDownloads: row.weeklyDownloads,
2440
3404
  downloadGrowth90d: row.downloadGrowth90d,
2441
- dependentsCount: row.dependentsCount,
2442
3405
  scorecard: row.scorecard,
2443
3406
  bundleMinGzipKb: row.bundleMinGzipKb,
2444
3407
  deprecated: row.deprecated,
@@ -2446,44 +3409,95 @@ function rowToEvaluate(row) {
2446
3409
  advisories: topAdvisories(row.advisories),
2447
3410
  summary: row.summary ? truncateSentences(row.summary, 3) : null,
2448
3411
  usageGuide: row.usageGuide ?? null,
2449
- repoUrl: row.repoUrl
3412
+ repoUrl: row.repoUrl,
3413
+ replacedBy: lookupSuccessor(row.name)
2450
3414
  };
2451
3415
  }
2452
3416
  async function latestDataAsOf(db) {
2453
- const [row] = await db.select({ m: sql5`max(${packages.dataAsOf})` }).from(packages);
3417
+ const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
2454
3418
  return new Date(row?.m ?? Date.now()).toISOString();
2455
3419
  }
3420
+ function cacheKey(parts) {
3421
+ return createHash3("sha1").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
3422
+ }
2456
3423
  async function handleRecommend(db, input) {
2457
- const candidates = await recommend(db, {
2458
- need: input.need,
2459
- category: input.category,
2460
- constraints: input.constraints,
2461
- limit: 5
2462
- });
2463
- return { dataAsOf: await latestDataAsOf(db), candidates };
3424
+ return cached2(
3425
+ "rec",
3426
+ cacheKey([input.need, input.category ?? null, input.constraints ?? null]),
3427
+ async () => {
3428
+ const candidates = await recommend(db, {
3429
+ need: input.need,
3430
+ category: input.category,
3431
+ constraints: input.constraints,
3432
+ limit: 5
3433
+ });
3434
+ return { dataAsOf: await latestDataAsOf(db), candidates };
3435
+ },
3436
+ // Don't cache empty results — the index may still be populating.
3437
+ { skipCache: (r) => r.candidates.length === 0 }
3438
+ );
2464
3439
  }
2465
3440
  async function handleEvaluate(db, input) {
2466
- const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
2467
- if (!row) {
2468
- return {
2469
- tracked: false,
2470
- 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.`
2471
- };
2472
- }
2473
- return rowToEvaluate(row);
3441
+ const out = await cached2(
3442
+ "eval",
3443
+ cacheKey([input.package]),
3444
+ async () => {
3445
+ const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
3446
+ if (!row) {
3447
+ return {
3448
+ tracked: false,
3449
+ suggestion: existsOnNpm ? `\u{1F389} Congrats \u2014 you're the first to add "${input.package}" to lurq's registry! It's being fetched and scored now; retry in a few seconds for the full evidence read.` : `"${input.package}" was not found on the npm registry. Check the package name.`
3450
+ };
3451
+ }
3452
+ const evaluated = rowToEvaluate(row);
3453
+ const verification = await getLatestVerificationByName(db, row.name);
3454
+ return verification ? { ...evaluated, buildVerified: toBuildVerified(verification) } : evaluated;
3455
+ },
3456
+ // Don't cache "not found / not scored yet" — it may resolve on a later fetch.
3457
+ { skipCache: (r) => "tracked" in r }
3458
+ );
3459
+ return "tracked" in out ? out : refreshStale(out);
2474
3460
  }
2475
3461
  async function handleCompare(db, input) {
2476
- const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
2477
- const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
2478
- const missing = input.packages.filter(
2479
- (name) => !rows.some((r) => r.name === name)
3462
+ const out = await cached2(
3463
+ "cmp",
3464
+ cacheKey(input.packages),
3465
+ async () => {
3466
+ const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
3467
+ const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
3468
+ const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
3469
+ return {
3470
+ dataAsOf: await latestDataAsOf(db),
3471
+ rows,
3472
+ ...missing.length ? {
3473
+ missing,
3474
+ note: "\u{1F389} You're the first to add these to lurq's registry! They're being scored now; retry shortly for the full comparison."
3475
+ } : {}
3476
+ };
3477
+ },
3478
+ // Don't cache a transient miss (a package that momentarily failed to fetch).
3479
+ { skipCache: (r) => Boolean(r.missing?.length) }
2480
3480
  );
2481
- return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
3481
+ out.rows = out.rows.map(refreshStale);
3482
+ return out;
3483
+ }
3484
+ function toBuildVerified(v) {
3485
+ return {
3486
+ version: v.version,
3487
+ installed: v.installed,
3488
+ loaded: v.imported,
3489
+ driver: v.driver,
3490
+ ranAt: v.ranAt ? v.ranAt.toISOString() : ""
3491
+ };
3492
+ }
3493
+ async function handleCompat(db, input) {
3494
+ return checkCompat(db, input.packages);
2482
3495
  }
2483
3496
  async function handleVerify(db, input) {
2484
3497
  const name = input.package;
2485
3498
  const exists = await npmPackageExists(name);
2486
3499
  if (!exists) {
3500
+ const typo2 = detectTyposquat(name, typosquatCorpus(await getTopPackageNames(db).catch(() => [])));
2487
3501
  return {
2488
3502
  exists: false,
2489
3503
  tracked: false,
@@ -2491,25 +3505,46 @@ async function handleVerify(db, input) {
2491
3505
  archived: false,
2492
3506
  latestVersion: null,
2493
3507
  weeklyDownloads: null,
2494
- riskFlags: ["not-found-on-registry"],
3508
+ riskFlags: typo2 ? ["not-found-on-registry", `possible-typosquat-of:${typo2.target}`] : ["not-found-on-registry"],
3509
+ risk: "high",
3510
+ typosquatOf: typo2?.target ?? null,
2495
3511
  confidence: null,
2496
3512
  advisoryCount: 0
2497
3513
  };
2498
3514
  }
2499
- const registry = await fetchNpmRegistry(name).catch(() => null);
2500
- const { row, wasTracked } = await getOrFetchPackage(db, name);
2501
- const weeklyDownloads = row?.weeklyDownloads ?? null;
2502
- const advisoryCount = row?.advisories?.length ?? 0;
3515
+ const [registry, { row, wasTracked }, popular] = await Promise.all([
3516
+ fetchNpmRegistry(name).catch(() => null),
3517
+ getOrFetchPackage(db, name),
3518
+ getTopPackageNames(db).catch(() => [])
3519
+ ]);
3520
+ const weeklyDownloads = row?.weeklyDownloads ?? await fetchWeeklyDownloads(name).catch(() => null);
3521
+ const advisories = row?.advisories ?? [];
3522
+ const advisoryCount = advisories.length;
2503
3523
  const deprecated = Boolean(row?.deprecated || registry?.deprecated);
2504
3524
  const archived = Boolean(row?.archived);
3525
+ const brandNew = withinDays(registry?.firstPublishedAt ?? null, 7);
3526
+ const lowTrust = weeklyDownloads === null || weeklyDownloads < 1e3;
3527
+ const installScripts = registry?.hasInstallScripts ?? false;
3528
+ const typo = detectTyposquat(name, typosquatCorpus(popular));
2505
3529
  const riskFlags = [];
3530
+ if (typo) riskFlags.push(`possible-typosquat-of:${typo.target}`);
2506
3531
  if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
2507
3532
  else if (weeklyDownloads < 1e3) riskFlags.push("low-downloads");
2508
- if (withinDays(registry?.firstPublishedAt ?? null, 7)) riskFlags.push("published-within-7-days");
3533
+ if (brandNew) riskFlags.push("published-within-7-days");
2509
3534
  if (registry?.maintainersCount === 1) riskFlags.push("single-maintainer");
3535
+ if (installScripts) riskFlags.push("runs-install-scripts");
2510
3536
  if (advisoryCount > 0) riskFlags.push("has-known-advisory");
2511
3537
  if (deprecated) riskFlags.push("deprecated");
2512
3538
  if (archived) riskFlags.push("archived");
3539
+ const risk = assessRisk({
3540
+ flags: riskFlags,
3541
+ hasCriticalOrHighAdvisory: hasCriticalOrHighAdvisory(advisories),
3542
+ typosquat: Boolean(typo),
3543
+ installScripts,
3544
+ brandNew,
3545
+ lowTrust,
3546
+ deprecatedOrArchived: deprecated || archived
3547
+ });
2513
3548
  return {
2514
3549
  exists: true,
2515
3550
  tracked: wasTracked,
@@ -2518,21 +3553,42 @@ async function handleVerify(db, input) {
2518
3553
  latestVersion: registry?.latestVersion ?? row?.latestVersion ?? null,
2519
3554
  weeklyDownloads,
2520
3555
  riskFlags,
3556
+ risk,
3557
+ typosquatOf: typo?.target ?? null,
2521
3558
  confidence: row?.confidence ?? null,
2522
3559
  advisoryCount
2523
3560
  };
2524
3561
  }
3562
+ async function handleReportOutcome(db, input, ownerId = null) {
3563
+ await recordOutcome(db, {
3564
+ ownerId,
3565
+ packageName: input.package,
3566
+ accepted: input.accepted,
3567
+ buildSignal: input.buildSignal ?? null,
3568
+ need: input.need ?? null
3569
+ });
3570
+ return { recorded: true };
3571
+ }
2525
3572
  var SEVERITY_RANK, DAY_MS2;
2526
3573
  var init_handlers = __esm({
2527
3574
  "src/mcp/handlers.ts"() {
2528
3575
  "use strict";
2529
3576
  init_esm_shims();
3577
+ init_cache();
2530
3578
  init_constants();
3579
+ init_check();
3580
+ init_packages();
3581
+ init_verification();
3582
+ init_outcomes();
3583
+ init_successors2();
2531
3584
  init_schema();
2532
3585
  init_sources();
2533
3586
  init_summarize();
2534
3587
  init_single();
3588
+ init_score();
2535
3589
  init_recommend();
3590
+ init_risk();
3591
+ init_typosquat();
2536
3592
  SEVERITY_RANK = {
2537
3593
  critical: 4,
2538
3594
  high: 3,
@@ -2545,7 +3601,7 @@ var init_handlers = __esm({
2545
3601
  });
2546
3602
 
2547
3603
  // src/mcp/diagram.ts
2548
- import { inArray } from "drizzle-orm";
3604
+ import { inArray as inArray2 } from "drizzle-orm";
2549
3605
  function layerFor(item) {
2550
3606
  if (!item.category) return UNCLASSIFIED;
2551
3607
  if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
@@ -2598,7 +3654,7 @@ async function handleDiagram(db, input) {
2598
3654
  note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
2599
3655
  };
2600
3656
  }
2601
- const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray(packages.name, input.stack));
3657
+ const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray2(packages.name, input.stack));
2602
3658
  const known = new Map(rows.map((r) => [r.name, r.category]));
2603
3659
  const items = input.stack.map((name) => ({
2604
3660
  label: name,
@@ -2652,19 +3708,502 @@ var init_diagram = __esm({
2652
3708
  }
2653
3709
  });
2654
3710
 
3711
+ // src/compat/optimize.ts
3712
+ function conflictsFor(members, sandboxConflicts) {
3713
+ const out = resolveArchitectureCompat(members);
3714
+ for (let i = 0; i < members.length; i++) {
3715
+ for (let j = i + 1; j < members.length; j++) {
3716
+ const a = members[i].name;
3717
+ const b = members[j].name;
3718
+ const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
3719
+ if (sandboxConflicts.has(key)) {
3720
+ out.push({
3721
+ source: "sandbox",
3722
+ packages: [a, b],
3723
+ detail: `${a} and ${b} are recorded as incompatible (sandbox)`
3724
+ });
3725
+ }
3726
+ }
3727
+ }
3728
+ return out;
3729
+ }
3730
+ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
3731
+ if (slots.some((s) => s.length === 0)) {
3732
+ throw new Error("optimizeStack: every slot must have at least one candidate");
3733
+ }
3734
+ const n = slots.length;
3735
+ const budget = 5e4;
3736
+ let nodes = 0;
3737
+ let exhausted = false;
3738
+ let bestSelection = new Array(n).fill(0);
3739
+ let bestRegret = conflictsFor(
3740
+ slots.map((c) => c[0]).filter((m) => Boolean(m)),
3741
+ sandboxConflicts
3742
+ ).length === 0 ? 0 : Infinity;
3743
+ const chosen = new Array(n).fill(0);
3744
+ const dfs = (slot, regret) => {
3745
+ if (nodes++ > budget) {
3746
+ exhausted = true;
3747
+ return;
3748
+ }
3749
+ if (regret >= bestRegret) return;
3750
+ if (slot === n) {
3751
+ bestSelection = chosen.slice();
3752
+ bestRegret = regret;
3753
+ return;
3754
+ }
3755
+ const candidates = slots[slot];
3756
+ for (let i = 0; i < candidates.length; i++) {
3757
+ chosen[slot] = i;
3758
+ const assigned = chosen.slice(0, slot + 1).map((idx, s) => slots[s][idx]);
3759
+ if (conflictsFor(assigned, sandboxConflicts).length === 0) {
3760
+ dfs(slot + 1, regret + i);
3761
+ }
3762
+ if (nodes > budget) {
3763
+ exhausted = true;
3764
+ return;
3765
+ }
3766
+ }
3767
+ };
3768
+ dfs(0, 0);
3769
+ const members = bestSelection.map((idx, s) => slots[s][idx]);
3770
+ return {
3771
+ selection: bestSelection,
3772
+ conflicts: conflictsFor(members, sandboxConflicts),
3773
+ regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret,
3774
+ bounded: exhausted
3775
+ };
3776
+ }
3777
+ var init_optimize = __esm({
3778
+ "src/compat/optimize.ts"() {
3779
+ "use strict";
3780
+ init_esm_shims();
3781
+ init_peerCompat();
3782
+ }
3783
+ });
3784
+
3785
+ // src/mcp/plan.ts
3786
+ var plan_exports = {};
3787
+ __export(plan_exports, {
3788
+ decomposeHeuristic: () => decomposeHeuristic,
3789
+ familyOf: () => familyOf,
3790
+ flagSlotConflicts: () => flagSlotConflicts,
3791
+ handlePlan: () => handlePlan,
3792
+ orderCandidates: () => orderCandidates,
3793
+ resolvePins: () => resolvePins
3794
+ });
3795
+ import { createHash as createHash4 } from "crypto";
3796
+ import { inArray as inArray3 } from "drizzle-orm";
3797
+ function packageToCandidate(row) {
3798
+ return {
3799
+ name: row.name,
3800
+ category: row.category,
3801
+ healthScore: row.healthScore ?? 0,
3802
+ qualityScore: row.qualityScore,
3803
+ confidence: row.confidence ?? "unproven",
3804
+ why: "pinned by you",
3805
+ latestVersion: row.latestVersion,
3806
+ weeklyDownloads: row.weeklyDownloads,
3807
+ lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
3808
+ repoUrl: row.repoUrl
3809
+ };
3810
+ }
3811
+ async function resolvePins(db, using, recommendedNames) {
3812
+ const slots = [];
3813
+ const unresolved = [];
3814
+ for (const name of new Set(using ?? [])) {
3815
+ if (recommendedNames.has(name)) continue;
3816
+ const { row } = await getOrFetchPackage(db, name);
3817
+ if (!row) {
3818
+ unresolved.push(name);
3819
+ continue;
3820
+ }
3821
+ slots.push({
3822
+ need: `using ${name}`,
3823
+ category: row.category,
3824
+ layer: layerFor({ label: name, category: row.category }),
3825
+ recommended: packageToCandidate(row),
3826
+ alternatives: [],
3827
+ // fixed: the user chose this, so it never gets swapped
3828
+ note: "pinned by you"
3829
+ });
3830
+ }
3831
+ return { slots, unresolved };
3832
+ }
3833
+ async function handlePlan(db, input) {
3834
+ const optimize = input.optimize ?? "balanced";
3835
+ const decomposed = input.needs?.length ? { needs: dedupeNeeds(input.needs), source: "needs" } : input.document?.trim() ? await decompose(input.document) : null;
3836
+ const hasPins = Boolean(input.using?.length);
3837
+ if ((!decomposed || decomposed.needs.length === 0) && !hasPins) {
3838
+ return {
3839
+ 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."
3840
+ };
3841
+ }
3842
+ const needs = (decomposed?.needs ?? []).slice(0, MAX_SLOTS);
3843
+ const source = decomposed?.source ?? "needs";
3844
+ const safeRecommend = (need, category) => recommend(db, { need, category, limit: PER_SLOT }).catch((err) => {
3845
+ logger.warn(`plan: recommend failed for "${need}": ${err.message}`);
3846
+ return [];
3847
+ });
3848
+ const effCat = (n) => n.category ?? inferCategory(n.need) ?? void 0;
3849
+ const anchorFamily = familyOf(`${input.document ?? ""} ${needs.map((n) => n.need).join(" ")}`);
3850
+ const metaIdx = needs.findIndex((n) => effCat(n) === "meta-framework");
3851
+ const anchorIdx = metaIdx >= 0 ? metaIdx : needs.findIndex((n) => effCat(n) === "framework");
3852
+ const recs = new Array(needs.length);
3853
+ let framework = anchorFamily;
3854
+ if (anchorIdx >= 0) {
3855
+ recs[anchorIdx] = await safeRecommend(needs[anchorIdx].need, effCat(needs[anchorIdx]));
3856
+ framework = recs[anchorIdx][0]?.name ?? anchorFamily;
3857
+ }
3858
+ const [, dataAsOf] = await Promise.all([
3859
+ Promise.all(
3860
+ needs.map(async (n, i) => {
3861
+ if (i === anchorIdx) return;
3862
+ const need = framework ? `${n.need} (for a ${framework} app)` : n.need;
3863
+ recs[i] = await safeRecommend(need, effCat(n));
3864
+ })
3865
+ ),
3866
+ latestDataAsOf(db)
3867
+ ]);
3868
+ const bundleByName = optimize === "speed" ? await bundleSizes(db, recs.flat()) : /* @__PURE__ */ new Map();
3869
+ const recSlots = needs.map((n, i) => {
3870
+ const candidates = orderCandidates(recs[i], anchorFamily, optimize, bundleByName);
3871
+ const recommended = candidates[0] ?? null;
3872
+ const category = n.category ?? recommended?.category ?? inferCategory(n.need);
3873
+ return {
3874
+ need: n.need,
3875
+ category,
3876
+ layer: layerFor({ label: recommended?.name ?? n.need, category }),
3877
+ recommended,
3878
+ alternatives: candidates.slice(1),
3879
+ note: recommended ? void 0 : "no tracked package matched this need yet"
3880
+ };
3881
+ });
3882
+ const recommendedNames = new Set(
3883
+ recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
3884
+ );
3885
+ const { slots: pinnedSlots, unresolved: unresolvedPins } = await resolvePins(
3886
+ db,
3887
+ input.using,
3888
+ recommendedNames
3889
+ );
3890
+ const slots = [...pinnedSlots, ...recSlots];
3891
+ const compatibility = await resolveCompat(db, slots);
3892
+ const unmatched = [
3893
+ ...slots.filter((s) => !s.recommended).map((s) => s.need),
3894
+ ...unresolvedPins.map((n) => `${n} (pinned, but not found on npm)`)
3895
+ ];
3896
+ const mermaid = buildMermaid(
3897
+ slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
3898
+ );
3899
+ return {
3900
+ dataAsOf,
3901
+ optimize,
3902
+ source,
3903
+ framework,
3904
+ slots,
3905
+ unmatched,
3906
+ mermaid,
3907
+ note: planNote(source, unmatched.length, optimize, framework),
3908
+ compatibility
3909
+ };
3910
+ }
3911
+ function flagSlotConflicts(picks, conflicts) {
3912
+ const out = /* @__PURE__ */ new Map();
3913
+ for (const c of conflicts) {
3914
+ for (const p of picks) {
3915
+ if (p.name && c.packages.includes(p.name)) {
3916
+ const others = c.packages.filter((n) => n !== p.name);
3917
+ out.set(p.need, [.../* @__PURE__ */ new Set([...out.get(p.need) ?? [], ...others])]);
3918
+ }
3919
+ }
3920
+ }
3921
+ return out;
3922
+ }
3923
+ async function resolveCompat(db, slots) {
3924
+ const eligible = slots.filter((s) => s.recommended);
3925
+ if (eligible.length < 2) return null;
3926
+ try {
3927
+ const allNames = [
3928
+ ...new Set(eligible.flatMap((s) => [s.recommended, ...s.alternatives].map((c) => c.name)))
3929
+ ];
3930
+ const [{ members }, edges] = await Promise.all([
3931
+ assembleMembers(db, allNames),
3932
+ getCompatEdges(db, allNames)
3933
+ ]);
3934
+ const metaByName = new Map(members.map((m) => [m.name, m]));
3935
+ const sandboxConflicts = new Set(
3936
+ edges.filter((e) => e.status === "conflict").map((e) => `${e.packageA}|${e.packageB}`)
3937
+ );
3938
+ const slotCandidates = eligible.map(
3939
+ (s) => [s.recommended, ...s.alternatives].map((c) => metaByName.get(c.name) ?? NO_META(c))
3940
+ );
3941
+ const { selection } = optimizeStack(slotCandidates, sandboxConflicts);
3942
+ eligible.forEach((s, i) => {
3943
+ const idx = selection[i] ?? 0;
3944
+ if (idx <= 0) return;
3945
+ const all = [s.recommended, ...s.alternatives];
3946
+ const chosen = all[idx];
3947
+ if (!chosen) return;
3948
+ s.recommended = chosen;
3949
+ s.alternatives = all.filter((c) => c.name !== chosen.name);
3950
+ s.swappedFrom = all[0].name;
3951
+ });
3952
+ } catch (err) {
3953
+ logger.warn(`plan: compat optimization failed: ${String(err)}`);
3954
+ }
3955
+ const compat = await checkCompat(
3956
+ db,
3957
+ eligible.map((s) => s.recommended.name)
3958
+ ).catch(() => null);
3959
+ if (compat) {
3960
+ const flags = flagSlotConflicts(
3961
+ slots.map((s) => ({ need: s.need, name: s.recommended?.name ?? null })),
3962
+ compat.conflicts
3963
+ );
3964
+ for (const s of slots) {
3965
+ const cw = flags.get(s.need);
3966
+ s.conflictsWith = cw?.length ? cw : void 0;
3967
+ }
3968
+ }
3969
+ return compat;
3970
+ }
3971
+ function planNote(source, unmatched, optimize, framework) {
3972
+ 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.";
3973
+ const grounding = " Each package is recommended from lurq\u2019s scored index \u2014 a labeled, evidence-backed starting point, not a validated architecture.";
3974
+ const ctx = framework ? ` Anchored to the ${framework} ecosystem so sibling libraries stay coherent across the stack.` : "";
3975
+ const tail = unmatched ? ` ${unmatched} need(s) had no tracked match (listed in \`unmatched\`).` : "";
3976
+ const opt = optimize === "speed" ? " Ranking favored the lightest-bundle option per slot." : "";
3977
+ return base + grounding + ctx + opt + tail;
3978
+ }
3979
+ function dedupeNeeds(needs) {
3980
+ const seen = /* @__PURE__ */ new Map();
3981
+ for (const n of needs) {
3982
+ const key = n.need.trim().toLowerCase();
3983
+ if (!key) continue;
3984
+ if (!seen.has(key)) seen.set(key, { need: n.need.trim(), category: n.category });
3985
+ }
3986
+ return [...seen.values()];
3987
+ }
3988
+ function familyOf(name) {
3989
+ const n = name.toLowerCase();
3990
+ for (const f of FAMILY_TOKENS) if (f.re.test(n)) return f.family;
3991
+ return null;
3992
+ }
3993
+ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
3994
+ const coherence = (c) => {
3995
+ const fam = familyOf(c.name);
3996
+ if (!fam) return 0;
3997
+ return fam === anchorFamily ? 1 : -1;
3998
+ };
3999
+ return [...cands].sort((a, b) => {
4000
+ if (anchorFamily) {
4001
+ const d = coherence(b) - coherence(a);
4002
+ if (d) return d;
4003
+ }
4004
+ if (optimize === "speed") {
4005
+ return (bundleByName.get(a.name) ?? Infinity) - (bundleByName.get(b.name) ?? Infinity);
4006
+ }
4007
+ return 0;
4008
+ });
4009
+ }
4010
+ async function bundleSizes(db, candidates) {
4011
+ const names = [...new Set(candidates.map((c) => c.name))];
4012
+ if (names.length === 0) return /* @__PURE__ */ new Map();
4013
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray3(packages.name, names));
4014
+ return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
4015
+ }
4016
+ async function decompose(document) {
4017
+ const config = getConfig();
4018
+ if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
4019
+ const llm = await decomposeWithLlm(
4020
+ document,
4021
+ config.SUMMARY_API_KEY,
4022
+ config.SUMMARY_MODEL,
4023
+ config.SUMMARY_BASE_URL
4024
+ ).catch((err) => {
4025
+ logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
4026
+ return null;
4027
+ });
4028
+ if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
4029
+ }
4030
+ return { needs: decomposeHeuristic(document), source: "heuristic" };
4031
+ }
4032
+ async function decomposeWithLlm(document, apiKey, model, baseUrl) {
4033
+ const prompt = [
4034
+ "Project description:",
4035
+ document.slice(0, 8e3),
4036
+ "",
4037
+ 'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
4038
+ "One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
4039
+ ].join("\n");
4040
+ const { data } = await httpRequest(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
4041
+ host: new URL(baseUrl).host,
4042
+ method: "POST",
4043
+ ttlMs: 24 * 60 * 60 * 1e3,
4044
+ // Hash the FULL document — length + a 64-char prefix collide for same-length
4045
+ // edits or shared templated headers, which would serve a stale decomposition.
4046
+ cacheKey: `openai-plan ${model} ${createHash4("sha1").update(document).digest("hex")}`,
4047
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
4048
+ body: JSON.stringify({
4049
+ model,
4050
+ messages: [
4051
+ { role: "system", content: DECOMPOSE_SYSTEM },
4052
+ { role: "user", content: prompt }
4053
+ ],
4054
+ response_format: { type: "json_object" },
4055
+ temperature: 0.2
4056
+ })
4057
+ });
4058
+ const content = data?.choices?.[0]?.message?.content;
4059
+ const parsed = content ? JSON.parse(content) : {};
4060
+ const raw = Array.isArray(parsed?.needs) ? parsed.needs : [];
4061
+ return raw.map((n) => {
4062
+ const need = typeof n?.need === "string" ? n.need.trim() : "";
4063
+ if (!need) return null;
4064
+ const cat = typeof n?.category === "string" && isCategory(n.category) ? n.category : void 0;
4065
+ return { need, category: cat };
4066
+ }).filter(Boolean);
4067
+ }
4068
+ function decomposeHeuristic(document) {
4069
+ const byCategory = /* @__PURE__ */ new Map();
4070
+ for (const rawLine of document.split("\n")) {
4071
+ const line = rawLine.replace(/^[#>*\-\s]+/, "").replace(/^\d+\.\s+/, "").trim();
4072
+ if (line.length < 3) continue;
4073
+ const category = inferCategory(line);
4074
+ if (category && !byCategory.has(category)) {
4075
+ byCategory.set(category, line.slice(0, 120));
4076
+ }
4077
+ }
4078
+ return [...byCategory.entries()].map(([category, need]) => ({ need, category }));
4079
+ }
4080
+ var PER_SLOT, MAX_SLOTS, NO_META, FAMILY_TOKENS, DECOMPOSE_SYSTEM;
4081
+ var init_plan = __esm({
4082
+ "src/mcp/plan.ts"() {
4083
+ "use strict";
4084
+ init_esm_shims();
4085
+ init_config();
4086
+ init_http();
4087
+ init_check();
4088
+ init_members();
4089
+ init_optimize();
4090
+ init_compat();
4091
+ init_logger();
4092
+ init_types();
4093
+ init_schema();
4094
+ init_categoryInference();
4095
+ init_recommend();
4096
+ init_single();
4097
+ init_diagram();
4098
+ init_handlers();
4099
+ PER_SLOT = 3;
4100
+ MAX_SLOTS = 24;
4101
+ NO_META = (c) => ({
4102
+ name: c.name,
4103
+ version: c.latestVersion,
4104
+ peerDependencies: null,
4105
+ peerDependenciesMeta: null,
4106
+ engines: null
4107
+ });
4108
+ FAMILY_TOKENS = [
4109
+ { family: "react", re: /\breact\b|preact/ },
4110
+ { family: "vue", re: /\bvue\b|nuxt/ },
4111
+ { family: "angular", re: /angular/ },
4112
+ { family: "svelte", re: /svelte/ },
4113
+ { family: "solid", re: /\bsolid(-?js)?\b/ }
4114
+ ];
4115
+ 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.";
4116
+ }
4117
+ });
4118
+
4119
+ // src/mcp/metrics.ts
4120
+ function recordToolCall(tool, ok, ms) {
4121
+ const s = stats.get(tool) ?? { calls: 0, errors: 0, totalMs: 0 };
4122
+ s.calls += 1;
4123
+ if (!ok) s.errors += 1;
4124
+ s.totalMs += ms;
4125
+ stats.set(tool, s);
4126
+ }
4127
+ async function timed(tool, fn) {
4128
+ const start = Date.now();
4129
+ let ok = false;
4130
+ try {
4131
+ const result = await fn();
4132
+ ok = true;
4133
+ return result;
4134
+ } finally {
4135
+ recordToolCall(tool, ok, Date.now() - start);
4136
+ }
4137
+ }
4138
+ function renderPrometheus() {
4139
+ const lines = [
4140
+ "# HELP lurq_tool_calls_total Total MCP tool invocations.",
4141
+ "# TYPE lurq_tool_calls_total counter",
4142
+ "# HELP lurq_tool_errors_total MCP tool invocations that threw.",
4143
+ "# TYPE lurq_tool_errors_total counter",
4144
+ "# HELP lurq_tool_duration_ms_total Cumulative tool handler time in ms.",
4145
+ "# TYPE lurq_tool_duration_ms_total counter"
4146
+ ];
4147
+ for (const [tool, s] of stats) {
4148
+ const label = `{tool="${tool}"}`;
4149
+ lines.push(`lurq_tool_calls_total${label} ${s.calls}`);
4150
+ lines.push(`lurq_tool_errors_total${label} ${s.errors}`);
4151
+ lines.push(`lurq_tool_duration_ms_total${label} ${s.totalMs}`);
4152
+ }
4153
+ return lines.join("\n") + "\n";
4154
+ }
4155
+ var stats;
4156
+ var init_metrics = __esm({
4157
+ "src/mcp/metrics.ts"() {
4158
+ "use strict";
4159
+ init_esm_shims();
4160
+ stats = /* @__PURE__ */ new Map();
4161
+ }
4162
+ });
4163
+
4164
+ // src/mcp/compact.ts
4165
+ function isEmptyObject(value) {
4166
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
4167
+ }
4168
+ function compact(value) {
4169
+ if (Array.isArray(value)) {
4170
+ return value.map((v) => compact(v)).filter((v) => v !== null && v !== void 0);
4171
+ }
4172
+ if (value !== null && typeof value === "object") {
4173
+ const out = {};
4174
+ for (const [key, raw] of Object.entries(value)) {
4175
+ if (raw === null || raw === void 0) continue;
4176
+ const cleaned = compact(raw);
4177
+ if (cleaned === null || cleaned === void 0) continue;
4178
+ if (Array.isArray(cleaned) && cleaned.length === 0) continue;
4179
+ if (isEmptyObject(cleaned)) continue;
4180
+ out[key] = cleaned;
4181
+ }
4182
+ return out;
4183
+ }
4184
+ return value;
4185
+ }
4186
+ var init_compact = __esm({
4187
+ "src/mcp/compact.ts"() {
4188
+ "use strict";
4189
+ init_esm_shims();
4190
+ }
4191
+ });
4192
+
2655
4193
  // src/mcp/server.ts
2656
4194
  var server_exports = {};
2657
4195
  __export(server_exports, {
2658
4196
  buildMcpServer: () => buildMcpServer,
4197
+ npmName: () => npmName,
2659
4198
  startMcpServer: () => startMcpServer
2660
4199
  });
2661
4200
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2662
4201
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2663
4202
  import { z as z2 } from "zod";
2664
4203
  function json(obj) {
2665
- return { content: [{ type: "text", text: JSON.stringify(obj) }] };
4204
+ return { content: [{ type: "text", text: JSON.stringify(compact(obj)) }] };
2666
4205
  }
2667
- function buildMcpServer(db) {
4206
+ function buildMcpServer(db, ctx = {}) {
2668
4207
  const server = new McpServer({ name: SERVER_NAME, version: VERSION });
2669
4208
  server.registerTool(
2670
4209
  "recommend",
@@ -2677,7 +4216,7 @@ function buildMcpServer(db) {
2677
4216
  constraints: constraintsSchema
2678
4217
  }
2679
4218
  },
2680
- async (args) => json(await handleRecommend(db, args))
4219
+ async (args) => json(await timed("recommend", () => handleRecommend(db, args)))
2681
4220
  );
2682
4221
  server.registerTool(
2683
4222
  "evaluate",
@@ -2685,10 +4224,10 @@ function buildMcpServer(db) {
2685
4224
  title: "Evaluate a package",
2686
4225
  description: "Full evidence read for one npm package: scores, signals, advisories, summary, and a usage guide. Fetches & scores on demand if not yet tracked.",
2687
4226
  inputSchema: {
2688
- package: z2.string().min(1).describe("npm package name")
4227
+ package: npmName.describe("npm package name")
2689
4228
  }
2690
4229
  },
2691
- async (args) => json(await handleEvaluate(db, args))
4230
+ async (args) => json(await timed("evaluate", () => handleEvaluate(db, args)))
2692
4231
  );
2693
4232
  server.registerTool(
2694
4233
  "compare",
@@ -2696,10 +4235,21 @@ function buildMcpServer(db) {
2696
4235
  title: "Compare packages",
2697
4236
  description: "Side-by-side comparison of 2\u20135 npm packages, ranked by health score.",
2698
4237
  inputSchema: {
2699
- packages: z2.array(z2.string().min(1)).min(2).max(5).describe("2\u20135 npm package names")
4238
+ packages: z2.array(npmName).min(2).max(5).describe("2\u20135 npm package names")
4239
+ }
4240
+ },
4241
+ async (args) => json(await timed("compare", () => handleCompare(db, args)))
4242
+ );
4243
+ server.registerTool(
4244
+ "compat",
4245
+ {
4246
+ title: "Check package compatibility",
4247
+ 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.",
4248
+ inputSchema: {
4249
+ packages: z2.array(npmName).min(2).max(8).describe("2\u20138 npm package names to check together")
2700
4250
  }
2701
4251
  },
2702
- async (args) => json(await handleCompare(db, args))
4252
+ async (args) => json(await timed("compat", () => handleCompat(db, args)))
2703
4253
  );
2704
4254
  server.registerTool(
2705
4255
  "verify",
@@ -2707,10 +4257,10 @@ function buildMcpServer(db) {
2707
4257
  title: "Verify a package",
2708
4258
  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.",
2709
4259
  inputSchema: {
2710
- package: z2.string().min(1).describe("npm package name to verify")
4260
+ package: npmName.describe("npm package name to verify")
2711
4261
  }
2712
4262
  },
2713
- async (args) => json(await handleVerify(db, args))
4263
+ async (args) => json(await timed("verify", () => handleVerify(db, args)))
2714
4264
  );
2715
4265
  server.registerTool(
2716
4266
  "diagram",
@@ -2718,10 +4268,47 @@ function buildMcpServer(db) {
2718
4268
  title: "Reference architecture diagram",
2719
4269
  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.",
2720
4270
  inputSchema: {
2721
- stack: z2.array(z2.string()).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
4271
+ stack: z2.array(npmName).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
4272
+ }
4273
+ },
4274
+ async (args) => json(await timed("diagram", () => handleDiagram(db, args)))
4275
+ );
4276
+ server.registerTool(
4277
+ "plan",
4278
+ {
4279
+ title: "Plan a stack from a program description",
4280
+ 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.",
4281
+ inputSchema: {
4282
+ document: z2.string().optional().describe("Detailed description of the program (spec/README); lurq decomposes it into components"),
4283
+ needs: z2.array(
4284
+ z2.object({
4285
+ need: z2.string().min(1).describe("A component that needs a library"),
4286
+ category: categoryEnum.optional()
4287
+ })
4288
+ ).optional().describe("Pre-decomposed components (skip if you pass a document)"),
4289
+ using: z2.array(npmName).max(12).optional().describe(
4290
+ "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."
4291
+ ),
4292
+ optimize: z2.enum(["speed", "balanced"]).optional().describe("'speed' prefers the lightest-bundle option per slot; default 'balanced'")
4293
+ }
4294
+ },
4295
+ async (args) => json(await timed("plan", () => handlePlan(db, args)))
4296
+ );
4297
+ server.registerTool(
4298
+ "report_outcome",
4299
+ {
4300
+ title: "Report a recommendation outcome",
4301
+ description: "Opt-in feedback after acting on a lurq recommendation: report whether you went with the package and whether it built. No source code \u2014 only the coarse decision + a build signal. Helps lurq learn which packages agents actually succeed with; safe to skip.",
4302
+ inputSchema: {
4303
+ package: npmName.describe("The package that was recommended"),
4304
+ accepted: z2.boolean().describe("Did you go with this package?"),
4305
+ buildSignal: z2.enum(["installed", "compiled", "tests_passed", "failed"]).optional().describe("Coarse post-install result, if known"),
4306
+ need: z2.string().max(500).optional().describe("The original need this was recommended for (no source code)")
2722
4307
  }
2723
4308
  },
2724
- async (args) => json(await handleDiagram(db, args))
4309
+ // ownerId comes from the authenticated key (ctx), NOT the tool arguments —
4310
+ // a caller must never be able to attribute an outcome to another org.
4311
+ async (args) => json(await timed("report_outcome", () => handleReportOutcome(db, args, ctx.ownerId ?? null)))
2725
4312
  );
2726
4313
  return server;
2727
4314
  }
@@ -2741,7 +4328,7 @@ async function startMcpServer() {
2741
4328
  await server.connect(transport);
2742
4329
  logger.info(`${SERVER_NAME} MCP server v${VERSION} running on stdio.`);
2743
4330
  }
2744
- var categoryEnum, confidenceEnum, constraintsSchema;
4331
+ var categoryEnum, confidenceEnum, npmName, constraintsSchema;
2745
4332
  var init_server = __esm({
2746
4333
  "src/mcp/server.ts"() {
2747
4334
  "use strict";
@@ -2752,8 +4339,12 @@ var init_server = __esm({
2752
4339
  init_logger();
2753
4340
  init_handlers();
2754
4341
  init_diagram();
4342
+ init_plan();
4343
+ init_metrics();
4344
+ init_compact();
2755
4345
  categoryEnum = z2.enum(CATEGORIES);
2756
4346
  confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
4347
+ 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");
2757
4348
  constraintsSchema = z2.object({
2758
4349
  runtime: z2.enum(["browser", "node", "both"]).optional(),
2759
4350
  license: z2.string().optional(),
@@ -2764,10 +4355,10 @@ var init_server = __esm({
2764
4355
  });
2765
4356
 
2766
4357
  // src/auth/apiKeys.ts
2767
- import { createHash as createHash3, randomBytes } from "crypto";
2768
- import { and as and3, desc, eq as eq4, isNull } from "drizzle-orm";
4358
+ import { createHash as createHash5, randomBytes } from "crypto";
4359
+ import { and as and5, desc as desc3, eq as eq5, isNull } from "drizzle-orm";
2769
4360
  function hashKey(key) {
2770
- return createHash3("sha256").update(key).digest("hex");
4361
+ return createHash5("sha256").update(key).digest("hex");
2771
4362
  }
2772
4363
  function generateApiKey() {
2773
4364
  const body = randomBytes(24).toString("base64url");
@@ -2785,48 +4376,106 @@ async function createKey(db, input = {}) {
2785
4376
  }).returning();
2786
4377
  return { key, row };
2787
4378
  }
4379
+ function stampLastUsed(db, entry, now) {
4380
+ if (now - entry.lastStampAt < STAMP_INTERVAL_MS) return;
4381
+ entry.lastStampAt = now;
4382
+ db.update(apiKeys).set({ lastUsedAt: new Date(now) }).where(eq5(apiKeys.id, entry.row.id)).then(void 0, (err) => logger.debug(`lastUsedAt stamp failed: ${String(err)}`));
4383
+ }
2788
4384
  async function lookupActiveKey(db, key) {
2789
4385
  const hash = hashKey(key);
2790
- const [row] = await db.select().from(apiKeys).where(and3(eq4(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
2791
- if (!row) return null;
2792
- db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq4(apiKeys.id, row.id)).then(void 0, () => {
2793
- });
4386
+ const now = Date.now();
4387
+ const cached3 = authCache.get(hash);
4388
+ if (cached3 && now - cached3.cachedAt < AUTH_TTL_MS) {
4389
+ stampLastUsed(db, cached3, now);
4390
+ return cached3.row;
4391
+ }
4392
+ const [row] = await db.select().from(apiKeys).where(and5(eq5(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
4393
+ if (!row) {
4394
+ authCache.delete(hash);
4395
+ return null;
4396
+ }
4397
+ const entry = { row, cachedAt: now, lastStampAt: 0 };
4398
+ authCache.set(hash, entry);
4399
+ stampLastUsed(db, entry, now);
2794
4400
  return row;
2795
4401
  }
2796
4402
  async function listKeys(db) {
2797
- return db.select().from(apiKeys).orderBy(desc(apiKeys.createdAt));
4403
+ return db.select().from(apiKeys).orderBy(desc3(apiKeys.createdAt));
2798
4404
  }
2799
- async function revokeKey(db, prefixOrId) {
4405
+ function matchByPrefixOrId(prefixOrId) {
2800
4406
  const asId = Number(prefixOrId);
2801
- const match = Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq4(apiKeys.id, asId) : eq4(apiKeys.prefix, prefixOrId);
2802
- const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and3(match, isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
4407
+ return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq5(apiKeys.id, asId) : eq5(apiKeys.prefix, prefixOrId);
4408
+ }
4409
+ async function revokeKey(db, prefixOrId) {
4410
+ const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
2803
4411
  return rows.length;
2804
4412
  }
2805
- var DISPLAY_BODY;
4413
+ async function rotateKey(db, prefixOrId) {
4414
+ const [previous] = await db.select().from(apiKeys).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).limit(1);
4415
+ if (!previous) return null;
4416
+ const { key, row } = await createKey(db, {
4417
+ label: previous.label ?? void 0,
4418
+ tier: previous.tier,
4419
+ ownerId: previous.ownerId ?? void 0
4420
+ });
4421
+ await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, previous.id));
4422
+ return { key, row, previous };
4423
+ }
4424
+ var DISPLAY_BODY, authCache, AUTH_TTL_MS, STAMP_INTERVAL_MS;
2806
4425
  var init_apiKeys = __esm({
2807
4426
  "src/auth/apiKeys.ts"() {
2808
4427
  "use strict";
2809
4428
  init_esm_shims();
2810
4429
  init_constants();
2811
4430
  init_schema();
4431
+ init_logger();
2812
4432
  DISPLAY_BODY = 6;
4433
+ authCache = /* @__PURE__ */ new Map();
4434
+ AUTH_TTL_MS = 6e4;
4435
+ STAMP_INTERVAL_MS = 6e4;
2813
4436
  }
2814
4437
  });
2815
4438
 
2816
4439
  // src/mcp/http.ts
2817
4440
  var http_exports = {};
2818
4441
  __export(http_exports, {
4442
+ secretEquals: () => secretEquals,
2819
4443
  startHttpServer: () => startHttpServer
2820
4444
  });
4445
+ import { createHash as createHash6, timingSafeEqual } from "crypto";
2821
4446
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2822
4447
  function rpcError(code, message) {
2823
4448
  return { jsonrpc: "2.0", error: { code, message }, id: null };
2824
4449
  }
4450
+ function secretEquals(a, b) {
4451
+ return timingSafeEqual(
4452
+ createHash6("sha256").update(a).digest(),
4453
+ createHash6("sha256").update(b).digest()
4454
+ );
4455
+ }
2825
4456
  async function startHttpServer(opts = {}) {
2826
4457
  const config = getConfig();
2827
4458
  const port = opts.port ?? config.PORT;
2828
4459
  const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
2829
4460
  const { db } = createDb({ max: 20 });
4461
+ if (!process.env.REDIS_URL) {
4462
+ logger.warn(
4463
+ "REDIS_URL not set \u2014 response caching is OFF; every request recomputes on the database. Set REDIS_URL before serving real traffic (and it also backs the rate limiter across instances)."
4464
+ );
4465
+ }
4466
+ let makeStore = null;
4467
+ if (process.env.REDIS_URL) {
4468
+ const [{ default: Redis }, { default: RedisStore }] = await Promise.all([
4469
+ import("ioredis"),
4470
+ import("rate-limit-redis")
4471
+ ]);
4472
+ const rlRedis = new Redis(process.env.REDIS_URL, { maxRetriesPerRequest: 1, family: 0 });
4473
+ rlRedis.on("error", (err) => logger.warn(`rate-limit redis: ${err.message}`));
4474
+ makeStore = (prefix) => new RedisStore({
4475
+ prefix,
4476
+ sendCommand: (...args) => rlRedis.call(args[0], ...args.slice(1))
4477
+ });
4478
+ }
2830
4479
  const app = express();
2831
4480
  app.set("trust proxy", 1);
2832
4481
  app.use(helmet());
@@ -2834,11 +4483,26 @@ async function startHttpServer(opts = {}) {
2834
4483
  app.get("/healthz", (_req, res) => {
2835
4484
  res.status(200).json({ status: "ok" });
2836
4485
  });
4486
+ app.get("/metrics", (req, res) => {
4487
+ const token = config.LURQ_METRICS_TOKEN;
4488
+ if (!token) {
4489
+ res.status(404).end();
4490
+ return;
4491
+ }
4492
+ const header = req.headers.authorization;
4493
+ const presented = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
4494
+ if (presented !== token) {
4495
+ res.status(401).end();
4496
+ return;
4497
+ }
4498
+ res.type("text/plain").send(renderPrometheus());
4499
+ });
2837
4500
  const ipLimiter = rateLimit({
2838
4501
  windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
2839
4502
  limit: config.LURQ_IP_RATE_LIMIT_MAX,
2840
4503
  standardHeaders: "draft-7",
2841
4504
  legacyHeaders: false,
4505
+ ...makeStore ? { store: makeStore("rl:ip:") } : {},
2842
4506
  message: rpcError(-32029, "Rate limit exceeded.")
2843
4507
  });
2844
4508
  const auth = async (req, res, next) => {
@@ -2866,14 +4530,47 @@ async function startHttpServer(opts = {}) {
2866
4530
  limit: config.LURQ_RATE_LIMIT_MAX,
2867
4531
  standardHeaders: "draft-7",
2868
4532
  legacyHeaders: false,
2869
- // Key on the resolved API key (always present — auth runs first). The IP
2870
- // fallback uses express-rate-limit's ipKeyGenerator so IPv6 addresses are
2871
- // normalized correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
2872
- keyGenerator: (req) => req.lurqKey?.prefix ?? ipKeyGenerator(req.ip ?? "0.0.0.0"),
4533
+ // Key on the resolved API key's unique row id (always present — auth runs
4534
+ // first). The display `prefix` is only 6 chars of body, so distinct keys
4535
+ // can collide on it and share a quota; the id cannot. The IP fallback uses
4536
+ // express-rate-limit's ipKeyGenerator so IPv6 addresses are normalized
4537
+ // correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
4538
+ keyGenerator: (req) => {
4539
+ const id = req.lurqKey?.id;
4540
+ return id != null ? `key:${id}` : ipKeyGenerator(req.ip ?? "0.0.0.0");
4541
+ },
4542
+ ...makeStore ? { store: makeStore("rl:key:") } : {},
2873
4543
  message: rpcError(-32029, "Rate limit exceeded.")
2874
4544
  });
4545
+ app.post("/keys", async (req, res) => {
4546
+ const secret = config.LURQ_ISSUER_SECRET;
4547
+ if (!secret) {
4548
+ res.status(404).end();
4549
+ return;
4550
+ }
4551
+ const header = req.headers.authorization;
4552
+ const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
4553
+ if (!token || !secretEquals(token, secret)) {
4554
+ res.status(401).json({ error: "Invalid issuer secret." });
4555
+ return;
4556
+ }
4557
+ const body = req.body ?? {};
4558
+ const ownerId = typeof body.ownerId === "string" ? body.ownerId.trim() : "";
4559
+ if (!ownerId) {
4560
+ res.status(400).json({ error: "ownerId is required." });
4561
+ return;
4562
+ }
4563
+ const label = typeof body.label === "string" ? body.label.slice(0, 200) : void 0;
4564
+ try {
4565
+ const { key, row } = await createKey(db, { ownerId, label, tier: "free" });
4566
+ res.status(201).json({ key, prefix: row.prefix });
4567
+ } catch (err) {
4568
+ logger.error("key issuance failed:", err instanceof Error ? err.message : String(err));
4569
+ res.status(500).json({ error: "Could not issue key." });
4570
+ }
4571
+ });
2875
4572
  app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
2876
- const server = buildMcpServer(db);
4573
+ const server = buildMcpServer(db, { ownerId: req.lurqKey?.ownerId ?? null });
2877
4574
  const transport = new StreamableHTTPServerTransport({
2878
4575
  sessionIdGenerator: void 0,
2879
4576
  enableJsonResponse: true
@@ -2906,27 +4603,29 @@ var init_http2 = __esm({
2906
4603
  init_apiKeys();
2907
4604
  init_client();
2908
4605
  init_server();
4606
+ init_metrics();
2909
4607
  }
2910
4608
  });
2911
4609
 
2912
4610
  // src/pipeline/rescore.ts
2913
- import { isNotNull as isNotNull3 } from "drizzle-orm";
2914
- import { eq as eq5 } from "drizzle-orm";
4611
+ import { isNotNull as isNotNull4 } from "drizzle-orm";
4612
+ import { eq as eq6 } from "drizzle-orm";
2915
4613
  async function runRescore() {
2916
4614
  const weights = loadWeights();
2917
4615
  const handle = createDb({ max: 4 });
2918
4616
  try {
2919
- const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull3(packages.scoreBreakdown));
4617
+ const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull4(packages.scoreBreakdown));
2920
4618
  let updated = 0;
2921
4619
  for (const row of rows) {
2922
4620
  if (!row.breakdown) continue;
2923
4621
  const health = computeHealthScore(row.breakdown, weights.health);
2924
4622
  if (health !== row.healthScore) {
2925
- await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq5(packages.id, row.id));
4623
+ await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq6(packages.id, row.id));
2926
4624
  updated++;
2927
4625
  }
2928
4626
  }
2929
4627
  logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
4628
+ if (updated > 0) await invalidateCache();
2930
4629
  return { seen: rows.length, updated };
2931
4630
  } finally {
2932
4631
  await handle.close();
@@ -2936,6 +4635,7 @@ var init_rescore = __esm({
2936
4635
  "src/pipeline/rescore.ts"() {
2937
4636
  "use strict";
2938
4637
  init_esm_shims();
4638
+ init_cache();
2939
4639
  init_logger();
2940
4640
  init_client();
2941
4641
  init_schema();
@@ -2945,7 +4645,7 @@ var init_rescore = __esm({
2945
4645
  });
2946
4646
 
2947
4647
  // src/db/discovery.ts
2948
- import { eq as eq6, inArray as inArray2, sql as sql6 } from "drizzle-orm";
4648
+ import { eq as eq7 } from "drizzle-orm";
2949
4649
  async function getKnownNames(db) {
2950
4650
  const [tracked, queued] = await Promise.all([
2951
4651
  db.select({ name: packages.name }).from(packages),
@@ -2960,10 +4660,10 @@ async function enqueueCandidates(db, candidates) {
2960
4660
  return inserted.length;
2961
4661
  }
2962
4662
  async function getPendingCandidates(db, limit) {
2963
- return db.select().from(discoveryQueue).where(eq6(discoveryQueue.status, "pending")).limit(limit);
4663
+ return db.select().from(discoveryQueue).where(eq7(discoveryQueue.status, "pending")).limit(limit);
2964
4664
  }
2965
4665
  async function setDiscoveryStatus(db, name, data) {
2966
- await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq6(discoveryQueue.name, name));
4666
+ await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq7(discoveryQueue.name, name));
2967
4667
  }
2968
4668
  var init_discovery = __esm({
2969
4669
  "src/db/discovery.ts"() {
@@ -2974,7 +4674,7 @@ var init_discovery = __esm({
2974
4674
  });
2975
4675
 
2976
4676
  // src/pipeline/discovery.ts
2977
- import { isNotNull as isNotNull4 } from "drizzle-orm";
4677
+ import { isNotNull as isNotNull5 } from "drizzle-orm";
2978
4678
  function selectCandidates(raw, known) {
2979
4679
  const seen = new Set(known);
2980
4680
  const out = [];
@@ -3007,7 +4707,7 @@ async function preScorePackage(name, fetchImpl) {
3007
4707
  }
3008
4708
  }
3009
4709
  async function graphChannel(db) {
3010
- const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull4(packages.latestVersion));
4710
+ const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull5(packages.latestVersion));
3011
4711
  const out = [];
3012
4712
  for (const t of tracked) {
3013
4713
  if (!t.version) continue;
@@ -3049,9 +4749,13 @@ async function runDiscovery(opts = {}) {
3049
4749
  logger.info(
3050
4750
  `Discovery: ${graph.length} graph + ${search.length} search candidates \u2192 ${enqueued} new queued.`
3051
4751
  );
3052
- const pending = await getPendingCandidates(handle.db, cap * 4);
4752
+ const pending2 = await getPendingCandidates(handle.db, cap * 4);
3053
4753
  const scored = [];
3054
- for (const cand of pending) {
4754
+ for (const cand of pending2) {
4755
+ if (cand.preScore !== null) {
4756
+ scored.push({ name: cand.name, preScore: cand.preScore });
4757
+ continue;
4758
+ }
3055
4759
  const preScore = await preScorePackage(cand.name);
3056
4760
  await setDiscoveryStatus(handle.db, cand.name, {
3057
4761
  status: passesGate(preScore) ? "pending" : "rejected",
@@ -3059,7 +4763,7 @@ async function runDiscovery(opts = {}) {
3059
4763
  });
3060
4764
  if (passesGate(preScore)) scored.push({ name: cand.name, preScore });
3061
4765
  }
3062
- logger.info(`Discovery: gated ${pending.length}; ${scored.length} cleared the quality bar.`);
4766
+ logger.info(`Discovery: gated ${pending2.length}; ${scored.length} cleared the quality bar.`);
3063
4767
  scored.sort((a, b) => b.preScore - a.preScore);
3064
4768
  const toIngest = scored.slice(0, cap);
3065
4769
  const deferred = scored.slice(cap);
@@ -3083,7 +4787,7 @@ async function runDiscovery(opts = {}) {
3083
4787
  logger.info(`Discovery: ingested ${ingested}/${toIngest.length} candidate(s).`);
3084
4788
  return {
3085
4789
  enqueued,
3086
- gated: pending.length,
4790
+ gated: pending2.length,
3087
4791
  passed: scored.length,
3088
4792
  ingested,
3089
4793
  droppedToNextRun: deferred.length
@@ -3177,14 +4881,580 @@ var init_format = __esm({
3177
4881
  }
3178
4882
  });
3179
4883
 
4884
+ // src/cli/planView.ts
4885
+ var planView_exports = {};
4886
+ __export(planView_exports, {
4887
+ renderPlanHtml: () => renderPlanHtml
4888
+ });
4889
+ function esc(s) {
4890
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4891
+ }
4892
+ function slotRows(plan) {
4893
+ return plan.slots.map((s) => {
4894
+ const rec = s.recommended;
4895
+ 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>';
4896
+ const alts = s.alternatives.map((a) => esc(a.name)).join(", ") || '<span class="muted">\u2014</span>';
4897
+ return `<tr>
4898
+ <td>${esc(s.need)}</td>
4899
+ <td><span class="layer">${esc(s.layer)}</span></td>
4900
+ <td class="pkg">${name}</td>
4901
+ <td class="num">${rec ? rec.healthScore : "\u2014"}</td>
4902
+ <td>${rec ? esc(rec.confidence) : "\u2014"}</td>
4903
+ <td class="alts">${alts}</td>
4904
+ </tr>`;
4905
+ }).join("\n");
4906
+ }
4907
+ function renderPlanHtml(plan) {
4908
+ return `<!doctype html>
4909
+ <html lang="en">
4910
+ <head>
4911
+ <meta charset="utf-8" />
4912
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4913
+ <title>lurq plan \u2014 roadmap</title>
4914
+ <style>
4915
+ :root { color-scheme: dark; }
4916
+ * { box-sizing: border-box; }
4917
+ body { margin: 0; font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
4918
+ background: #0b0b0f; color: #e7e7ea; padding: 2.5rem clamp(1rem, 5vw, 4rem); }
4919
+ h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
4920
+ h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: .08em; color: #9a9aa6; margin: 2.5rem 0 .75rem; }
4921
+ .sub { color: #9a9aa6; margin: 0 0 1rem; max-width: 70ch; }
4922
+ .diagram { background: #141419; border: 1px solid #26262e; border-radius: 14px; padding: 1.5rem; overflow: auto; }
4923
+ table { width: 100%; border-collapse: collapse; margin-top: .5rem; }
4924
+ th, td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid #1e1e25; vertical-align: top; }
4925
+ th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #9a9aa6; }
4926
+ .pkg a { color: #d98cff; text-decoration: none; } .pkg a:hover { text-decoration: underline; }
4927
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
4928
+ .muted { color: #6a6a76; } .alts { color: #b8b8c2; font-size: .92rem; }
4929
+ .layer { font-size: .8rem; color: #8ad; background: #15202b; border-radius: 6px; padding: .1rem .45rem; }
4930
+ footer { margin-top: 2.5rem; color: #6a6a76; font-size: .85rem; }
4931
+ </style>
4932
+ </head>
4933
+ <body>
4934
+ <h1>lurq plan <span class="muted">\xB7 ${esc(plan.optimize)} \xB7 ${esc(plan.source)}${plan.framework ? ` \xB7 ${esc(plan.framework)} ecosystem` : ""}</span></h1>
4935
+ <p class="sub">${esc(plan.note)}</p>
4936
+
4937
+ <h2>Roadmap</h2>
4938
+ <div class="diagram"><pre class="mermaid">${esc(plan.mermaid)}</pre></div>
4939
+
4940
+ <h2>Components</h2>
4941
+ <table>
4942
+ <thead><tr><th>Component</th><th>Layer</th><th>Recommended</th><th>Health</th><th>Confidence</th><th>Alternatives</th></tr></thead>
4943
+ <tbody>
4944
+ ${slotRows(plan)}
4945
+ </tbody>
4946
+ </table>
4947
+
4948
+ <footer>data as of ${esc(plan.dataAsOf)} \xB7 generated by lurq</footer>
4949
+
4950
+ <script type="module">
4951
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
4952
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
4953
+ </script>
4954
+ </body>
4955
+ </html>
4956
+ `;
4957
+ }
4958
+ var init_planView = __esm({
4959
+ "src/cli/planView.ts"() {
4960
+ "use strict";
4961
+ init_esm_shims();
4962
+ }
4963
+ });
4964
+
4965
+ // src/db/watch.ts
4966
+ import { eq as eq8 } from "drizzle-orm";
4967
+ async function getWatchCursor(db, id) {
4968
+ const rows = await db.select({ seq: watchState.seq }).from(watchState).where(eq8(watchState.id, id)).limit(1);
4969
+ return rows[0]?.seq ?? null;
4970
+ }
4971
+ async function setWatchCursor(db, id, seq) {
4972
+ await db.insert(watchState).values({ id, seq, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({ target: watchState.id, set: { seq, updatedAt: /* @__PURE__ */ new Date() } });
4973
+ }
4974
+ var init_watch = __esm({
4975
+ "src/db/watch.ts"() {
4976
+ "use strict";
4977
+ init_esm_shims();
4978
+ init_schema();
4979
+ }
4980
+ });
4981
+
4982
+ // src/pipeline/watch.ts
4983
+ var watch_exports = {};
4984
+ __export(watch_exports, {
4985
+ parseChangeLine: () => parseChangeLine,
4986
+ watchNpmChanges: () => watchNpmChanges
4987
+ });
4988
+ function parseChangeLine(line) {
4989
+ const trimmed = line.trim();
4990
+ if (!trimmed) return null;
4991
+ try {
4992
+ const obj = JSON.parse(trimmed);
4993
+ if (typeof obj?.id !== "string" || obj.seq == null) return null;
4994
+ return { seq: obj.seq, id: obj.id, deleted: Boolean(obj.deleted) };
4995
+ } catch {
4996
+ return null;
4997
+ }
4998
+ }
4999
+ async function watchNpmChanges(db, opts = {}) {
5000
+ const { signal } = opts;
5001
+ let backoff2 = 1e3;
5002
+ while (!signal?.aborted) {
5003
+ let tracked = new Set(await getAllPackageNames(db));
5004
+ let trackedAt = Date.now();
5005
+ const since = await getWatchCursor(db, FEED_ID) ?? opts.since ?? "now";
5006
+ const url = `${FEED_URL}?feed=continuous&since=${encodeURIComponent(since)}&heartbeat=${HEARTBEAT_MS}`;
5007
+ logger.info(`watch: connecting from seq=${since} (${tracked.size} tracked packages)`);
5008
+ try {
5009
+ const res = await fetch(url, { signal });
5010
+ if (!res.ok || !res.body) throw new Error(`feed responded ${res.status}`);
5011
+ backoff2 = 1e3;
5012
+ let sinceCheckpoint = 0;
5013
+ for await (const line of ndjsonLines(res.body, signal)) {
5014
+ const change = parseChangeLine(line);
5015
+ if (!change) continue;
5016
+ const seq = String(change.seq);
5017
+ sinceCheckpoint++;
5018
+ if (Date.now() - trackedAt > TRACKED_REFRESH_MS) {
5019
+ tracked = new Set(await getAllPackageNames(db));
5020
+ trackedAt = Date.now();
5021
+ }
5022
+ if (!change.deleted && tracked.has(change.id)) {
5023
+ logger.info(`watch: re-syncing ${change.id} (seq=${seq})`);
5024
+ await syncOnePackage(db, change.id).catch(
5025
+ (err) => logger.warn(`watch: re-sync failed for ${change.id}: ${String(err)}`)
5026
+ );
5027
+ await setWatchCursor(db, FEED_ID, seq);
5028
+ sinceCheckpoint = 0;
5029
+ } else if (sinceCheckpoint >= CHECKPOINT_EVERY) {
5030
+ await setWatchCursor(db, FEED_ID, seq);
5031
+ sinceCheckpoint = 0;
5032
+ }
5033
+ }
5034
+ logger.info("watch: feed stream ended; reconnecting");
5035
+ } catch (err) {
5036
+ if (signal?.aborted) break;
5037
+ logger.warn(`watch: ${String(err)} \u2014 retrying in ${backoff2}ms`);
5038
+ await sleep(backoff2, signal);
5039
+ backoff2 = Math.min(backoff2 * 2, MAX_BACKOFF_MS);
5040
+ }
5041
+ }
5042
+ }
5043
+ async function* ndjsonLines(body, signal) {
5044
+ const reader = body.getReader();
5045
+ const decoder = new TextDecoder();
5046
+ let buffer = "";
5047
+ try {
5048
+ while (!signal?.aborted) {
5049
+ const { value, done } = await reader.read();
5050
+ if (done) break;
5051
+ buffer += decoder.decode(value, { stream: true });
5052
+ let nl;
5053
+ while ((nl = buffer.indexOf("\n")) >= 0) {
5054
+ yield buffer.slice(0, nl);
5055
+ buffer = buffer.slice(nl + 1);
5056
+ }
5057
+ }
5058
+ } finally {
5059
+ reader.releaseLock();
5060
+ }
5061
+ }
5062
+ function sleep(ms, signal) {
5063
+ return new Promise((resolve) => {
5064
+ const timer = setTimeout(resolve, ms);
5065
+ signal?.addEventListener(
5066
+ "abort",
5067
+ () => {
5068
+ clearTimeout(timer);
5069
+ resolve();
5070
+ },
5071
+ { once: true }
5072
+ );
5073
+ });
5074
+ }
5075
+ var FEED_ID, FEED_URL, HEARTBEAT_MS, TRACKED_REFRESH_MS, CHECKPOINT_EVERY, MAX_BACKOFF_MS;
5076
+ var init_watch2 = __esm({
5077
+ "src/pipeline/watch.ts"() {
5078
+ "use strict";
5079
+ init_esm_shims();
5080
+ init_logger();
5081
+ init_packages();
5082
+ init_watch();
5083
+ init_single();
5084
+ FEED_ID = "npm-changes";
5085
+ FEED_URL = "https://replicate.npmjs.com/_changes";
5086
+ HEARTBEAT_MS = 3e4;
5087
+ TRACKED_REFRESH_MS = 5 * 6e4;
5088
+ CHECKPOINT_EVERY = 200;
5089
+ MAX_BACKOFF_MS = 3e4;
5090
+ }
5091
+ });
5092
+
5093
+ // src/sandbox/types.ts
5094
+ var DEFAULT_TARGET;
5095
+ var init_types2 = __esm({
5096
+ "src/sandbox/types.ts"() {
5097
+ "use strict";
5098
+ init_esm_shims();
5099
+ DEFAULT_TARGET = {
5100
+ node: "20",
5101
+ moduleSystem: "cjs"
5102
+ };
5103
+ }
5104
+ });
5105
+
5106
+ // src/sandbox/local.ts
5107
+ import { execFile } from "child_process";
5108
+ import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
5109
+ import { tmpdir } from "os";
5110
+ import { join as join3 } from "path";
5111
+ import { promisify } from "util";
5112
+ function npmInstallArgs(specs, opts) {
5113
+ const bad = specs.find((s) => s.startsWith("-"));
5114
+ if (bad) throw new Error(`Invalid package spec: ${bad}`);
5115
+ const args = ["install", ...specs, "--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
5116
+ if (!opts.allowScripts) args.push("--ignore-scripts");
5117
+ return args;
5118
+ }
5119
+ function smokeScript(pkg, moduleSystem) {
5120
+ return moduleSystem === "esm" ? ["--input-type=module", "-e", `await import(${JSON.stringify(pkg)})`] : ["-e", `require(${JSON.stringify(pkg)})`];
5121
+ }
5122
+ function condense(s) {
5123
+ return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX);
5124
+ }
5125
+ function stderrOf(err) {
5126
+ if (err && typeof err === "object") {
5127
+ const e = err;
5128
+ if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
5129
+ if (typeof e.message === "string") return e.message;
5130
+ }
5131
+ return String(err);
5132
+ }
5133
+ var execFileAsync, INSTALL_TIMEOUT_MS, SMOKE_TIMEOUT_MS, ERROR_MAX, toSpec, LocalSandbox;
5134
+ var init_local = __esm({
5135
+ "src/sandbox/local.ts"() {
5136
+ "use strict";
5137
+ init_esm_shims();
5138
+ init_types2();
5139
+ execFileAsync = promisify(execFile);
5140
+ INSTALL_TIMEOUT_MS = 12e4;
5141
+ SMOKE_TIMEOUT_MS = 3e4;
5142
+ ERROR_MAX = 500;
5143
+ toSpec = (p) => p.version ? `${p.name}@${p.version}` : p.name;
5144
+ LocalSandbox = class {
5145
+ name = "local";
5146
+ async verify(pkg, version, opts = {}) {
5147
+ const set = await this.verifySet([{ name: pkg, version }], opts);
5148
+ return {
5149
+ driver: this.name,
5150
+ moduleSystem: set.moduleSystem,
5151
+ installed: set.installed,
5152
+ imported: set.loaded[0]?.loaded ?? null,
5153
+ ranScripts: opts.allowScripts ?? false,
5154
+ durationMs: set.durationMs,
5155
+ error: set.error
5156
+ };
5157
+ }
5158
+ async verifySet(packages2, opts = {}) {
5159
+ const target = opts.target ?? DEFAULT_TARGET;
5160
+ const allowScripts = opts.allowScripts ?? false;
5161
+ const specs = packages2.map(toSpec);
5162
+ const dir = await mkdtemp(join3(tmpdir(), "lurq-sandbox-"));
5163
+ const started = Date.now();
5164
+ const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
5165
+ let installed = false;
5166
+ let error = null;
5167
+ try {
5168
+ await writeFile2(
5169
+ join3(dir, "package.json"),
5170
+ JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
5171
+ );
5172
+ await execFileAsync("npm", npmInstallArgs(specs, { allowScripts }), {
5173
+ cwd: dir,
5174
+ timeout: opts.timeoutMs ?? INSTALL_TIMEOUT_MS,
5175
+ signal: opts.signal
5176
+ });
5177
+ installed = true;
5178
+ for (let i = 0; i < packages2.length; i++) {
5179
+ try {
5180
+ await execFileAsync("node", smokeScript(packages2[i].name, target.moduleSystem), {
5181
+ cwd: dir,
5182
+ timeout: SMOKE_TIMEOUT_MS,
5183
+ signal: opts.signal
5184
+ });
5185
+ loaded[i].loaded = true;
5186
+ } catch (err) {
5187
+ loaded[i].loaded = false;
5188
+ if (!error) error = condense(stderrOf(err));
5189
+ }
5190
+ }
5191
+ } catch (err) {
5192
+ error = condense(stderrOf(err));
5193
+ } finally {
5194
+ await rm(dir, { recursive: true, force: true }).catch(() => {
5195
+ });
5196
+ }
5197
+ return {
5198
+ driver: this.name,
5199
+ moduleSystem: target.moduleSystem,
5200
+ installed,
5201
+ loaded,
5202
+ durationMs: Date.now() - started,
5203
+ error
5204
+ };
5205
+ }
5206
+ };
5207
+ }
5208
+ });
5209
+
5210
+ // src/sandbox/e2b.ts
5211
+ var e2b_exports = {};
5212
+ __export(e2b_exports, {
5213
+ E2BSandbox: () => E2BSandbox,
5214
+ installCommand: () => installCommand,
5215
+ shQuote: () => shQuote,
5216
+ smokeCommand: () => smokeCommand
5217
+ });
5218
+ import Sandbox from "e2b";
5219
+ function shQuote(s) {
5220
+ return `'${s.replace(/'/g, `'\\''`)}'`;
5221
+ }
5222
+ function installCommand(specs, allowScripts) {
5223
+ const bad = specs.find((s) => s.startsWith("-"));
5224
+ if (bad) throw new Error(`Invalid package spec: ${bad}`);
5225
+ const flags = ["--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
5226
+ if (!allowScripts) flags.push("--ignore-scripts");
5227
+ return `npm install ${specs.map(shQuote).join(" ")} ${flags.join(" ")}`;
5228
+ }
5229
+ function smokeCommand(pkg, moduleSystem) {
5230
+ const js = moduleSystem === "esm" ? `await import(${JSON.stringify(pkg)})` : `require(${JSON.stringify(pkg)})`;
5231
+ const flags = moduleSystem === "esm" ? "--input-type=module " : "";
5232
+ return `node ${flags}-e ${shQuote(js)}`;
5233
+ }
5234
+ function condense2(s) {
5235
+ return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX2);
5236
+ }
5237
+ function errText(err) {
5238
+ if (err && typeof err === "object") {
5239
+ const e = err;
5240
+ if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
5241
+ if (typeof e.message === "string") return e.message;
5242
+ }
5243
+ return String(err);
5244
+ }
5245
+ var INSTALL_TIMEOUT_MS2, SMOKE_TIMEOUT_MS2, ERROR_MAX2, WORKDIR, toSpec2, E2BSandbox;
5246
+ var init_e2b = __esm({
5247
+ "src/sandbox/e2b.ts"() {
5248
+ "use strict";
5249
+ init_esm_shims();
5250
+ init_config();
5251
+ init_types2();
5252
+ INSTALL_TIMEOUT_MS2 = 12e4;
5253
+ SMOKE_TIMEOUT_MS2 = 3e4;
5254
+ ERROR_MAX2 = 500;
5255
+ WORKDIR = "/home/user";
5256
+ toSpec2 = (p) => p.version ? `${p.name}@${p.version}` : p.name;
5257
+ E2BSandbox = class {
5258
+ name = "e2b";
5259
+ async verify(pkg, version, opts = {}) {
5260
+ const set = await this.verifySet([{ name: pkg, version }], opts);
5261
+ return {
5262
+ driver: this.name,
5263
+ moduleSystem: set.moduleSystem,
5264
+ installed: set.installed,
5265
+ imported: set.loaded[0]?.loaded ?? null,
5266
+ ranScripts: opts.allowScripts ?? false,
5267
+ durationMs: set.durationMs,
5268
+ error: set.error
5269
+ };
5270
+ }
5271
+ async verifySet(packages2, opts = {}) {
5272
+ const config = getConfig();
5273
+ const target = opts.target ?? DEFAULT_TARGET;
5274
+ const allowScripts = opts.allowScripts ?? false;
5275
+ const specs = packages2.map(toSpec2);
5276
+ const installTimeout = opts.timeoutMs ?? INSTALL_TIMEOUT_MS2;
5277
+ const started = Date.now();
5278
+ const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
5279
+ let installed = false;
5280
+ let error = null;
5281
+ const install = installCommand(specs, allowScripts);
5282
+ const createOpts = {
5283
+ apiKey: config.E2B_API_KEY,
5284
+ timeoutMs: installTimeout + SMOKE_TIMEOUT_MS2 * packages2.length + 3e4
5285
+ };
5286
+ const sandbox = config.E2B_TEMPLATE ? await Sandbox.create(config.E2B_TEMPLATE, createOpts) : await Sandbox.create(createOpts);
5287
+ try {
5288
+ await sandbox.files.write(
5289
+ `${WORKDIR}/package.json`,
5290
+ JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
5291
+ );
5292
+ await sandbox.commands.run(install, { cwd: WORKDIR, timeoutMs: installTimeout });
5293
+ installed = true;
5294
+ for (let i = 0; i < packages2.length; i++) {
5295
+ try {
5296
+ await sandbox.commands.run(smokeCommand(packages2[i].name, target.moduleSystem), {
5297
+ cwd: WORKDIR,
5298
+ timeoutMs: SMOKE_TIMEOUT_MS2
5299
+ });
5300
+ loaded[i].loaded = true;
5301
+ } catch (err) {
5302
+ loaded[i].loaded = false;
5303
+ if (!error) error = condense2(errText(err));
5304
+ }
5305
+ }
5306
+ } catch (err) {
5307
+ error = condense2(errText(err));
5308
+ } finally {
5309
+ await sandbox.kill().catch(() => {
5310
+ });
5311
+ }
5312
+ return {
5313
+ driver: this.name,
5314
+ moduleSystem: target.moduleSystem,
5315
+ installed,
5316
+ loaded,
5317
+ durationMs: Date.now() - started,
5318
+ error
5319
+ };
5320
+ }
5321
+ };
5322
+ }
5323
+ });
5324
+
5325
+ // src/sandbox/index.ts
5326
+ async function getSandbox() {
5327
+ if (getConfig().E2B_API_KEY) {
5328
+ const { E2BSandbox: E2BSandbox2 } = await Promise.resolve().then(() => (init_e2b(), e2b_exports));
5329
+ return new E2BSandbox2();
5330
+ }
5331
+ return new LocalSandbox();
5332
+ }
5333
+ var init_sandbox = __esm({
5334
+ "src/sandbox/index.ts"() {
5335
+ "use strict";
5336
+ init_esm_shims();
5337
+ init_config();
5338
+ init_local();
5339
+ init_types2();
5340
+ init_local();
5341
+ }
5342
+ });
5343
+
5344
+ // src/pipeline/sandbox.ts
5345
+ var sandbox_exports = {};
5346
+ __export(sandbox_exports, {
5347
+ verifyPackageInSandbox: () => verifyPackageInSandbox
5348
+ });
5349
+ async function verifyPackageInSandbox(db, pkg, version, opts = {}) {
5350
+ const result = await (await getSandbox()).verify(pkg, version, opts);
5351
+ await storeVerificationRun(db, {
5352
+ packageName: pkg,
5353
+ version: version ?? "latest",
5354
+ driver: result.driver,
5355
+ moduleSystem: result.moduleSystem,
5356
+ installed: result.installed,
5357
+ imported: result.imported,
5358
+ ranScripts: result.ranScripts,
5359
+ durationMs: result.durationMs,
5360
+ error: result.error,
5361
+ ranAt: /* @__PURE__ */ new Date()
5362
+ }).catch(() => {
5363
+ });
5364
+ return result;
5365
+ }
5366
+ var init_sandbox2 = __esm({
5367
+ "src/pipeline/sandbox.ts"() {
5368
+ "use strict";
5369
+ init_esm_shims();
5370
+ init_verification();
5371
+ init_sandbox();
5372
+ }
5373
+ });
5374
+
5375
+ // src/pipeline/compat.ts
5376
+ var compat_exports = {};
5377
+ __export(compat_exports, {
5378
+ deriveCompatEdges: () => deriveCompatEdges,
5379
+ verifyCompatibility: () => verifyCompatibility
5380
+ });
5381
+ function deriveCompatEdges(resolved, result) {
5382
+ const allLoaded = result.loaded.every((l) => l.loaded === true);
5383
+ const edges = [];
5384
+ if (result.installed && allLoaded) {
5385
+ for (let i = 0; i < resolved.length; i++) {
5386
+ for (let j = i + 1; j < resolved.length; j++) {
5387
+ edges.push({
5388
+ a: resolved[i].name,
5389
+ aVersion: resolved[i].version,
5390
+ b: resolved[j].name,
5391
+ bVersion: resolved[j].version,
5392
+ status: "compatible"
5393
+ });
5394
+ }
5395
+ }
5396
+ } else if (resolved.length === 2) {
5397
+ edges.push({
5398
+ a: resolved[0].name,
5399
+ aVersion: resolved[0].version,
5400
+ b: resolved[1].name,
5401
+ bVersion: resolved[1].version,
5402
+ status: "conflict"
5403
+ });
5404
+ }
5405
+ return edges;
5406
+ }
5407
+ async function verifyCompatibility(db, packages2, opts = {}) {
5408
+ const resolved = await Promise.all(
5409
+ packages2.map(async (name) => ({
5410
+ name,
5411
+ version: (await getPackageByName(db, name))?.latestVersion ?? "latest"
5412
+ }))
5413
+ );
5414
+ const result = await (await getSandbox()).verifySet(
5415
+ resolved.map((r) => ({ name: r.name, version: r.version === "latest" ? null : r.version })),
5416
+ { allowScripts: opts.allowScripts }
5417
+ );
5418
+ const edges = deriveCompatEdges(resolved, result);
5419
+ for (const e of edges) {
5420
+ const pair = canonicalPair(
5421
+ { name: e.a, version: e.aVersion },
5422
+ { name: e.b, version: e.bVersion }
5423
+ );
5424
+ await upsertCompatEdge(db, {
5425
+ ...pair,
5426
+ status: e.status,
5427
+ driver: result.driver,
5428
+ ranAt: /* @__PURE__ */ new Date()
5429
+ }).catch(() => {
5430
+ });
5431
+ }
5432
+ const failed = !result.installed || !result.loaded.every((l) => l.loaded === true);
5433
+ return { result, edges, unattributedConflict: failed && edges.length === 0 };
5434
+ }
5435
+ var init_compat2 = __esm({
5436
+ "src/pipeline/compat.ts"() {
5437
+ "use strict";
5438
+ init_esm_shims();
5439
+ init_compat();
5440
+ init_packages();
5441
+ init_sandbox();
5442
+ }
5443
+ });
5444
+
3180
5445
  // src/cli/commands.ts
3181
5446
  var commands_exports = {};
3182
5447
  __export(commands_exports, {
3183
5448
  runCompare: () => runCompare,
5449
+ runCompat: () => runCompat,
3184
5450
  runEditWeights: () => runEditWeights,
3185
5451
  runEvaluate: () => runEvaluate,
5452
+ runPlan: () => runPlan,
3186
5453
  runRecommend: () => runRecommend,
5454
+ runSandbox: () => runSandbox,
3187
5455
  runVerify: () => runVerify,
5456
+ runVersions: () => runVersions,
5457
+ runWatch: () => runWatch,
3188
5458
  runWeights: () => runWeights
3189
5459
  });
3190
5460
  async function withDb(fn) {
@@ -3257,6 +5527,12 @@ async function runEvaluate(pkg, opts) {
3257
5527
  ["repo", res.repoUrl ?? "\u2014"]
3258
5528
  ])
3259
5529
  );
5530
+ if (res.buildVerified) {
5531
+ const bv = res.buildVerified;
5532
+ const state = !bv.installed ? red("install failed") : bv.loaded === false ? yellow("installs, load failed") : green("installs and loads");
5533
+ console.log(`
5534
+ sandbox: ${state} ${dim(`${bv.version} \xB7 ${bv.driver}`)}`);
5535
+ }
3260
5536
  if (res.summary) console.log("\n" + res.summary);
3261
5537
  if (res.usageGuide) {
3262
5538
  const g = res.usageGuide;
@@ -3301,9 +5577,9 @@ not found: ${res.missing.join(", ")}`));
3301
5577
  }
3302
5578
  function runWeights(opts = {}) {
3303
5579
  const w = loadWeights();
3304
- const active = activeWeightsPath();
5580
+ const active2 = activeWeightsPath();
3305
5581
  if (opts.json) {
3306
- console.log(JSON.stringify({ ...w, source: active?.source ?? "defaults" }, null, 2));
5582
+ console.log(JSON.stringify({ ...w, source: active2?.source ?? "defaults" }, null, 2));
3307
5583
  return;
3308
5584
  }
3309
5585
  const pct = (n) => n.toFixed(2);
@@ -3330,13 +5606,15 @@ function runWeights(opts = {}) {
3330
5606
  ])
3331
5607
  );
3332
5608
  console.log(dim(`
3333
- Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
5609
+ Source: ${active2 ? `${active2.source} (${active2.path})` : "defaults (no user overrides)"}`));
3334
5610
  }
3335
- function runEditWeights(opts) {
5611
+ async function runEditWeights(opts) {
5612
+ const { invalidateCache: invalidateCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
3336
5613
  if (opts.reset) {
3337
5614
  const removed = resetWeights();
3338
5615
  console.log(removed.length ? `Removed overrides:
3339
5616
  ${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
5617
+ if (removed.length) await invalidateCache2();
3340
5618
  return;
3341
5619
  }
3342
5620
  if (opts.explain) {
@@ -3352,6 +5630,7 @@ function runEditWeights(opts) {
3352
5630
  const next = applyOverrides(loadWeights(), opts.set);
3353
5631
  const { weights, normalized } = validateWeights(next);
3354
5632
  const path2 = saveWeights(weights, opts.project ? "project" : "user");
5633
+ await invalidateCache2();
3355
5634
  console.log(`Saved overrides to ${path2}`);
3356
5635
  if (normalized) {
3357
5636
  console.log(
@@ -3365,11 +5644,83 @@ function runEditWeights(opts) {
3365
5644
  `));
3366
5645
  runWeights(opts);
3367
5646
  }
5647
+ async function openInBrowser(target) {
5648
+ const { spawn } = await import("child_process");
5649
+ const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", target]] : ["xdg-open", [target]];
5650
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
5651
+ }
5652
+ async function runPlan(file, opts) {
5653
+ const { readFileSync: readFileSync4 } = await import("fs");
5654
+ let document;
5655
+ try {
5656
+ document = readFileSync4(file, "utf8");
5657
+ } catch {
5658
+ throw new Error(`Could not read "${file}".`);
5659
+ }
5660
+ if (opts.optimize && opts.optimize !== "speed" && opts.optimize !== "balanced") {
5661
+ throw new Error("--optimize must be 'speed' or 'balanced'.");
5662
+ }
5663
+ await withDb(async (db) => {
5664
+ const { handlePlan: handlePlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
5665
+ const res = await handlePlan2(db, {
5666
+ document,
5667
+ optimize: opts.optimize
5668
+ });
5669
+ if (opts.json) return console.log(JSON.stringify(res, null, 2));
5670
+ if (!("slots" in res)) {
5671
+ console.log(res.note);
5672
+ return;
5673
+ }
5674
+ if (opts.html || opts.open) {
5675
+ const { writeFileSync: writeFileSync3 } = await import("fs");
5676
+ const { tmpdir: tmpdir2 } = await import("os");
5677
+ const { join: join5 } = await import("path");
5678
+ const { renderPlanHtml: renderPlanHtml2 } = await Promise.resolve().then(() => (init_planView(), planView_exports));
5679
+ const out = opts.html ?? join5(tmpdir2(), `lurq-plan-${Date.now()}.html`);
5680
+ writeFileSync3(out, renderPlanHtml2(res), "utf8");
5681
+ console.log(`Roadmap written to ${out}`);
5682
+ if (opts.open) await openInBrowser(out);
5683
+ }
5684
+ console.log(
5685
+ table(
5686
+ ["Component", "Layer", "Recommended", "Health", "Confidence", "Alternatives"],
5687
+ res.slots.map((s) => [
5688
+ s.need.length > 32 ? s.need.slice(0, 31) + "\u2026" : s.need,
5689
+ s.layer,
5690
+ s.recommended ? `${s.recommended.name}@${s.recommended.latestVersion ?? "?"}` : dim("\u2014"),
5691
+ s.recommended ? String(s.recommended.healthScore) : "\u2014",
5692
+ s.recommended ? confidenceLabel(s.recommended.confidence) : "\u2014",
5693
+ s.alternatives.map((a) => a.name).join(", ") || "\u2014"
5694
+ ])
5695
+ )
5696
+ );
5697
+ if (res.unmatched.length) console.log(yellow(`
5698
+ No match for: ${res.unmatched.join(", ")}`));
5699
+ if (res.compatibility) {
5700
+ const c = res.compatibility;
5701
+ const col = c.overall === "compatible" ? green : c.overall === "conflict" ? red : dim;
5702
+ console.log("\n" + bold("Compatibility: ") + col(c.overall));
5703
+ for (const s of res.slots) {
5704
+ if (s.swappedFrom && s.recommended) {
5705
+ console.log(green(` \u2713 swapped ${s.swappedFrom} \u2192 ${s.recommended.name} for compatibility`));
5706
+ }
5707
+ }
5708
+ for (const cf of c.conflicts) console.log(red(` \u2717 ${cf.detail} (no compatible alternative)`));
5709
+ if (c.unverified.length) console.log(dim(` unverified: ${c.unverified.join(", ")}`));
5710
+ }
5711
+ console.log("\n" + bold("Roadmap (Mermaid):"));
5712
+ console.log(res.mermaid);
5713
+ console.log(dim(`
5714
+ ${res.note}`));
5715
+ console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
5716
+ });
5717
+ }
3368
5718
  async function runVerify(pkg, opts) {
3369
5719
  await withDb(async (db) => {
3370
5720
  const res = await handleVerify(db, { package: pkg });
3371
5721
  if (opts.json) return console.log(JSON.stringify(res, null, 2));
3372
- 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");
5722
+ const riskColor = res.risk === "high" ? red : res.risk === "medium" ? yellow : green;
5723
+ 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");
3373
5724
  console.log(`${bold(pkg)} ${verdict}`);
3374
5725
  console.log(
3375
5726
  detail([
@@ -3377,11 +5728,107 @@ async function runVerify(pkg, opts) {
3377
5728
  ["weekly dl", formatNumber(res.weeklyDownloads)],
3378
5729
  ["confidence", res.confidence ? confidenceLabel(res.confidence) : "\u2014"],
3379
5730
  ["advisories", String(res.advisoryCount)],
5731
+ ["risk", riskColor(res.risk)],
3380
5732
  ["risk flags", res.riskFlags.length ? yellow(res.riskFlags.join(", ")) : "none"]
3381
5733
  ])
3382
5734
  );
3383
5735
  });
3384
5736
  }
5737
+ async function runVersions(pkg, opts) {
5738
+ const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10) || 30) : 30;
5739
+ await withDb(async (db) => {
5740
+ const versions = await getPackageVersions(db, pkg, limit);
5741
+ if (opts.json) return console.log(JSON.stringify(versions, null, 2));
5742
+ if (versions.length === 0) {
5743
+ console.log(`No stored versions for ${bold(pkg)}. Run \`lurq sync --package ${pkg}\` first.`);
5744
+ return;
5745
+ }
5746
+ console.log(bold(pkg));
5747
+ console.log(
5748
+ table(
5749
+ ["Version", "Published"],
5750
+ versions.map((v) => [
5751
+ v.version,
5752
+ v.publishedAt ? v.publishedAt.toISOString().slice(0, 10) : "\u2014"
5753
+ ])
5754
+ )
5755
+ );
5756
+ });
5757
+ }
5758
+ async function runWatch() {
5759
+ const { watchNpmChanges: watchNpmChanges2 } = await Promise.resolve().then(() => (init_watch2(), watch_exports));
5760
+ await withDb(async (db) => {
5761
+ const controller = new AbortController();
5762
+ const stop = () => controller.abort();
5763
+ process.once("SIGINT", stop);
5764
+ process.once("SIGTERM", stop);
5765
+ console.log(dim("watching npm for releases of tracked packages \u2014 Ctrl-C to stop"));
5766
+ try {
5767
+ await watchNpmChanges2(db, { signal: controller.signal });
5768
+ } finally {
5769
+ process.off("SIGINT", stop);
5770
+ process.off("SIGTERM", stop);
5771
+ }
5772
+ });
5773
+ }
5774
+ async function runSandbox(pkg, version, opts) {
5775
+ if (opts.allowScripts) {
5776
+ console.error(
5777
+ yellow("warning: running install scripts and loading the package locally without isolation")
5778
+ );
5779
+ }
5780
+ const { verifyPackageInSandbox: verifyPackageInSandbox2 } = await Promise.resolve().then(() => (init_sandbox2(), sandbox_exports));
5781
+ await withDb(async (db) => {
5782
+ const result = await verifyPackageInSandbox2(db, pkg, version ?? null, {
5783
+ target: { node: "20", moduleSystem: opts.esm ? "esm" : "cjs" },
5784
+ allowScripts: opts.allowScripts
5785
+ });
5786
+ if (opts.json) return console.log(JSON.stringify(result, null, 2));
5787
+ const ok = result.installed && result.imported !== false;
5788
+ const label = version ? `${pkg}@${version}` : pkg;
5789
+ const verdict = ok ? green("\u2713 installs and loads") : red("\u2717 failed");
5790
+ console.log(`${bold(label)} ${verdict} ${dim(`(${result.durationMs}ms \xB7 ${result.driver})`)}`);
5791
+ console.log(
5792
+ detail([
5793
+ ["installed", result.installed ? "yes" : "no"],
5794
+ ["loaded", result.imported === null ? "\u2014" : result.imported ? "yes" : "no"],
5795
+ ["module", result.moduleSystem],
5796
+ ["scripts", result.ranScripts ? "ran" : "skipped"],
5797
+ ["error", result.error ?? "none"]
5798
+ ])
5799
+ );
5800
+ });
5801
+ }
5802
+ async function runCompat(pkgs, opts) {
5803
+ await withDb(async (db) => {
5804
+ if (opts.run) {
5805
+ console.error(
5806
+ yellow("co-installing in the sandbox (loads package code locally without isolation)")
5807
+ );
5808
+ const { verifyCompatibility: verifyCompatibility2 } = await Promise.resolve().then(() => (init_compat2(), compat_exports));
5809
+ await verifyCompatibility2(db, pkgs);
5810
+ }
5811
+ const { handleCompat: handleCompat2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
5812
+ const res = await handleCompat2(db, { packages: pkgs });
5813
+ if (opts.json) return console.log(JSON.stringify(res, null, 2));
5814
+ const color = res.overall === "compatible" ? green : res.overall === "conflict" ? red : dim;
5815
+ console.log(`${bold(res.packages.join(" + "))} ${color(res.overall)}`);
5816
+ if (res.conflicts.length) {
5817
+ console.log(
5818
+ table(
5819
+ ["Source", "Detail"],
5820
+ res.conflicts.map((c) => [c.source, c.detail])
5821
+ )
5822
+ );
5823
+ } else if (res.overall === "compatible") {
5824
+ console.log(dim("no peer-dependency or engine conflicts across the set"));
5825
+ }
5826
+ if (res.unverified.length) {
5827
+ console.log(dim(`
5828
+ unverified (no metadata): ${res.unverified.join(", ")}`));
5829
+ }
5830
+ });
5831
+ }
3385
5832
  var init_commands = __esm({
3386
5833
  "src/cli/commands.ts"() {
3387
5834
  "use strict";
@@ -3389,6 +5836,7 @@ var init_commands = __esm({
3389
5836
  init_config();
3390
5837
  init_types();
3391
5838
  init_client();
5839
+ init_packages();
3392
5840
  init_handlers();
3393
5841
  init_weights();
3394
5842
  init_weights();
@@ -3414,9 +5862,9 @@ __export(installSkill_exports, {
3414
5862
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3415
5863
  import { copyFileSync } from "fs";
3416
5864
  import { homedir as homedir3 } from "os";
3417
- import { dirname as dirname4, join as join3 } from "path";
5865
+ import { dirname as dirname4, join as join4 } from "path";
3418
5866
  function home(...p) {
3419
- return join3(homedir3(), ...p);
5867
+ return join4(homedir3(), ...p);
3420
5868
  }
3421
5869
  function agentSpecs() {
3422
5870
  return [
@@ -3550,10 +5998,10 @@ function installAgent(spec, mode) {
3550
5998
  }
3551
5999
  }
3552
6000
  function installInstructionsFile() {
3553
- const src = join3(packageRoot(), "templates", "skill-instructions.md");
6001
+ const src = join4(packageRoot(), "templates", "skill-instructions.md");
3554
6002
  if (!existsSync4(src)) return null;
3555
6003
  const destDir = home(".lurq");
3556
- const dest = join3(destDir, "skill-instructions.md");
6004
+ const dest = join4(destDir, "skill-instructions.md");
3557
6005
  mkdirSync2(destDir, { recursive: true });
3558
6006
  copyFileSync(src, dest);
3559
6007
  return dest;
@@ -3654,9 +6102,11 @@ async function validateKey(url, apiKey) {
3654
6102
  },
3655
6103
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
3656
6104
  });
3657
- return res.ok;
6105
+ if (res.ok) return "valid";
6106
+ if (res.status === 401 || res.status === 403) return "invalid";
6107
+ return "unreachable";
3658
6108
  } catch {
3659
- return false;
6109
+ return "unreachable";
3660
6110
  }
3661
6111
  }
3662
6112
  async function runInstallWizard(opts) {
@@ -3664,7 +6114,7 @@ async function runInstallWizard(opts) {
3664
6114
  const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
3665
6115
  let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
3666
6116
  if (interactive) {
3667
- const { input, checkbox, confirm } = await import("@inquirer/prompts");
6117
+ const { input, checkbox, confirm, select } = await import("@inquirer/prompts");
3668
6118
  console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
3669
6119
  if (!apiKey) {
3670
6120
  console.log(` Need a key? Get one at ${GET_KEY_URL}
@@ -3675,11 +6125,13 @@ async function runInstallWizard(opts) {
3675
6125
  })).trim();
3676
6126
  }
3677
6127
  process.stdout.write(" Validating key\u2026 ");
3678
- const ok = await validateKey(url, apiKey);
3679
- console.log(ok ? "ok" : "could not reach endpoint");
3680
- if (!ok) {
6128
+ const check = await validateKey(url, apiKey);
6129
+ console.log(
6130
+ check === "valid" ? "ok" : check === "invalid" ? "rejected" : "could not reach endpoint"
6131
+ );
6132
+ if (check !== "valid") {
3681
6133
  const proceed = await confirm({
3682
- message: `Couldn't validate the key against ${url}. Continue anyway?`,
6134
+ message: check === "invalid" ? `That key was rejected (401) by ${url}. Continue anyway?` : `Couldn't reach ${url} to validate the key. Continue anyway?`,
3683
6135
  default: false
3684
6136
  });
3685
6137
  if (!proceed) {
@@ -3687,22 +6139,45 @@ async function runInstallWizard(opts) {
3687
6139
  return;
3688
6140
  }
3689
6141
  }
3690
- let selected2;
3691
6142
  if (opts.agent) {
3692
- selected2 = resolveAgents(opts.agent);
3693
- } else {
3694
- const specs = agentSpecs();
3695
- const ids = await checkbox({
3696
- message: "Which assistant(s) should I configure?",
3697
- choices: specs.map((s) => ({
3698
- name: `${s.label}${s.detected ? " (detected)" : ""}`,
3699
- value: s.id,
3700
- checked: s.detected
3701
- }))
6143
+ await finish(resolveAgents(opts.agent), { url, apiKey });
6144
+ return;
6145
+ }
6146
+ const specs = agentSpecs();
6147
+ const detected = specs.filter((s) => s.detected);
6148
+ const primary = detected.find((s) => s.id === "claude-code") ?? detected[0];
6149
+ if (primary) {
6150
+ const others = detected.length - 1;
6151
+ const choice = await select({
6152
+ 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?`,
6153
+ default: "yes",
6154
+ choices: [
6155
+ {
6156
+ name: others > 0 ? `Yes \u2014 set up all ${detected.length} detected agents` : `Yes \u2014 set up ${primary.label} for me`,
6157
+ value: "yes"
6158
+ },
6159
+ { name: "No \u2014 let me choose which agent(s)", value: "other" },
6160
+ { name: "Cancel \u2014 change nothing", value: "cancel" }
6161
+ ]
3702
6162
  });
3703
- selected2 = specs.filter((s) => ids.includes(s.id));
6163
+ if (choice === "cancel") {
6164
+ console.log("No problem \u2014 nothing was changed. Run `lurq install` again anytime.");
6165
+ return;
6166
+ }
6167
+ if (choice === "yes") {
6168
+ await finish(detected, { url, apiKey });
6169
+ return;
6170
+ }
3704
6171
  }
3705
- await finish(selected2, { url, apiKey });
6172
+ const ids = await checkbox({
6173
+ message: "Which assistant(s) should I configure?",
6174
+ choices: specs.map((s) => ({
6175
+ name: `${s.label}${s.detected ? " (detected)" : ""}`,
6176
+ value: s.id,
6177
+ checked: s.detected
6178
+ }))
6179
+ });
6180
+ await finish(specs.filter((s) => ids.includes(s.id)), { url, apiKey });
3706
6181
  return;
3707
6182
  }
3708
6183
  if (!apiKey) {
@@ -3742,7 +6217,8 @@ var keys_exports = {};
3742
6217
  __export(keys_exports, {
3743
6218
  runKeysCreate: () => runKeysCreate,
3744
6219
  runKeysList: () => runKeysList,
3745
- runKeysRevoke: () => runKeysRevoke
6220
+ runKeysRevoke: () => runKeysRevoke,
6221
+ runKeysRotate: () => runKeysRotate
3746
6222
  });
3747
6223
  function waitForEnter(prompt) {
3748
6224
  return new Promise((resolve) => {
@@ -3754,28 +6230,55 @@ function waitForEnter(prompt) {
3754
6230
  });
3755
6231
  });
3756
6232
  }
6233
+ async function presentNewKey(key, row, opts) {
6234
+ if (opts.json) {
6235
+ console.log(
6236
+ JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label, ...opts.extraJson })
6237
+ );
6238
+ return;
6239
+ }
6240
+ const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
6241
+ const block = [bold(opts.header ?? "API key created."), "", ` ${green(key)}`, "", dim(meta)];
6242
+ console.log(block.join("\n"));
6243
+ if (process.stdout.isTTY && process.stdin.isTTY) {
6244
+ await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
6245
+ process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
6246
+ console.log(
6247
+ 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.`)
6248
+ );
6249
+ } else {
6250
+ console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
6251
+ }
6252
+ }
3757
6253
  async function runKeysCreate(opts) {
3758
6254
  requireConfig(["DATABASE_URL"]);
3759
6255
  const { db, close } = createDb({ max: 1 });
3760
6256
  try {
3761
- const { key, row } = await createKey(db, { label: opts.label, tier: opts.tier });
3762
- if (opts.json) {
3763
- console.log(JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label }));
6257
+ const { key, row } = await createKey(db, {
6258
+ label: opts.label,
6259
+ tier: opts.tier,
6260
+ ownerId: opts.owner
6261
+ });
6262
+ await presentNewKey(key, row, opts);
6263
+ } finally {
6264
+ await close();
6265
+ }
6266
+ }
6267
+ async function runKeysRotate(prefixOrId, opts) {
6268
+ requireConfig(["DATABASE_URL"]);
6269
+ const { db, close } = createDb({ max: 1 });
6270
+ try {
6271
+ const result = await rotateKey(db, prefixOrId);
6272
+ if (!result) {
6273
+ logger.warn(`No active key matched "${prefixOrId}".`);
6274
+ process.exitCode = 1;
3764
6275
  return;
3765
6276
  }
3766
- const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
3767
- const block = [bold("API key created."), "", ` ${green(key)}`, "", dim(meta)];
3768
- const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
3769
- console.log(block.join("\n"));
3770
- if (interactive) {
3771
- await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
3772
- process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
3773
- console.log(
3774
- dim(`API key created (prefix ${row.prefix}) \u2014 value erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
3775
- );
3776
- } else {
3777
- console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
3778
- }
6277
+ await presentNewKey(result.key, result.row, {
6278
+ json: opts.json,
6279
+ header: `API key rotated \u2014 replaces ${result.previous.prefix} (now revoked).`,
6280
+ extraJson: { replaced: result.previous.prefix }
6281
+ });
3779
6282
  } finally {
3780
6283
  await close();
3781
6284
  }
@@ -3856,7 +6359,7 @@ var init_keys = __esm({
3856
6359
 
3857
6360
  // src/db/seed.ts
3858
6361
  import { readFileSync as readFileSync3 } from "fs";
3859
- import { sql as sql7 } from "drizzle-orm";
6362
+ import { sql as sql5 } from "drizzle-orm";
3860
6363
  import { z as z3 } from "zod";
3861
6364
  function loadSeedFile(path2 = seedJsonPath()) {
3862
6365
  const raw = JSON.parse(readFileSync3(path2, "utf8"));
@@ -3873,7 +6376,7 @@ async function loadSeedPackages(db, path2) {
3873
6376
  await db.insert(seedPackages).values(entries.map((e) => ({ name: e.name, category: e.category ?? null }))).onConflictDoUpdate({
3874
6377
  target: seedPackages.name,
3875
6378
  // Refresh category to the incoming value on conflict (EXCLUDED.category).
3876
- set: { category: sql7`excluded.category` }
6379
+ set: { category: sql5`excluded.category` }
3877
6380
  });
3878
6381
  logger.info(`Loaded ${entries.length} packages into seed_packages.`);
3879
6382
  return entries.length;
@@ -3994,13 +6497,21 @@ function buildProgram() {
3994
6497
  const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
3995
6498
  await runVerify2(pkg, opts);
3996
6499
  });
6500
+ 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) => {
6501
+ const { runVersions: runVersions2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6502
+ await runVersions2(pkg, opts);
6503
+ });
6504
+ 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) => {
6505
+ const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6506
+ await runPlan2(file, opts);
6507
+ });
3997
6508
  program.command("weights").description("show and explain the scoring weight model (health, quality, composite \u03BB)").option("--json", "output the weight model as JSON").action(async (opts) => {
3998
6509
  const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
3999
6510
  runWeights2(opts);
4000
6511
  });
4001
6512
  program.command("edit-weights").description("override, reset, or explain the scoring weights (layered over defaults)").option("--set <pair>", "override key=value, e.g. composite.lambda=0.5 (repeatable)", (v, acc) => acc.concat(v), []).option("--reset", "remove all overrides and restore defaults").option("--explain <component>", "explain a component (e.g. adoption, quality, lambda)").option("--project", "write to project-local .lurq/weights.json instead of the user config").action(async (opts) => {
4002
6513
  const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
4003
- runEditWeights2(opts);
6514
+ await runEditWeights2(opts);
4004
6515
  });
4005
6516
  program.command("discover").description("operator-side: proactively crawl for new packages and queue/gate them (\xA72B)").option("--cap <n>", "max candidates to fully ingest this run", (v) => parseInt(v, 10)).option("--dry-run", "discover, queue, and gate, but do not ingest survivors").option("--json", "output the discovery summary as JSON").action(async (opts) => {
4006
6517
  const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -4016,6 +6527,24 @@ function buildProgram() {
4016
6527
  const summary = await runRescore2();
4017
6528
  if (opts.json) console.log(JSON.stringify(summary, null, 2));
4018
6529
  });
6530
+ program.command("watch").description(
6531
+ "operator-side: follow the npm changes feed, re-syncing tracked packages on new releases"
6532
+ ).action(async () => {
6533
+ const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6534
+ await runWatch2();
6535
+ });
6536
+ program.command("sandbox").argument("<package>", "npm package name").argument("[version]", "specific version (default: latest)").description(
6537
+ "operator-side: install + smoke-load a package in a sandbox to verify it actually works"
6538
+ ).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(
6539
+ async (pkg, version, opts) => {
6540
+ const { runSandbox: runSandbox2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6541
+ await runSandbox2(pkg, version, opts);
6542
+ }
6543
+ );
6544
+ 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) => {
6545
+ const { runCompat: runCompat2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
6546
+ await runCompat2(pkgs, opts);
6547
+ });
4019
6548
  program.command("install").description("guided setup: connect lurq to your AI assistant(s)").option("--api-key <key>", "hosted API key (skips the prompt)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--agent <agent>", "claude-code | cursor | copilot | windsurf | codex | all").option("--yes", "non-interactive: use flags/env and detected agents without prompting").action(async (opts) => {
4020
6549
  const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
4021
6550
  await runInstallWizard2(opts);
@@ -4029,7 +6558,7 @@ function buildProgram() {
4029
6558
  await runInstallSkill2(opts);
4030
6559
  });
4031
6560
  const keys = program.command("keys").description("manage API keys for the hosted service (operator; needs DATABASE_URL)");
4032
- keys.command("create").description("create a new API key (shown once; erased from the terminal after you copy it)").option("--label <label>", "human label (owner / org / purpose)").option("--tier <tier>", "tier name", "free").option("--json", "print the key as JSON and skip the interactive erase (for scripts)").action(async (opts) => {
6561
+ 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("--owner <id>", "org/owner id to attribute this key to (e.g. a Clerk org id)").option("--json", "print the key as JSON and skip the interactive erase (for scripts)").action(async (opts) => {
4033
6562
  const { runKeysCreate: runKeysCreate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4034
6563
  await runKeysCreate2(opts);
4035
6564
  });
@@ -4037,6 +6566,10 @@ function buildProgram() {
4037
6566
  const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4038
6567
  await runKeysList2(opts);
4039
6568
  });
6569
+ 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) => {
6570
+ const { runKeysRotate: runKeysRotate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
6571
+ await runKeysRotate2(prefixOrId, opts);
6572
+ });
4040
6573
  keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
4041
6574
  const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4042
6575
  await runKeysRevoke2(prefixOrId);