lurqrun 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -213,10 +213,14 @@ var init_logger = __esm({
213
213
  var schema_exports = {};
214
214
  __export(schema_exports, {
215
215
  apiKeys: () => apiKeys,
216
+ compatEdges: () => compatEdges,
216
217
  discoveryQueue: () => discoveryQueue,
218
+ packageVersions: () => packageVersions,
217
219
  packages: () => packages,
218
220
  seedPackages: () => seedPackages,
219
- syncRuns: () => syncRuns
221
+ syncRuns: () => syncRuns,
222
+ verificationRuns: () => verificationRuns,
223
+ watchState: () => watchState
220
224
  });
221
225
  import { sql } from "drizzle-orm";
222
226
  import {
@@ -227,13 +231,15 @@ import {
227
231
  integer,
228
232
  jsonb,
229
233
  pgTable,
234
+ primaryKey,
230
235
  real,
231
236
  serial,
232
237
  text,
233
238
  timestamp,
239
+ uniqueIndex,
234
240
  vector
235
241
  } from "drizzle-orm/pg-core";
236
- var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys;
242
+ var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges;
237
243
  var init_schema = __esm({
238
244
  "src/db/schema.ts"() {
239
245
  "use strict";
@@ -270,7 +276,6 @@ var init_schema = __esm({
270
276
  // Adoption signals
271
277
  weeklyDownloads: bigint("weekly_downloads", { mode: "number" }),
272
278
  downloadGrowth90d: real("download_growth_90d"),
273
- dependentsCount: integer("dependents_count"),
274
279
  stars: integer("stars"),
275
280
  openIssues: integer("open_issues"),
276
281
  closedIssues: integer("closed_issues"),
@@ -278,6 +283,11 @@ var init_schema = __esm({
278
283
  scorecard: real("scorecard"),
279
284
  bundleMinGzipKb: real("bundle_min_gzip_kb"),
280
285
  advisories: jsonb("advisories").$type(),
286
+ // Compatibility metadata (Tier-1): declared peer-deps + engines of the
287
+ // latest version, so a whole-stack peer-range check is one indexed query.
288
+ peerDependencies: jsonb("peer_dependencies").$type(),
289
+ peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
290
+ engines: jsonb("engines").$type(),
281
291
  // Computed outputs
282
292
  healthScore: integer("health_score"),
283
293
  /** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
@@ -287,6 +297,10 @@ var init_schema = __esm({
287
297
  scoreBreakdown: jsonb("score_breakdown").$type(),
288
298
  usageGuide: jsonb("usage_guide").$type(),
289
299
  embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
300
+ // Identity of the vector space `embedding` was produced in (e.g.
301
+ // `openai:text-embedding-3-small`, `local`). Vector search filters on the
302
+ // active provider so switching models can't compare incompatible spaces.
303
+ embeddingProvider: text("embedding_provider"),
290
304
  // Lexical search vector (§3): name weighted highest (A), then category (B),
291
305
  // then summary/description (C). Generated + STORED so it stays in sync with
292
306
  // the row automatically; indexed with GIN for fast `@@` matching.
@@ -353,6 +367,61 @@ var init_schema = __esm({
353
367
  },
354
368
  (table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
355
369
  );
370
+ packageVersions = pgTable(
371
+ "package_versions",
372
+ {
373
+ packageName: text("package_name").notNull(),
374
+ version: text("version").notNull(),
375
+ publishedAt: ts("published_at")
376
+ },
377
+ (table2) => [
378
+ primaryKey({ columns: [table2.packageName, table2.version] }),
379
+ index("package_versions_name_published_idx").on(table2.packageName, table2.publishedAt)
380
+ ]
381
+ );
382
+ watchState = pgTable("watch_state", {
383
+ id: text("id").primaryKey(),
384
+ seq: text("seq").notNull(),
385
+ updatedAt: ts("updated_at")
386
+ });
387
+ verificationRuns = pgTable(
388
+ "verification_runs",
389
+ {
390
+ id: serial("id").primaryKey(),
391
+ packageName: text("package_name").notNull(),
392
+ version: text("version").notNull(),
393
+ driver: text("driver").notNull(),
394
+ moduleSystem: text("module_system").notNull(),
395
+ installed: boolean("installed").notNull(),
396
+ imported: boolean("imported"),
397
+ ranScripts: boolean("ran_scripts").notNull().default(false),
398
+ durationMs: integer("duration_ms"),
399
+ error: text("error"),
400
+ ranAt: ts("ran_at")
401
+ },
402
+ (table2) => [index("verification_runs_pkg_idx").on(table2.packageName, table2.version)]
403
+ );
404
+ compatEdges = pgTable(
405
+ "compat_edges",
406
+ {
407
+ id: serial("id").primaryKey(),
408
+ packageA: text("package_a").notNull(),
409
+ versionA: text("version_a").notNull(),
410
+ packageB: text("package_b").notNull(),
411
+ versionB: text("version_b").notNull(),
412
+ status: text("status").$type().notNull(),
413
+ driver: text("driver").notNull(),
414
+ ranAt: ts("ran_at")
415
+ },
416
+ (table2) => [
417
+ uniqueIndex("compat_edges_pair_idx").on(
418
+ table2.packageA,
419
+ table2.versionA,
420
+ table2.packageB,
421
+ table2.versionB
422
+ )
423
+ ]
424
+ );
356
425
  }
357
426
  });
358
427
 
@@ -361,10 +430,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
361
430
  import postgres from "postgres";
362
431
  function createDb(opts = {}) {
363
432
  const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
364
- const sql8 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
433
+ const sql6 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
365
434
  } });
366
- const db = drizzle(sql8, { schema: schema_exports });
367
- return { db, sql: sql8, close: () => sql8.end() };
435
+ const db = drizzle(sql6, { schema: schema_exports });
436
+ return { db, sql: sql6, close: () => sql6.end() };
368
437
  }
369
438
  var init_client = __esm({
370
439
  "src/db/client.ts"() {
@@ -375,6 +444,131 @@ var init_client = __esm({
375
444
  }
376
445
  });
377
446
 
447
+ // src/core/cache.ts
448
+ var cache_exports = {};
449
+ __export(cache_exports, {
450
+ cached: () => cached2,
451
+ invalidateCache: () => invalidateCache
452
+ });
453
+ function ttlSeconds() {
454
+ const n = Number(process.env.LURQ_CACHE_TTL_SEC);
455
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_TTL_SEC;
456
+ }
457
+ async function getClient() {
458
+ if (!process.env.REDIS_URL) return null;
459
+ if (!clientPromise) {
460
+ clientPromise = (async () => {
461
+ try {
462
+ const { default: Redis } = await import("ioredis");
463
+ const client = new Redis(process.env.REDIS_URL, {
464
+ maxRetriesPerRequest: 1,
465
+ enableOfflineQueue: false,
466
+ lazyConnect: false,
467
+ // Railway's private network (redis.railway.internal) is IPv6-only and
468
+ // ioredis defaults to IPv4 — family:0 lets it resolve either stack, so
469
+ // both the private URL and a public/Upstash URL work unchanged.
470
+ family: 0
471
+ });
472
+ client.on("error", (err) => logger.warn(`redis: ${err.message}`));
473
+ logger.info("Response cache enabled (REDIS_URL set).");
474
+ return client;
475
+ } catch (err) {
476
+ logger.warn(`redis disabled (init failed): ${err.message}`);
477
+ return null;
478
+ }
479
+ })();
480
+ }
481
+ return clientPromise;
482
+ }
483
+ async function namespaceVersion(client) {
484
+ if (versionMemo && Date.now() - versionMemo.at < VERSION_MEMO_MS) return versionMemo.value;
485
+ const value = await client.get(VERSION_KEY).catch(() => null) ?? "0";
486
+ versionMemo = { value, at: Date.now() };
487
+ return value;
488
+ }
489
+ async function cached2(namespace, key, compute, opts = {}) {
490
+ const client = await getClient();
491
+ if (!client) return compute();
492
+ let fullKey;
493
+ try {
494
+ const version = await namespaceVersion(client);
495
+ fullKey = `lurq:${namespace}:${version}:${key}`;
496
+ const hit = await client.get(fullKey);
497
+ if (hit != null) return JSON.parse(hit);
498
+ } catch (err) {
499
+ logger.warn(`redis read failed, bypassing cache: ${err.message}`);
500
+ return compute();
501
+ }
502
+ const value = await compute();
503
+ if (!opts.skipCache?.(value)) {
504
+ client.set(fullKey, JSON.stringify(value), "EX", ttlSeconds()).catch(() => {
505
+ });
506
+ }
507
+ return value;
508
+ }
509
+ async function invalidateCache() {
510
+ const client = await getClient();
511
+ if (!client) return;
512
+ try {
513
+ await client.incr(VERSION_KEY);
514
+ versionMemo = null;
515
+ } catch (err) {
516
+ logger.warn(`redis invalidate failed: ${err.message}`);
517
+ }
518
+ }
519
+ var VERSION_KEY, DEFAULT_TTL_SEC, VERSION_MEMO_MS, clientPromise, versionMemo;
520
+ var init_cache = __esm({
521
+ "src/core/cache.ts"() {
522
+ "use strict";
523
+ init_esm_shims();
524
+ init_logger();
525
+ VERSION_KEY = "lurq:cachever";
526
+ DEFAULT_TTL_SEC = 3600;
527
+ VERSION_MEMO_MS = 1e4;
528
+ clientPromise = null;
529
+ versionMemo = null;
530
+ }
531
+ });
532
+
533
+ // src/db/compat.ts
534
+ import { and, inArray } from "drizzle-orm";
535
+ async function getCompatMetadata(db, names) {
536
+ if (names.length === 0) return [];
537
+ return db.select({
538
+ name: packages.name,
539
+ latestVersion: packages.latestVersion,
540
+ peerDependencies: packages.peerDependencies,
541
+ peerDependenciesMeta: packages.peerDependenciesMeta,
542
+ engines: packages.engines
543
+ }).from(packages).where(inArray(packages.name, names));
544
+ }
545
+ function canonicalPair(a, b) {
546
+ const [low, high] = a.name <= b.name ? [a, b] : [b, a];
547
+ return { packageA: low.name, versionA: low.version, packageB: high.name, versionB: high.version };
548
+ }
549
+ async function upsertCompatEdge(db, edge) {
550
+ await db.insert(compatEdges).values(edge).onConflictDoUpdate({
551
+ target: [
552
+ compatEdges.packageA,
553
+ compatEdges.versionA,
554
+ compatEdges.packageB,
555
+ compatEdges.versionB
556
+ ],
557
+ set: { status: edge.status, driver: edge.driver, ranAt: edge.ranAt }
558
+ });
559
+ }
560
+ async function getCompatEdges(db, names) {
561
+ if (names.length === 0) return [];
562
+ return db.select().from(compatEdges).where(and(inArray(compatEdges.packageA, names), inArray(compatEdges.packageB, names)));
563
+ }
564
+ var init_compat = __esm({
565
+ "src/db/compat.ts"() {
566
+ "use strict";
567
+ init_esm_shims();
568
+ init_schema();
569
+ }
570
+ });
571
+
378
572
  // src/core/http.ts
379
573
  import { createHash } from "crypto";
380
574
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -439,9 +633,9 @@ async function httpRequest(url, opts) {
439
633
  } = opts;
440
634
  const key = opts.cacheKey ?? `${method} ${url} ${body ?? ""}`;
441
635
  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 };
636
+ const cached3 = await readCache(key, ttlMs);
637
+ if (cached3) {
638
+ return { status: cached3.status, data: decode(cached3.body, accept), fromCache: true };
445
639
  }
446
640
  }
447
641
  const limiter = limiterFor(host);
@@ -626,9 +820,45 @@ function parseNpmRegistry(json2) {
626
820
  hasTypes: detectTypes(name, latestManifest),
627
821
  hasTestScript: detectTestScript(latestManifest),
628
822
  directDependenciesCount: countDeps(latestManifest?.dependencies),
629
- hasProvenance: detectProvenance(latestManifest)
823
+ hasProvenance: detectProvenance(latestManifest),
824
+ hasInstallScripts: detectInstallScripts(latestManifest),
825
+ peerDependencies: parseDepMap(latestManifest?.peerDependencies),
826
+ peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
827
+ engines: parseDepMap(latestManifest?.engines),
828
+ versionTimeline: parseVersionTimeline(json2)
630
829
  };
631
830
  }
831
+ function parseDepMap(value) {
832
+ if (!value || typeof value !== "object") return null;
833
+ const out = {};
834
+ for (const [name, range] of Object.entries(value)) {
835
+ if (typeof range === "string" && range.trim() !== "") out[name] = range;
836
+ }
837
+ return Object.keys(out).length ? out : null;
838
+ }
839
+ function parsePeerMeta(value) {
840
+ if (!value || typeof value !== "object") return null;
841
+ const out = {};
842
+ for (const [name, meta] of Object.entries(value)) {
843
+ if (meta && typeof meta === "object" && "optional" in meta) {
844
+ out[name] = { optional: Boolean(meta.optional) };
845
+ }
846
+ }
847
+ return Object.keys(out).length ? out : null;
848
+ }
849
+ function detectInstallScripts(manifest) {
850
+ const scripts = manifest?.scripts;
851
+ if (!scripts || typeof scripts !== "object") return false;
852
+ return ["preinstall", "install", "postinstall"].some(
853
+ (hook) => typeof scripts[hook] === "string" && scripts[hook].trim() !== ""
854
+ );
855
+ }
856
+ function parseVersionTimeline(json2) {
857
+ const versions = json2?.versions;
858
+ if (!versions || typeof versions !== "object") return [];
859
+ const time = json2?.time ?? {};
860
+ return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
861
+ }
632
862
  function parseKeywords(value) {
633
863
  if (!Array.isArray(value)) return [];
634
864
  return value.filter((k) => typeof k === "string");
@@ -1025,6 +1255,248 @@ var init_sources = __esm({
1025
1255
  }
1026
1256
  });
1027
1257
 
1258
+ // src/compat/members.ts
1259
+ async function assembleMembers(db, names) {
1260
+ const tracked = new Map((await getCompatMetadata(db, names)).map((r) => [r.name, r]));
1261
+ const members = [];
1262
+ const unverified = [];
1263
+ await Promise.all(
1264
+ names.map(async (name) => {
1265
+ const row = tracked.get(name);
1266
+ if (row) {
1267
+ members.push({
1268
+ name,
1269
+ version: row.latestVersion,
1270
+ peerDependencies: row.peerDependencies,
1271
+ peerDependenciesMeta: row.peerDependenciesMeta,
1272
+ engines: row.engines
1273
+ });
1274
+ return;
1275
+ }
1276
+ const reg = await fetchNpmRegistry(name).catch(() => null);
1277
+ if (reg) {
1278
+ members.push({
1279
+ name,
1280
+ version: reg.latestVersion,
1281
+ peerDependencies: reg.peerDependencies,
1282
+ peerDependenciesMeta: reg.peerDependenciesMeta,
1283
+ engines: reg.engines
1284
+ });
1285
+ } else {
1286
+ unverified.push(name);
1287
+ }
1288
+ })
1289
+ );
1290
+ return { members, unverified };
1291
+ }
1292
+ var init_members = __esm({
1293
+ "src/compat/members.ts"() {
1294
+ "use strict";
1295
+ init_esm_shims();
1296
+ init_compat();
1297
+ init_sources();
1298
+ }
1299
+ });
1300
+
1301
+ // src/compat/peerCompat.ts
1302
+ import semver from "semver";
1303
+ function satisfiesRange(version, range) {
1304
+ if (!semver.validRange(range)) return null;
1305
+ const v = semver.valid(version) ? version : semver.coerce(version)?.version;
1306
+ if (!v) return null;
1307
+ return semver.satisfies(v, range, { includePrerelease: true });
1308
+ }
1309
+ function rangesIntersect(a, b) {
1310
+ if (!semver.validRange(a) || !semver.validRange(b)) return null;
1311
+ try {
1312
+ return semver.intersects(a, b, { includePrerelease: true });
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ }
1317
+ function resolveArchitectureCompat(members) {
1318
+ const conflicts = [];
1319
+ const pinned = /* @__PURE__ */ new Map();
1320
+ for (const m of members) if (m.version) pinned.set(m.name, m.version);
1321
+ const constraints = [];
1322
+ for (const m of members) {
1323
+ if (!m.peerDependencies) continue;
1324
+ for (const [peer, range] of Object.entries(m.peerDependencies)) {
1325
+ constraints.push({
1326
+ requirer: m.name,
1327
+ peer,
1328
+ range,
1329
+ optional: Boolean(m.peerDependenciesMeta?.[peer]?.optional)
1330
+ });
1331
+ }
1332
+ }
1333
+ for (const c of constraints) {
1334
+ const pv = pinned.get(c.peer);
1335
+ if (pv && satisfiesRange(pv, c.range) === false) {
1336
+ conflicts.push({
1337
+ source: "peer-deps",
1338
+ packages: [c.requirer, c.peer],
1339
+ detail: `${c.requirer} needs peer ${c.peer}@${c.range}, but the stack uses ${c.peer}@${pv}`
1340
+ });
1341
+ }
1342
+ }
1343
+ const byPeer = /* @__PURE__ */ new Map();
1344
+ for (const c of constraints) {
1345
+ if (pinned.has(c.peer) || c.optional) continue;
1346
+ const arr = byPeer.get(c.peer);
1347
+ if (arr) arr.push(c);
1348
+ else byPeer.set(c.peer, [c]);
1349
+ }
1350
+ for (const [peer, cs] of byPeer) {
1351
+ for (let i = 0; i < cs.length; i++) {
1352
+ for (let j = i + 1; j < cs.length; j++) {
1353
+ if (cs[i].range !== cs[j].range && rangesIntersect(cs[i].range, cs[j].range) === false) {
1354
+ conflicts.push({
1355
+ source: "peer-deps",
1356
+ packages: [cs[i].requirer, cs[j].requirer],
1357
+ detail: `${cs[i].requirer} needs ${peer}@${cs[i].range} but ${cs[j].requirer} needs ${peer}@${cs[j].range} \u2014 no overlapping version`
1358
+ });
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+ const nodeReqs = members.map((m) => ({ name: m.name, range: m.engines?.node })).filter((r) => typeof r.range === "string");
1364
+ for (let i = 0; i < nodeReqs.length; i++) {
1365
+ for (let j = i + 1; j < nodeReqs.length; j++) {
1366
+ if (rangesIntersect(nodeReqs[i].range, nodeReqs[j].range) === false) {
1367
+ conflicts.push({
1368
+ source: "engines",
1369
+ packages: [nodeReqs[i].name, nodeReqs[j].name],
1370
+ detail: `${nodeReqs[i].name} needs node ${nodeReqs[i].range} but ${nodeReqs[j].name} needs node ${nodeReqs[j].range} \u2014 no overlap`
1371
+ });
1372
+ }
1373
+ }
1374
+ }
1375
+ return conflicts;
1376
+ }
1377
+ var init_peerCompat = __esm({
1378
+ "src/compat/peerCompat.ts"() {
1379
+ "use strict";
1380
+ init_esm_shims();
1381
+ }
1382
+ });
1383
+
1384
+ // src/compat/check.ts
1385
+ async function checkCompat(db, packages2) {
1386
+ const names = [...new Set(packages2)];
1387
+ const { members, unverified } = await assembleMembers(db, names);
1388
+ const conflicts = resolveArchitectureCompat(members);
1389
+ for (const edge of await getCompatEdges(db, names)) {
1390
+ if (edge.status === "conflict") {
1391
+ conflicts.push({
1392
+ source: "sandbox",
1393
+ packages: [edge.packageA, edge.packageB],
1394
+ detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
1395
+ });
1396
+ }
1397
+ }
1398
+ const overall = conflicts.length ? "conflict" : unverified.length ? "unknown" : "compatible";
1399
+ return {
1400
+ packages: names,
1401
+ overall,
1402
+ conflicts,
1403
+ unverified,
1404
+ checked: members.map((m) => ({ name: m.name, version: m.version }))
1405
+ };
1406
+ }
1407
+ var init_check = __esm({
1408
+ "src/compat/check.ts"() {
1409
+ "use strict";
1410
+ init_esm_shims();
1411
+ init_compat();
1412
+ init_members();
1413
+ init_peerCompat();
1414
+ }
1415
+ });
1416
+
1417
+ // src/db/packages.ts
1418
+ import { desc, eq, isNotNull } from "drizzle-orm";
1419
+ async function getSeedTargets(db) {
1420
+ const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
1421
+ return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
1422
+ }
1423
+ async function getPackageByName(db, name) {
1424
+ const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
1425
+ return rows[0] ?? null;
1426
+ }
1427
+ async function getAllPackageNames(db) {
1428
+ const rows = await db.select({ name: packages.name }).from(packages);
1429
+ return rows.map((r) => r.name);
1430
+ }
1431
+ async function upsertPackageVersions(db, name, versions) {
1432
+ if (versions.length === 0) return;
1433
+ for (let i = 0; i < versions.length; i += VERSION_CHUNK) {
1434
+ const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
1435
+ packageName: name,
1436
+ version: v.version,
1437
+ publishedAt: v.publishedAt
1438
+ }));
1439
+ await db.insert(packageVersions).values(rows).onConflictDoNothing();
1440
+ }
1441
+ }
1442
+ async function getPackageVersions(db, name, limit = 50) {
1443
+ const rows = await db.select({ version: packageVersions.version, publishedAt: packageVersions.publishedAt }).from(packageVersions).where(eq(packageVersions.packageName, name)).orderBy(desc(packageVersions.publishedAt)).limit(limit);
1444
+ return rows.map((r) => ({ version: r.version, publishedAt: r.publishedAt }));
1445
+ }
1446
+ async function getTopPackageNames(db, limit = 1e3) {
1447
+ const rows = await db.select({ name: packages.name }).from(packages).where(isNotNull(packages.weeklyDownloads)).orderBy(desc(packages.weeklyDownloads)).limit(limit);
1448
+ return rows.map((r) => r.name);
1449
+ }
1450
+ async function ensureSeedEntry(db, name, category) {
1451
+ await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
1452
+ }
1453
+ async function upsertPackage(db, row) {
1454
+ const { name: _name, createdAt: _createdAt, ...mutable } = row;
1455
+ await db.insert(packages).values(row).onConflictDoUpdate({
1456
+ target: packages.name,
1457
+ set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
1458
+ });
1459
+ }
1460
+ async function startSyncRun(db) {
1461
+ const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
1462
+ return row.id;
1463
+ }
1464
+ async function finishSyncRun(db, id, data) {
1465
+ await db.update(syncRuns).set({
1466
+ finishedAt: /* @__PURE__ */ new Date(),
1467
+ packagesSeen: data.packagesSeen,
1468
+ packagesUpdated: data.packagesUpdated,
1469
+ errors: data.errors,
1470
+ status: data.status
1471
+ }).where(eq(syncRuns.id, id));
1472
+ }
1473
+ var VERSION_CHUNK;
1474
+ var init_packages = __esm({
1475
+ "src/db/packages.ts"() {
1476
+ "use strict";
1477
+ init_esm_shims();
1478
+ init_schema();
1479
+ VERSION_CHUNK = 500;
1480
+ }
1481
+ });
1482
+
1483
+ // src/db/verification.ts
1484
+ import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
1485
+ async function storeVerificationRun(db, run) {
1486
+ await db.insert(verificationRuns).values(run);
1487
+ }
1488
+ async function getLatestVerificationByName(db, packageName) {
1489
+ const rows = await db.select().from(verificationRuns).where(eq2(verificationRuns.packageName, packageName)).orderBy(desc2(verificationRuns.ranAt)).limit(1);
1490
+ return rows[0] ?? null;
1491
+ }
1492
+ var init_verification = __esm({
1493
+ "src/db/verification.ts"() {
1494
+ "use strict";
1495
+ init_esm_shims();
1496
+ init_schema();
1497
+ }
1498
+ });
1499
+
1028
1500
  // src/ingestion/sources/githubReadme.ts
1029
1501
  async function fetchGithubReadme(owner, repo, fetchImpl) {
1030
1502
  for (const file of CANDIDATES) {
@@ -1422,10 +1894,10 @@ function settableKeys() {
1422
1894
  function applyOverrides(base, sets) {
1423
1895
  const next = structuredClone(base);
1424
1896
  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());
1897
+ const eq9 = entry.indexOf("=");
1898
+ if (eq9 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
1899
+ const key = entry.slice(0, eq9).trim();
1900
+ const value = Number(entry.slice(eq9 + 1).trim());
1429
1901
  const apply = SETTABLE[key];
1430
1902
  if (!apply) {
1431
1903
  throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
@@ -1589,8 +2061,6 @@ function toScoringInput(signals, category) {
1589
2061
  weeklyDownloads: downloads?.weeklyDownloads ?? null,
1590
2062
  downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
1591
2063
  stars: github?.stars ?? null,
1592
- dependentsCount: null,
1593
- // not collected in v1 (see spec R1 note)
1594
2064
  firstPublishedAt: registry?.firstPublishedAt ?? null,
1595
2065
  lastReleaseAt: github?.lastReleaseAt ?? registry?.lastReleaseAt ?? null,
1596
2066
  releasesLast12mo: github?.releasesLast12mo ?? null,
@@ -1746,7 +2216,8 @@ function computeConfidence(input, now, qualityScore = null) {
1746
2216
  if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
1747
2217
  return "emerging";
1748
2218
  }
1749
- const promisingReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
2219
+ const promisingRecencyMonths = lastReleaseMonths ?? ageMonths;
2220
+ const promisingReleaseOk = promisingRecencyMonths !== null && promisingRecencyMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
1750
2221
  if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
1751
2222
  return "promising";
1752
2223
  }
@@ -1859,6 +2330,7 @@ var init_embeddings = __esm({
1859
2330
  LocalEmbeddingProvider = class {
1860
2331
  kind = "local";
1861
2332
  dimensions = EMBEDDING_DIM;
2333
+ id = "local";
1862
2334
  async embed(texts) {
1863
2335
  return texts.map((t) => localEmbed(t));
1864
2336
  }
@@ -1869,17 +2341,19 @@ var init_embeddings = __esm({
1869
2341
  this.apiKey = apiKey;
1870
2342
  this.model = model;
1871
2343
  this.fetchImpl = fetchImpl;
2344
+ this.id = `openai:${model}`;
1872
2345
  }
1873
2346
  apiKey;
1874
2347
  model;
1875
2348
  fetchImpl;
1876
2349
  kind = "openai";
1877
2350
  dimensions = EMBEDDING_DIM;
2351
+ id;
1878
2352
  async embed(texts) {
1879
2353
  const out = [];
1880
2354
  for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
1881
2355
  const batch = texts.slice(i, i + OPENAI_BATCH);
1882
- const body = JSON.stringify({ model: this.model, input: batch });
2356
+ const body = JSON.stringify({ model: this.model, input: batch, dimensions: EMBEDDING_DIM });
1883
2357
  const { data } = await httpRequest("https://api.openai.com/v1/embeddings", {
1884
2358
  host: "api.openai.com",
1885
2359
  method: "POST",
@@ -1892,7 +2366,14 @@ var init_embeddings = __esm({
1892
2366
  });
1893
2367
  const vectors = data?.data ?? [];
1894
2368
  vectors.sort((a, b) => a.index - b.index);
1895
- for (const v of vectors) out.push(v.embedding);
2369
+ for (const v of vectors) {
2370
+ if (v.embedding.length !== EMBEDDING_DIM) {
2371
+ throw new Error(
2372
+ `OpenAI model ${this.model} returned ${v.embedding.length}-dim vectors; expected ${EMBEDDING_DIM}. Set EMBEDDING_MODEL to a text-embedding-3 model or update EMBEDDING_DIM.`
2373
+ );
2374
+ }
2375
+ out.push(v.embedding);
2376
+ }
1896
2377
  }
1897
2378
  return out;
1898
2379
  }
@@ -1900,47 +2381,6 @@ var init_embeddings = __esm({
1900
2381
  }
1901
2382
  });
1902
2383
 
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
2384
  // src/core/concurrency.ts
1945
2385
  async function pMap(items, mapper, concurrency) {
1946
2386
  const results = new Array(items.length);
@@ -2068,6 +2508,7 @@ async function runSync(opts = {}) {
2068
2508
  healthScore,
2069
2509
  qualityScore: c.quality,
2070
2510
  embedding: embeddings[i] ?? null,
2511
+ embeddingProvider: embProvider.id,
2071
2512
  now
2072
2513
  })
2073
2514
  );
@@ -2081,6 +2522,7 @@ async function runSync(opts = {}) {
2081
2522
  status
2082
2523
  });
2083
2524
  logger.info(`Sync ${status}: ${updated}/${targets.length} updated, ${allErrors.length} source errors.`);
2525
+ if (updated > 0) await invalidateCache();
2084
2526
  return { seen: targets.length, updated, errors: allErrors.length, status };
2085
2527
  } catch (err) {
2086
2528
  await finishSyncRun(handle.db, runId, {
@@ -2140,19 +2582,22 @@ function assemblePackageRow(p) {
2140
2582
  lastReleaseAt: p.input.lastReleaseAt,
2141
2583
  weeklyDownloads: p.input.weeklyDownloads,
2142
2584
  downloadGrowth90d: p.input.downloadGrowth90d,
2143
- dependentsCount: p.input.dependentsCount,
2144
2585
  stars: p.input.stars,
2145
2586
  openIssues: p.input.openIssues,
2146
2587
  closedIssues: p.input.closedIssues,
2147
2588
  scorecard: p.input.scorecard,
2148
2589
  bundleMinGzipKb: p.input.bundleMinGzipKb,
2149
2590
  advisories: p.input.advisories,
2591
+ peerDependencies: r?.peerDependencies ?? null,
2592
+ peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
2593
+ engines: r?.engines ?? null,
2150
2594
  healthScore: p.healthScore,
2151
2595
  qualityScore: p.qualityScore,
2152
2596
  confidence: p.confidence,
2153
2597
  scoreBreakdown: p.breakdown,
2154
2598
  usageGuide: p.usageGuide,
2155
2599
  embedding: p.embedding,
2600
+ embeddingProvider: p.embedding ? p.embeddingProvider : null,
2156
2601
  dataAsOf: p.now
2157
2602
  };
2158
2603
  }
@@ -2160,6 +2605,7 @@ var init_sync = __esm({
2160
2605
  "src/pipeline/sync.ts"() {
2161
2606
  "use strict";
2162
2607
  init_esm_shims();
2608
+ init_cache();
2163
2609
  init_config();
2164
2610
  init_concurrency();
2165
2611
  init_http();
@@ -2177,15 +2623,15 @@ var init_sync = __esm({
2177
2623
  });
2178
2624
 
2179
2625
  // src/pipeline/single.ts
2180
- import { and, eq as eq2, isNotNull, sql as sql3 } from "drizzle-orm";
2626
+ import { and as and3, eq as eq3, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
2181
2627
  async function getSeedCategory(db, name) {
2182
- const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq2(seedPackages.name, name)).limit(1);
2628
+ const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq3(seedPackages.name, name)).limit(1);
2183
2629
  return row?.category ?? null;
2184
2630
  }
2185
2631
  async function getCategoryMedianBundle(db, category) {
2186
2632
  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)));
2633
+ m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
2634
+ }).from(packages).where(and3(eq3(packages.category, category), isNotNull2(packages.bundleMinGzipKb)));
2189
2635
  return row?.m ?? null;
2190
2636
  }
2191
2637
  async function syncOnePackage(db, name, opts = {}) {
@@ -2225,7 +2671,8 @@ async function syncOnePackage(db, name, opts = {}) {
2225
2671
  };
2226
2672
  const healthScore = computeHealthScore(breakdown);
2227
2673
  const confidence = computeConfidence(input, now, quality);
2228
- const [embedding] = await createEmbeddingProvider().embed([
2674
+ const embProvider = createEmbeddingProvider();
2675
+ const [embedding] = await embProvider.embed([
2229
2676
  buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
2230
2677
  ]);
2231
2678
  await upsertPackage(
@@ -2243,9 +2690,14 @@ async function syncOnePackage(db, name, opts = {}) {
2243
2690
  healthScore,
2244
2691
  qualityScore: quality,
2245
2692
  embedding: embedding ?? null,
2693
+ embeddingProvider: embProvider.id,
2246
2694
  now
2247
2695
  })
2248
2696
  );
2697
+ await upsertPackageVersions(db, name, signals.registry?.versionTimeline ?? []).catch(
2698
+ () => {
2699
+ }
2700
+ );
2249
2701
  return await getPackageByName(db, name);
2250
2702
  }
2251
2703
  async function getOrFetchPackage(db, name) {
@@ -2254,7 +2706,9 @@ async function getOrFetchPackage(db, name) {
2254
2706
  const exists = await npmPackageExists(name);
2255
2707
  if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
2256
2708
  const row = await syncOnePackage(db, name);
2257
- await ensureSeedEntry(db, name, row.category);
2709
+ if (row.confidence && row.confidence !== "unproven") {
2710
+ await ensureSeedEntry(db, name, row.category);
2711
+ }
2258
2712
  return { row, wasTracked: false, existsOnNpm: true };
2259
2713
  }
2260
2714
  var init_single = __esm({
@@ -2275,16 +2729,16 @@ var init_single = __esm({
2275
2729
  });
2276
2730
 
2277
2731
  // src/search/recommend.ts
2278
- import { and as and2, cosineDistance, eq as eq3, isNotNull as isNotNull2, lte, sql as sql4 } from "drizzle-orm";
2732
+ import { and as and4, cosineDistance, eq as eq4, isNotNull as isNotNull3, lte, sql as sql3 } from "drizzle-orm";
2279
2733
  async function recommend(db, opts, provider = createEmbeddingProvider()) {
2280
2734
  const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
2281
2735
  const [queryVec] = await provider.embed([opts.need]);
2282
2736
  if (!queryVec) return [];
2283
2737
  const category = opts.category ?? inferCategory(opts.need);
2284
2738
  const pool = Math.max(limit * 5, 25);
2285
- let fused = await hybridSearch(db, queryVec, opts.need, opts.constraints, category, pool);
2739
+ let fused = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, category, pool);
2286
2740
  if (category && fused.length < limit) {
2287
- const broad = await hybridSearch(db, queryVec, opts.need, opts.constraints, null, pool);
2741
+ const broad = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, null, pool);
2288
2742
  const seen = new Set(fused.map((f) => f.row.name));
2289
2743
  fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
2290
2744
  }
@@ -2298,9 +2752,9 @@ async function recommend(db, opts, provider = createEmbeddingProvider()) {
2298
2752
  }).sort((a, b) => b.score - a.score).slice(0, limit);
2299
2753
  return ranked.map(({ row }) => toCandidate(row));
2300
2754
  }
2301
- async function hybridSearch(db, queryVec, need, constraints, category, pool) {
2755
+ async function hybridSearch(db, queryVec, providerId, need, constraints, category, pool) {
2302
2756
  const [vectorRows, lexicalRows] = await Promise.all([
2303
- runVectorQuery(db, queryVec, constraints, category, pool),
2757
+ runVectorQuery(db, queryVec, providerId, constraints, category, pool),
2304
2758
  runLexicalQuery(db, need, constraints, category, pool)
2305
2759
  ]);
2306
2760
  return rrfFuse([vectorRows, lexicalRows]);
@@ -2319,8 +2773,8 @@ function rrfFuse(lists, k = RRF_K) {
2319
2773
  }
2320
2774
  function buildConditions(constraints, category) {
2321
2775
  const conditions = [];
2322
- if (category) conditions.push(eq3(packages.category, category));
2323
- if (constraints?.license) conditions.push(eq3(packages.license, constraints.license));
2776
+ if (category) conditions.push(eq4(packages.category, category));
2777
+ if (constraints?.license) conditions.push(eq4(packages.license, constraints.license));
2324
2778
  if (constraints?.maxBundleKb !== void 0) {
2325
2779
  conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
2326
2780
  }
@@ -2329,21 +2783,25 @@ function buildConditions(constraints, category) {
2329
2783
  (c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
2330
2784
  );
2331
2785
  conditions.push(
2332
- sql4`${packages.confidence} in ${sql4.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
2786
+ sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
2333
2787
  );
2334
2788
  }
2335
2789
  return conditions;
2336
2790
  }
2337
- async function runVectorQuery(db, queryVec, constraints, category, pool) {
2791
+ async function runVectorQuery(db, queryVec, providerId, constraints, category, pool) {
2338
2792
  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);
2793
+ const conditions = [
2794
+ isNotNull3(packages.embedding),
2795
+ eq4(packages.embeddingProvider, providerId),
2796
+ ...buildConditions(constraints, category)
2797
+ ];
2798
+ return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(distance).limit(pool);
2341
2799
  }
2342
2800
  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);
2801
+ const tsquery = sql3`websearch_to_tsquery('english', ${need})`;
2802
+ const rank = sql3`ts_rank(${packages.searchVector}, ${tsquery})`;
2803
+ const conditions = [sql3`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
2804
+ return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(sql3`${rank} desc`).limit(pool);
2347
2805
  }
2348
2806
  function toCandidate(row) {
2349
2807
  return {
@@ -2404,12 +2862,99 @@ var init_recommend = __esm({
2404
2862
  }
2405
2863
  });
2406
2864
 
2865
+ // src/security/risk.ts
2866
+ function assessRisk(i) {
2867
+ const malwarePattern = i.installScripts && i.brandNew && i.lowTrust;
2868
+ if (i.typosquat || i.hasCriticalOrHighAdvisory || malwarePattern) return "high";
2869
+ if (i.deprecatedOrArchived || i.installScripts && (i.lowTrust || i.brandNew) || i.lowTrust && i.flags.includes("single-maintainer")) {
2870
+ return "medium";
2871
+ }
2872
+ return "low";
2873
+ }
2874
+ var init_risk = __esm({
2875
+ "src/security/risk.ts"() {
2876
+ "use strict";
2877
+ init_esm_shims();
2878
+ }
2879
+ });
2880
+
2881
+ // src/security/typosquat.ts
2882
+ function bareName(name) {
2883
+ const slash = name.indexOf("/");
2884
+ return (slash >= 0 ? name.slice(slash + 1) : name).toLowerCase();
2885
+ }
2886
+ function editDistance(a, b) {
2887
+ const m = a.length;
2888
+ const n = b.length;
2889
+ if (m === 0) return n;
2890
+ if (n === 0) return m;
2891
+ let prevPrev = new Array(n + 1).fill(0);
2892
+ let prev = Array.from({ length: n + 1 }, (_, j) => j);
2893
+ for (let i = 1; i <= m; i++) {
2894
+ const cur = new Array(n + 1).fill(0);
2895
+ cur[0] = i;
2896
+ for (let j = 1; j <= n; j++) {
2897
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2898
+ let val = Math.min(
2899
+ (prev[j] ?? 0) + 1,
2900
+ (cur[j - 1] ?? 0) + 1,
2901
+ (prev[j - 1] ?? 0) + cost
2902
+ );
2903
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
2904
+ val = Math.min(val, (prevPrev[j - 2] ?? 0) + 1);
2905
+ }
2906
+ cur[j] = val;
2907
+ }
2908
+ prevPrev = prev;
2909
+ prev = cur;
2910
+ }
2911
+ return prev[n] ?? 0;
2912
+ }
2913
+ function detectTyposquat(name, popular, maxDistance = 2) {
2914
+ const target = bareName(name);
2915
+ if (target.length < 4) return null;
2916
+ if (popular.some((p) => p.toLowerCase() === name.toLowerCase())) return null;
2917
+ let best = null;
2918
+ for (const p of popular) {
2919
+ const cand = bareName(p);
2920
+ if (cand === target) continue;
2921
+ if (Math.abs(cand.length - target.length) > maxDistance) continue;
2922
+ const dist = editDistance(target, cand);
2923
+ if (dist >= 1 && dist <= maxDistance && (!best || dist < best.distance)) {
2924
+ best = { target: p, distance: dist };
2925
+ if (dist === 1) break;
2926
+ }
2927
+ }
2928
+ return best;
2929
+ }
2930
+ var init_typosquat = __esm({
2931
+ "src/security/typosquat.ts"() {
2932
+ "use strict";
2933
+ init_esm_shims();
2934
+ }
2935
+ });
2936
+
2407
2937
  // src/mcp/handlers.ts
2408
- import { sql as sql5 } from "drizzle-orm";
2938
+ var handlers_exports = {};
2939
+ __export(handlers_exports, {
2940
+ handleCompare: () => handleCompare,
2941
+ handleCompat: () => handleCompat,
2942
+ handleEvaluate: () => handleEvaluate,
2943
+ handleRecommend: () => handleRecommend,
2944
+ handleVerify: () => handleVerify,
2945
+ latestDataAsOf: () => latestDataAsOf,
2946
+ rowToEvaluate: () => rowToEvaluate
2947
+ });
2948
+ import { createHash as createHash3 } from "crypto";
2949
+ import { sql as sql4 } from "drizzle-orm";
2409
2950
  function isStale(dataAsOf) {
2410
2951
  if (!dataAsOf) return true;
2411
2952
  return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
2412
2953
  }
2954
+ function refreshStale(out) {
2955
+ out.stale = isStale(out.dataAsOf ? new Date(out.dataAsOf) : null) || void 0;
2956
+ return out;
2957
+ }
2413
2958
  function withinDays(date, days) {
2414
2959
  return date ? Date.now() - date.getTime() <= days * DAY_MS2 : false;
2415
2960
  }
@@ -2438,7 +2983,6 @@ function rowToEvaluate(row) {
2438
2983
  lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
2439
2984
  weeklyDownloads: row.weeklyDownloads,
2440
2985
  downloadGrowth90d: row.downloadGrowth90d,
2441
- dependentsCount: row.dependentsCount,
2442
2986
  scorecard: row.scorecard,
2443
2987
  bundleMinGzipKb: row.bundleMinGzipKb,
2444
2988
  deprecated: row.deprecated,
@@ -2450,40 +2994,83 @@ function rowToEvaluate(row) {
2450
2994
  };
2451
2995
  }
2452
2996
  async function latestDataAsOf(db) {
2453
- const [row] = await db.select({ m: sql5`max(${packages.dataAsOf})` }).from(packages);
2997
+ const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
2454
2998
  return new Date(row?.m ?? Date.now()).toISOString();
2455
2999
  }
3000
+ function cacheKey(parts) {
3001
+ return createHash3("sha1").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
3002
+ }
2456
3003
  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 };
3004
+ return cached2(
3005
+ "rec",
3006
+ cacheKey([input.need, input.category ?? null, input.constraints ?? null]),
3007
+ async () => {
3008
+ const candidates = await recommend(db, {
3009
+ need: input.need,
3010
+ category: input.category,
3011
+ constraints: input.constraints,
3012
+ limit: 5
3013
+ });
3014
+ return { dataAsOf: await latestDataAsOf(db), candidates };
3015
+ },
3016
+ // Don't cache empty results — the index may still be populating.
3017
+ { skipCache: (r) => r.candidates.length === 0 }
3018
+ );
2464
3019
  }
2465
3020
  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);
3021
+ const out = await cached2(
3022
+ "eval",
3023
+ cacheKey([input.package]),
3024
+ async () => {
3025
+ const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
3026
+ if (!row) {
3027
+ return {
3028
+ tracked: false,
3029
+ suggestion: existsOnNpm ? `"${input.package}" exists on npm but could not be scored right now; try again.` : `"${input.package}" was not found on the npm registry. Check the package name.`
3030
+ };
3031
+ }
3032
+ const evaluated = rowToEvaluate(row);
3033
+ const verification = await getLatestVerificationByName(db, row.name);
3034
+ return verification ? { ...evaluated, buildVerified: toBuildVerified(verification) } : evaluated;
3035
+ },
3036
+ // Don't cache "not found / not scored yet" — it may resolve on a later fetch.
3037
+ { skipCache: (r) => "tracked" in r }
3038
+ );
3039
+ return "tracked" in out ? out : refreshStale(out);
2474
3040
  }
2475
3041
  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)
3042
+ const out = await cached2(
3043
+ "cmp",
3044
+ cacheKey(input.packages),
3045
+ async () => {
3046
+ const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
3047
+ const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
3048
+ const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
3049
+ return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
3050
+ },
3051
+ // Don't cache a transient miss (a package that momentarily failed to fetch).
3052
+ { skipCache: (r) => Boolean(r.missing?.length) }
2480
3053
  );
2481
- return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
3054
+ out.rows = out.rows.map(refreshStale);
3055
+ return out;
3056
+ }
3057
+ function toBuildVerified(v) {
3058
+ return {
3059
+ version: v.version,
3060
+ installed: v.installed,
3061
+ loaded: v.imported,
3062
+ driver: v.driver,
3063
+ ranAt: v.ranAt ? v.ranAt.toISOString() : ""
3064
+ };
3065
+ }
3066
+ async function handleCompat(db, input) {
3067
+ return checkCompat(db, input.packages);
2482
3068
  }
2483
3069
  async function handleVerify(db, input) {
2484
3070
  const name = input.package;
2485
3071
  const exists = await npmPackageExists(name);
2486
3072
  if (!exists) {
3073
+ const typo2 = detectTyposquat(name, await getTopPackageNames(db).catch(() => []));
2487
3074
  return {
2488
3075
  exists: false,
2489
3076
  tracked: false,
@@ -2491,25 +3078,46 @@ async function handleVerify(db, input) {
2491
3078
  archived: false,
2492
3079
  latestVersion: null,
2493
3080
  weeklyDownloads: null,
2494
- riskFlags: ["not-found-on-registry"],
3081
+ riskFlags: typo2 ? ["not-found-on-registry", `possible-typosquat-of:${typo2.target}`] : ["not-found-on-registry"],
3082
+ risk: "high",
3083
+ typosquatOf: typo2?.target ?? null,
2495
3084
  confidence: null,
2496
3085
  advisoryCount: 0
2497
3086
  };
2498
3087
  }
2499
- const registry = await fetchNpmRegistry(name).catch(() => null);
2500
- const { row, wasTracked } = await getOrFetchPackage(db, name);
3088
+ const [registry, { row, wasTracked }, popular] = await Promise.all([
3089
+ fetchNpmRegistry(name).catch(() => null),
3090
+ getOrFetchPackage(db, name),
3091
+ getTopPackageNames(db).catch(() => [])
3092
+ ]);
2501
3093
  const weeklyDownloads = row?.weeklyDownloads ?? null;
2502
- const advisoryCount = row?.advisories?.length ?? 0;
3094
+ const advisories = row?.advisories ?? [];
3095
+ const advisoryCount = advisories.length;
2503
3096
  const deprecated = Boolean(row?.deprecated || registry?.deprecated);
2504
3097
  const archived = Boolean(row?.archived);
3098
+ const brandNew = withinDays(registry?.firstPublishedAt ?? null, 7);
3099
+ const lowTrust = weeklyDownloads === null || weeklyDownloads < 1e3;
3100
+ const installScripts = registry?.hasInstallScripts ?? false;
3101
+ const typo = detectTyposquat(name, popular);
2505
3102
  const riskFlags = [];
3103
+ if (typo) riskFlags.push(`possible-typosquat-of:${typo.target}`);
2506
3104
  if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
2507
3105
  else if (weeklyDownloads < 1e3) riskFlags.push("low-downloads");
2508
- if (withinDays(registry?.firstPublishedAt ?? null, 7)) riskFlags.push("published-within-7-days");
3106
+ if (brandNew) riskFlags.push("published-within-7-days");
2509
3107
  if (registry?.maintainersCount === 1) riskFlags.push("single-maintainer");
3108
+ if (installScripts) riskFlags.push("runs-install-scripts");
2510
3109
  if (advisoryCount > 0) riskFlags.push("has-known-advisory");
2511
3110
  if (deprecated) riskFlags.push("deprecated");
2512
3111
  if (archived) riskFlags.push("archived");
3112
+ const risk = assessRisk({
3113
+ flags: riskFlags,
3114
+ hasCriticalOrHighAdvisory: hasCriticalOrHighAdvisory(advisories),
3115
+ typosquat: Boolean(typo),
3116
+ installScripts,
3117
+ brandNew,
3118
+ lowTrust,
3119
+ deprecatedOrArchived: deprecated || archived
3120
+ });
2513
3121
  return {
2514
3122
  exists: true,
2515
3123
  tracked: wasTracked,
@@ -2518,6 +3126,8 @@ async function handleVerify(db, input) {
2518
3126
  latestVersion: registry?.latestVersion ?? row?.latestVersion ?? null,
2519
3127
  weeklyDownloads,
2520
3128
  riskFlags,
3129
+ risk,
3130
+ typosquatOf: typo?.target ?? null,
2521
3131
  confidence: row?.confidence ?? null,
2522
3132
  advisoryCount
2523
3133
  };
@@ -2527,12 +3137,19 @@ var init_handlers = __esm({
2527
3137
  "src/mcp/handlers.ts"() {
2528
3138
  "use strict";
2529
3139
  init_esm_shims();
3140
+ init_cache();
2530
3141
  init_constants();
3142
+ init_check();
3143
+ init_packages();
3144
+ init_verification();
2531
3145
  init_schema();
2532
3146
  init_sources();
2533
3147
  init_summarize();
2534
3148
  init_single();
3149
+ init_score();
2535
3150
  init_recommend();
3151
+ init_risk();
3152
+ init_typosquat();
2536
3153
  SEVERITY_RANK = {
2537
3154
  critical: 4,
2538
3155
  high: 3,
@@ -2545,7 +3162,7 @@ var init_handlers = __esm({
2545
3162
  });
2546
3163
 
2547
3164
  // src/mcp/diagram.ts
2548
- import { inArray } from "drizzle-orm";
3165
+ import { inArray as inArray2 } from "drizzle-orm";
2549
3166
  function layerFor(item) {
2550
3167
  if (!item.category) return UNCLASSIFIED;
2551
3168
  if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
@@ -2598,7 +3215,7 @@ async function handleDiagram(db, input) {
2598
3215
  note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
2599
3216
  };
2600
3217
  }
2601
- const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray(packages.name, input.stack));
3218
+ const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray2(packages.name, input.stack));
2602
3219
  const known = new Map(rows.map((r) => [r.name, r.category]));
2603
3220
  const items = input.stack.map((name) => ({
2604
3221
  label: name,
@@ -2652,13 +3269,410 @@ var init_diagram = __esm({
2652
3269
  }
2653
3270
  });
2654
3271
 
2655
- // src/mcp/server.ts
2656
- var server_exports = {};
2657
- __export(server_exports, {
2658
- buildMcpServer: () => buildMcpServer,
2659
- startMcpServer: () => startMcpServer
2660
- });
2661
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3272
+ // src/compat/optimize.ts
3273
+ function conflictsFor(members, sandboxConflicts) {
3274
+ const out = resolveArchitectureCompat(members);
3275
+ for (let i = 0; i < members.length; i++) {
3276
+ for (let j = i + 1; j < members.length; j++) {
3277
+ const a = members[i].name;
3278
+ const b = members[j].name;
3279
+ const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
3280
+ if (sandboxConflicts.has(key)) {
3281
+ out.push({
3282
+ source: "sandbox",
3283
+ packages: [a, b],
3284
+ detail: `${a} and ${b} are recorded as incompatible (sandbox)`
3285
+ });
3286
+ }
3287
+ }
3288
+ }
3289
+ return out;
3290
+ }
3291
+ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
3292
+ if (slots.some((s) => s.length === 0)) {
3293
+ throw new Error("optimizeStack: every slot must have at least one candidate");
3294
+ }
3295
+ const n = slots.length;
3296
+ const budget = 5e4;
3297
+ let nodes = 0;
3298
+ let bestSelection = new Array(n).fill(0);
3299
+ let bestRegret = conflictsFor(
3300
+ slots.map((c) => c[0]).filter((m) => Boolean(m)),
3301
+ sandboxConflicts
3302
+ ).length === 0 ? 0 : Infinity;
3303
+ const chosen = new Array(n).fill(0);
3304
+ const dfs = (slot, regret) => {
3305
+ if (nodes++ > budget || regret >= bestRegret) return;
3306
+ if (slot === n) {
3307
+ bestSelection = chosen.slice();
3308
+ bestRegret = regret;
3309
+ return;
3310
+ }
3311
+ const candidates = slots[slot];
3312
+ for (let i = 0; i < candidates.length; i++) {
3313
+ chosen[slot] = i;
3314
+ const assigned = chosen.slice(0, slot + 1).map((idx, s) => slots[s][idx]);
3315
+ if (conflictsFor(assigned, sandboxConflicts).length === 0) {
3316
+ dfs(slot + 1, regret + i);
3317
+ }
3318
+ if (nodes > budget) return;
3319
+ }
3320
+ };
3321
+ dfs(0, 0);
3322
+ const members = bestSelection.map((idx, s) => slots[s][idx]);
3323
+ return {
3324
+ selection: bestSelection,
3325
+ conflicts: conflictsFor(members, sandboxConflicts),
3326
+ regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret
3327
+ };
3328
+ }
3329
+ var init_optimize = __esm({
3330
+ "src/compat/optimize.ts"() {
3331
+ "use strict";
3332
+ init_esm_shims();
3333
+ init_peerCompat();
3334
+ }
3335
+ });
3336
+
3337
+ // src/mcp/plan.ts
3338
+ var plan_exports = {};
3339
+ __export(plan_exports, {
3340
+ decomposeHeuristic: () => decomposeHeuristic,
3341
+ familyOf: () => familyOf,
3342
+ flagSlotConflicts: () => flagSlotConflicts,
3343
+ handlePlan: () => handlePlan,
3344
+ orderCandidates: () => orderCandidates,
3345
+ resolvePins: () => resolvePins
3346
+ });
3347
+ import { createHash as createHash4 } from "crypto";
3348
+ import { inArray as inArray3 } from "drizzle-orm";
3349
+ function packageToCandidate(row) {
3350
+ return {
3351
+ name: row.name,
3352
+ category: row.category,
3353
+ healthScore: row.healthScore ?? 0,
3354
+ qualityScore: row.qualityScore,
3355
+ confidence: row.confidence ?? "unproven",
3356
+ why: "pinned by you",
3357
+ latestVersion: row.latestVersion,
3358
+ weeklyDownloads: row.weeklyDownloads,
3359
+ lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
3360
+ repoUrl: row.repoUrl
3361
+ };
3362
+ }
3363
+ async function resolvePins(db, using, recommendedNames) {
3364
+ const slots = [];
3365
+ const unresolved = [];
3366
+ for (const name of new Set(using ?? [])) {
3367
+ if (recommendedNames.has(name)) continue;
3368
+ const { row } = await getOrFetchPackage(db, name);
3369
+ if (!row) {
3370
+ unresolved.push(name);
3371
+ continue;
3372
+ }
3373
+ slots.push({
3374
+ need: `using ${name}`,
3375
+ category: row.category,
3376
+ layer: layerFor({ label: name, category: row.category }),
3377
+ recommended: packageToCandidate(row),
3378
+ alternatives: [],
3379
+ // fixed: the user chose this, so it never gets swapped
3380
+ note: "pinned by you"
3381
+ });
3382
+ }
3383
+ return { slots, unresolved };
3384
+ }
3385
+ async function handlePlan(db, input) {
3386
+ const optimize = input.optimize ?? "balanced";
3387
+ const decomposed = input.needs?.length ? { needs: dedupeNeeds(input.needs), source: "needs" } : input.document?.trim() ? await decompose(input.document) : null;
3388
+ const hasPins = Boolean(input.using?.length);
3389
+ if ((!decomposed || decomposed.needs.length === 0) && !hasPins) {
3390
+ return {
3391
+ note: "Provide a `document` (a detailed description of your program), a `needs` array, or a `using` list of packages you have already chosen. lurq recommends evidence-scored packages per component \u2014 it does not invent an architecture from a bare prompt."
3392
+ };
3393
+ }
3394
+ const needs = (decomposed?.needs ?? []).slice(0, MAX_SLOTS);
3395
+ const source = decomposed?.source ?? "needs";
3396
+ const safeRecommend = (need, category) => recommend(db, { need, category, limit: PER_SLOT }).catch((err) => {
3397
+ logger.warn(`plan: recommend failed for "${need}": ${err.message}`);
3398
+ return [];
3399
+ });
3400
+ const effCat = (n) => n.category ?? inferCategory(n.need) ?? void 0;
3401
+ const anchorFamily = familyOf(`${input.document ?? ""} ${needs.map((n) => n.need).join(" ")}`);
3402
+ const metaIdx = needs.findIndex((n) => effCat(n) === "meta-framework");
3403
+ const anchorIdx = metaIdx >= 0 ? metaIdx : needs.findIndex((n) => effCat(n) === "framework");
3404
+ const recs = new Array(needs.length);
3405
+ let framework = anchorFamily;
3406
+ if (anchorIdx >= 0) {
3407
+ recs[anchorIdx] = await safeRecommend(needs[anchorIdx].need, effCat(needs[anchorIdx]));
3408
+ framework = recs[anchorIdx][0]?.name ?? anchorFamily;
3409
+ }
3410
+ const [, dataAsOf] = await Promise.all([
3411
+ Promise.all(
3412
+ needs.map(async (n, i) => {
3413
+ if (i === anchorIdx) return;
3414
+ const need = framework ? `${n.need} (for a ${framework} app)` : n.need;
3415
+ recs[i] = await safeRecommend(need, effCat(n));
3416
+ })
3417
+ ),
3418
+ latestDataAsOf(db)
3419
+ ]);
3420
+ const bundleByName = optimize === "speed" ? await bundleSizes(db, recs.flat()) : /* @__PURE__ */ new Map();
3421
+ const recSlots = needs.map((n, i) => {
3422
+ const candidates = orderCandidates(recs[i], anchorFamily, optimize, bundleByName);
3423
+ const recommended = candidates[0] ?? null;
3424
+ const category = n.category ?? recommended?.category ?? inferCategory(n.need);
3425
+ return {
3426
+ need: n.need,
3427
+ category,
3428
+ layer: layerFor({ label: recommended?.name ?? n.need, category }),
3429
+ recommended,
3430
+ alternatives: candidates.slice(1),
3431
+ note: recommended ? void 0 : "no tracked package matched this need yet"
3432
+ };
3433
+ });
3434
+ const recommendedNames = new Set(
3435
+ recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
3436
+ );
3437
+ const { slots: pinnedSlots, unresolved: unresolvedPins } = await resolvePins(
3438
+ db,
3439
+ input.using,
3440
+ recommendedNames
3441
+ );
3442
+ const slots = [...pinnedSlots, ...recSlots];
3443
+ const compatibility = await resolveCompat(db, slots);
3444
+ const unmatched = [
3445
+ ...slots.filter((s) => !s.recommended).map((s) => s.need),
3446
+ ...unresolvedPins.map((n) => `${n} (pinned, but not found on npm)`)
3447
+ ];
3448
+ const mermaid = buildMermaid(
3449
+ slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
3450
+ );
3451
+ return {
3452
+ dataAsOf,
3453
+ optimize,
3454
+ source,
3455
+ framework,
3456
+ slots,
3457
+ unmatched,
3458
+ mermaid,
3459
+ note: planNote(source, unmatched.length, optimize, framework),
3460
+ compatibility
3461
+ };
3462
+ }
3463
+ function flagSlotConflicts(picks, conflicts) {
3464
+ const out = /* @__PURE__ */ new Map();
3465
+ for (const c of conflicts) {
3466
+ for (const p of picks) {
3467
+ if (p.name && c.packages.includes(p.name)) {
3468
+ const others = c.packages.filter((n) => n !== p.name);
3469
+ out.set(p.need, [.../* @__PURE__ */ new Set([...out.get(p.need) ?? [], ...others])]);
3470
+ }
3471
+ }
3472
+ }
3473
+ return out;
3474
+ }
3475
+ async function resolveCompat(db, slots) {
3476
+ const eligible = slots.filter((s) => s.recommended);
3477
+ if (eligible.length < 2) return null;
3478
+ try {
3479
+ const allNames = [
3480
+ ...new Set(eligible.flatMap((s) => [s.recommended, ...s.alternatives].map((c) => c.name)))
3481
+ ];
3482
+ const [{ members }, edges] = await Promise.all([
3483
+ assembleMembers(db, allNames),
3484
+ getCompatEdges(db, allNames)
3485
+ ]);
3486
+ const metaByName = new Map(members.map((m) => [m.name, m]));
3487
+ const sandboxConflicts = new Set(
3488
+ edges.filter((e) => e.status === "conflict").map((e) => `${e.packageA}|${e.packageB}`)
3489
+ );
3490
+ const slotCandidates = eligible.map(
3491
+ (s) => [s.recommended, ...s.alternatives].map((c) => metaByName.get(c.name) ?? NO_META(c))
3492
+ );
3493
+ const { selection } = optimizeStack(slotCandidates, sandboxConflicts);
3494
+ eligible.forEach((s, i) => {
3495
+ const idx = selection[i] ?? 0;
3496
+ if (idx <= 0) return;
3497
+ const all = [s.recommended, ...s.alternatives];
3498
+ const chosen = all[idx];
3499
+ if (!chosen) return;
3500
+ s.recommended = chosen;
3501
+ s.alternatives = all.filter((c) => c.name !== chosen.name);
3502
+ s.swappedFrom = all[0].name;
3503
+ });
3504
+ } catch (err) {
3505
+ logger.warn(`plan: compat optimization failed: ${String(err)}`);
3506
+ }
3507
+ const compat = await checkCompat(
3508
+ db,
3509
+ eligible.map((s) => s.recommended.name)
3510
+ ).catch(() => null);
3511
+ if (compat) {
3512
+ const flags = flagSlotConflicts(
3513
+ slots.map((s) => ({ need: s.need, name: s.recommended?.name ?? null })),
3514
+ compat.conflicts
3515
+ );
3516
+ for (const s of slots) {
3517
+ const cw = flags.get(s.need);
3518
+ s.conflictsWith = cw?.length ? cw : void 0;
3519
+ }
3520
+ }
3521
+ return compat;
3522
+ }
3523
+ function planNote(source, unmatched, optimize, framework) {
3524
+ const base = source === "heuristic" ? "Components were extracted from your document with a keyword heuristic (no summary LLM configured) \u2014 coarse; pass a `needs` array or set SUMMARY_API_KEY for sharper decomposition." : source === "llm" ? "Components were extracted from your document by the summary model." : "Components taken from the supplied needs.";
3525
+ const grounding = " Each package is recommended from lurq\u2019s scored index \u2014 a labeled, evidence-backed starting point, not a validated architecture.";
3526
+ const ctx = framework ? ` Anchored to the ${framework} ecosystem so sibling libraries stay coherent across the stack.` : "";
3527
+ const tail = unmatched ? ` ${unmatched} need(s) had no tracked match (listed in \`unmatched\`).` : "";
3528
+ const opt = optimize === "speed" ? " Ranking favored the lightest-bundle option per slot." : "";
3529
+ return base + grounding + ctx + opt + tail;
3530
+ }
3531
+ function dedupeNeeds(needs) {
3532
+ const seen = /* @__PURE__ */ new Map();
3533
+ for (const n of needs) {
3534
+ const key = n.need.trim().toLowerCase();
3535
+ if (!key) continue;
3536
+ if (!seen.has(key)) seen.set(key, { need: n.need.trim(), category: n.category });
3537
+ }
3538
+ return [...seen.values()];
3539
+ }
3540
+ function familyOf(name) {
3541
+ const n = name.toLowerCase();
3542
+ for (const f of FAMILY_TOKENS) if (f.re.test(n)) return f.family;
3543
+ return null;
3544
+ }
3545
+ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
3546
+ const coherence = (c) => {
3547
+ const fam = familyOf(c.name);
3548
+ if (!fam) return 0;
3549
+ return fam === anchorFamily ? 1 : -1;
3550
+ };
3551
+ return [...cands].sort((a, b) => {
3552
+ if (anchorFamily) {
3553
+ const d = coherence(b) - coherence(a);
3554
+ if (d) return d;
3555
+ }
3556
+ if (optimize === "speed") {
3557
+ return (bundleByName.get(a.name) ?? Infinity) - (bundleByName.get(b.name) ?? Infinity);
3558
+ }
3559
+ return 0;
3560
+ });
3561
+ }
3562
+ async function bundleSizes(db, candidates) {
3563
+ const names = [...new Set(candidates.map((c) => c.name))];
3564
+ if (names.length === 0) return /* @__PURE__ */ new Map();
3565
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray3(packages.name, names));
3566
+ return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
3567
+ }
3568
+ async function decompose(document) {
3569
+ const config = getConfig();
3570
+ if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
3571
+ const llm = await decomposeWithLlm(document, config.SUMMARY_API_KEY, config.SUMMARY_MODEL).catch(
3572
+ (err) => {
3573
+ logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
3574
+ return null;
3575
+ }
3576
+ );
3577
+ if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
3578
+ }
3579
+ return { needs: decomposeHeuristic(document), source: "heuristic" };
3580
+ }
3581
+ async function decomposeWithLlm(document, apiKey, model) {
3582
+ const prompt = [
3583
+ "Project description:",
3584
+ document.slice(0, 8e3),
3585
+ "",
3586
+ 'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
3587
+ "One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
3588
+ ].join("\n");
3589
+ const { data } = await httpRequest("https://api.openai.com/v1/chat/completions", {
3590
+ host: "api.openai.com",
3591
+ method: "POST",
3592
+ ttlMs: 24 * 60 * 60 * 1e3,
3593
+ // Hash the FULL document — length + a 64-char prefix collide for same-length
3594
+ // edits or shared templated headers, which would serve a stale decomposition.
3595
+ cacheKey: `openai-plan ${model} ${createHash4("sha1").update(document).digest("hex")}`,
3596
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3597
+ body: JSON.stringify({
3598
+ model,
3599
+ messages: [
3600
+ { role: "system", content: DECOMPOSE_SYSTEM },
3601
+ { role: "user", content: prompt }
3602
+ ],
3603
+ response_format: { type: "json_object" },
3604
+ temperature: 0.2
3605
+ })
3606
+ });
3607
+ const content = data?.choices?.[0]?.message?.content;
3608
+ const parsed = content ? JSON.parse(content) : {};
3609
+ const raw = Array.isArray(parsed?.needs) ? parsed.needs : [];
3610
+ return raw.map((n) => {
3611
+ const need = typeof n?.need === "string" ? n.need.trim() : "";
3612
+ if (!need) return null;
3613
+ const cat = typeof n?.category === "string" && isCategory(n.category) ? n.category : void 0;
3614
+ return { need, category: cat };
3615
+ }).filter(Boolean);
3616
+ }
3617
+ function decomposeHeuristic(document) {
3618
+ const byCategory = /* @__PURE__ */ new Map();
3619
+ for (const rawLine of document.split("\n")) {
3620
+ const line = rawLine.replace(/^[#>*\-\s]+/, "").replace(/^\d+\.\s+/, "").trim();
3621
+ if (line.length < 3) continue;
3622
+ const category = inferCategory(line);
3623
+ if (category && !byCategory.has(category)) {
3624
+ byCategory.set(category, line.slice(0, 120));
3625
+ }
3626
+ }
3627
+ return [...byCategory.entries()].map(([category, need]) => ({ need, category }));
3628
+ }
3629
+ var PER_SLOT, MAX_SLOTS, NO_META, FAMILY_TOKENS, DECOMPOSE_SYSTEM;
3630
+ var init_plan = __esm({
3631
+ "src/mcp/plan.ts"() {
3632
+ "use strict";
3633
+ init_esm_shims();
3634
+ init_config();
3635
+ init_http();
3636
+ init_check();
3637
+ init_members();
3638
+ init_optimize();
3639
+ init_compat();
3640
+ init_logger();
3641
+ init_types();
3642
+ init_schema();
3643
+ init_categoryInference();
3644
+ init_recommend();
3645
+ init_single();
3646
+ init_diagram();
3647
+ init_handlers();
3648
+ PER_SLOT = 3;
3649
+ MAX_SLOTS = 24;
3650
+ NO_META = (c) => ({
3651
+ name: c.name,
3652
+ version: c.latestVersion,
3653
+ peerDependencies: null,
3654
+ peerDependenciesMeta: null,
3655
+ engines: null
3656
+ });
3657
+ FAMILY_TOKENS = [
3658
+ { family: "react", re: /\breact\b|preact/ },
3659
+ { family: "vue", re: /\bvue\b|nuxt/ },
3660
+ { family: "angular", re: /angular/ },
3661
+ { family: "svelte", re: /svelte/ },
3662
+ { family: "solid", re: /\bsolid(-?js)?\b/ }
3663
+ ];
3664
+ DECOMPOSE_SYSTEM = "You break a software project description into the distinct technical components that each need a library. Return ONLY components the project actually requires, grounded in the description. Respond with a JSON object.";
3665
+ }
3666
+ });
3667
+
3668
+ // src/mcp/server.ts
3669
+ var server_exports = {};
3670
+ __export(server_exports, {
3671
+ buildMcpServer: () => buildMcpServer,
3672
+ npmName: () => npmName,
3673
+ startMcpServer: () => startMcpServer
3674
+ });
3675
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2662
3676
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2663
3677
  import { z as z2 } from "zod";
2664
3678
  function json(obj) {
@@ -2685,7 +3699,7 @@ function buildMcpServer(db) {
2685
3699
  title: "Evaluate a package",
2686
3700
  description: "Full evidence read for one npm package: scores, signals, advisories, summary, and a usage guide. Fetches & scores on demand if not yet tracked.",
2687
3701
  inputSchema: {
2688
- package: z2.string().min(1).describe("npm package name")
3702
+ package: npmName.describe("npm package name")
2689
3703
  }
2690
3704
  },
2691
3705
  async (args) => json(await handleEvaluate(db, args))
@@ -2696,18 +3710,29 @@ function buildMcpServer(db) {
2696
3710
  title: "Compare packages",
2697
3711
  description: "Side-by-side comparison of 2\u20135 npm packages, ranked by health score.",
2698
3712
  inputSchema: {
2699
- packages: z2.array(z2.string().min(1)).min(2).max(5).describe("2\u20135 npm package names")
3713
+ packages: z2.array(npmName).min(2).max(5).describe("2\u20135 npm package names")
2700
3714
  }
2701
3715
  },
2702
3716
  async (args) => json(await handleCompare(db, args))
2703
3717
  );
3718
+ server.registerTool(
3719
+ "compat",
3720
+ {
3721
+ title: "Check package compatibility",
3722
+ description: "Check whether a set of packages forms a coherent stack: peer-dependency and engine-range compatibility across the whole set (instant, from declared metadata), plus any recorded sandbox-verified conflicts. Returns the exact clashing constraints. Read-only \u2014 does not run installs. Call before committing to a multi-package stack.",
3723
+ inputSchema: {
3724
+ packages: z2.array(npmName).min(2).max(8).describe("2\u20138 npm package names to check together")
3725
+ }
3726
+ },
3727
+ async (args) => json(await handleCompat(db, args))
3728
+ );
2704
3729
  server.registerTool(
2705
3730
  "verify",
2706
3731
  {
2707
3732
  title: "Verify a package",
2708
3733
  description: "Confirm an npm package is real, healthy, and not risky before installing \u2014 guards against hallucinated or typosquatted dependency names. Checks the live registry.",
2709
3734
  inputSchema: {
2710
- package: z2.string().min(1).describe("npm package name to verify")
3735
+ package: npmName.describe("npm package name to verify")
2711
3736
  }
2712
3737
  },
2713
3738
  async (args) => json(await handleVerify(db, args))
@@ -2718,11 +3743,32 @@ function buildMcpServer(db) {
2718
3743
  title: "Reference architecture diagram",
2719
3744
  description: "Emit a reference-architecture Mermaid diagram for a stack you have already chosen (package names). A labeled starting point keyed by layer \u2014 not a validated architecture, and not an architecture designer.",
2720
3745
  inputSchema: {
2721
- stack: z2.array(z2.string()).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
3746
+ stack: z2.array(npmName).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
2722
3747
  }
2723
3748
  },
2724
3749
  async (args) => json(await handleDiagram(db, args))
2725
3750
  );
3751
+ server.registerTool(
3752
+ "plan",
3753
+ {
3754
+ title: "Plan a stack from a program description",
3755
+ description: "Turn a detailed program description (spec/README) or a list of component needs into an evidence-scored build plan: a real, lurq-scored package recommended per component, plus a Mermaid roadmap other agents can parse. Recommends building blocks slot-by-slot from the index \u2014 it does not invent an architecture from a bare prompt.",
3756
+ inputSchema: {
3757
+ document: z2.string().optional().describe("Detailed description of the program (spec/README); lurq decomposes it into components"),
3758
+ needs: z2.array(
3759
+ z2.object({
3760
+ need: z2.string().min(1).describe("A component that needs a library"),
3761
+ category: categoryEnum.optional()
3762
+ })
3763
+ ).optional().describe("Pre-decomposed components (skip if you pass a document)"),
3764
+ using: z2.array(npmName).max(12).optional().describe(
3765
+ "Packages you have already decided on. lurq pins these as fixed slots, recommends only the remaining needs, and checks/optimizes the whole stack around your picks."
3766
+ ),
3767
+ optimize: z2.enum(["speed", "balanced"]).optional().describe("'speed' prefers the lightest-bundle option per slot; default 'balanced'")
3768
+ }
3769
+ },
3770
+ async (args) => json(await handlePlan(db, args))
3771
+ );
2726
3772
  return server;
2727
3773
  }
2728
3774
  async function startMcpServer() {
@@ -2741,7 +3787,7 @@ async function startMcpServer() {
2741
3787
  await server.connect(transport);
2742
3788
  logger.info(`${SERVER_NAME} MCP server v${VERSION} running on stdio.`);
2743
3789
  }
2744
- var categoryEnum, confidenceEnum, constraintsSchema;
3790
+ var categoryEnum, confidenceEnum, npmName, constraintsSchema;
2745
3791
  var init_server = __esm({
2746
3792
  "src/mcp/server.ts"() {
2747
3793
  "use strict";
@@ -2752,8 +3798,10 @@ var init_server = __esm({
2752
3798
  init_logger();
2753
3799
  init_handlers();
2754
3800
  init_diagram();
3801
+ init_plan();
2755
3802
  categoryEnum = z2.enum(CATEGORIES);
2756
3803
  confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
3804
+ npmName = z2.string().trim().min(1).max(214).regex(/^(?:@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/i, "Invalid npm package name");
2757
3805
  constraintsSchema = z2.object({
2758
3806
  runtime: z2.enum(["browser", "node", "both"]).optional(),
2759
3807
  license: z2.string().optional(),
@@ -2764,10 +3812,10 @@ var init_server = __esm({
2764
3812
  });
2765
3813
 
2766
3814
  // 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";
3815
+ import { createHash as createHash5, randomBytes } from "crypto";
3816
+ import { and as and5, desc as desc3, eq as eq5, isNull } from "drizzle-orm";
2769
3817
  function hashKey(key) {
2770
- return createHash3("sha256").update(key).digest("hex");
3818
+ return createHash5("sha256").update(key).digest("hex");
2771
3819
  }
2772
3820
  function generateApiKey() {
2773
3821
  const body = randomBytes(24).toString("base64url");
@@ -2787,21 +3835,34 @@ async function createKey(db, input = {}) {
2787
3835
  }
2788
3836
  async function lookupActiveKey(db, key) {
2789
3837
  const hash = hashKey(key);
2790
- const [row] = await db.select().from(apiKeys).where(and3(eq4(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
3838
+ const [row] = await db.select().from(apiKeys).where(and5(eq5(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
2791
3839
  if (!row) return null;
2792
- db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq4(apiKeys.id, row.id)).then(void 0, () => {
3840
+ db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, row.id)).then(void 0, () => {
2793
3841
  });
2794
3842
  return row;
2795
3843
  }
2796
3844
  async function listKeys(db) {
2797
- return db.select().from(apiKeys).orderBy(desc(apiKeys.createdAt));
3845
+ return db.select().from(apiKeys).orderBy(desc3(apiKeys.createdAt));
2798
3846
  }
2799
- async function revokeKey(db, prefixOrId) {
3847
+ function matchByPrefixOrId(prefixOrId) {
2800
3848
  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 });
3849
+ return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq5(apiKeys.id, asId) : eq5(apiKeys.prefix, prefixOrId);
3850
+ }
3851
+ async function revokeKey(db, prefixOrId) {
3852
+ const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
2803
3853
  return rows.length;
2804
3854
  }
3855
+ async function rotateKey(db, prefixOrId) {
3856
+ const [previous] = await db.select().from(apiKeys).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).limit(1);
3857
+ if (!previous) return null;
3858
+ const { key, row } = await createKey(db, {
3859
+ label: previous.label ?? void 0,
3860
+ tier: previous.tier,
3861
+ ownerId: previous.ownerId ?? void 0
3862
+ });
3863
+ await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, previous.id));
3864
+ return { key, row, previous };
3865
+ }
2805
3866
  var DISPLAY_BODY;
2806
3867
  var init_apiKeys = __esm({
2807
3868
  "src/auth/apiKeys.ts"() {
@@ -2866,10 +3927,15 @@ async function startHttpServer(opts = {}) {
2866
3927
  limit: config.LURQ_RATE_LIMIT_MAX,
2867
3928
  standardHeaders: "draft-7",
2868
3929
  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"),
3930
+ // Key on the resolved API key's unique row id (always present — auth runs
3931
+ // first). The display `prefix` is only 6 chars of body, so distinct keys
3932
+ // can collide on it and share a quota; the id cannot. The IP fallback uses
3933
+ // express-rate-limit's ipKeyGenerator so IPv6 addresses are normalized
3934
+ // correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
3935
+ keyGenerator: (req) => {
3936
+ const id = req.lurqKey?.id;
3937
+ return id != null ? `key:${id}` : ipKeyGenerator(req.ip ?? "0.0.0.0");
3938
+ },
2873
3939
  message: rpcError(-32029, "Rate limit exceeded.")
2874
3940
  });
2875
3941
  app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
@@ -2910,23 +3976,24 @@ var init_http2 = __esm({
2910
3976
  });
2911
3977
 
2912
3978
  // src/pipeline/rescore.ts
2913
- import { isNotNull as isNotNull3 } from "drizzle-orm";
2914
- import { eq as eq5 } from "drizzle-orm";
3979
+ import { isNotNull as isNotNull4 } from "drizzle-orm";
3980
+ import { eq as eq6 } from "drizzle-orm";
2915
3981
  async function runRescore() {
2916
3982
  const weights = loadWeights();
2917
3983
  const handle = createDb({ max: 4 });
2918
3984
  try {
2919
- const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull3(packages.scoreBreakdown));
3985
+ const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull4(packages.scoreBreakdown));
2920
3986
  let updated = 0;
2921
3987
  for (const row of rows) {
2922
3988
  if (!row.breakdown) continue;
2923
3989
  const health = computeHealthScore(row.breakdown, weights.health);
2924
3990
  if (health !== row.healthScore) {
2925
- await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq5(packages.id, row.id));
3991
+ await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq6(packages.id, row.id));
2926
3992
  updated++;
2927
3993
  }
2928
3994
  }
2929
3995
  logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
3996
+ if (updated > 0) await invalidateCache();
2930
3997
  return { seen: rows.length, updated };
2931
3998
  } finally {
2932
3999
  await handle.close();
@@ -2936,6 +4003,7 @@ var init_rescore = __esm({
2936
4003
  "src/pipeline/rescore.ts"() {
2937
4004
  "use strict";
2938
4005
  init_esm_shims();
4006
+ init_cache();
2939
4007
  init_logger();
2940
4008
  init_client();
2941
4009
  init_schema();
@@ -2945,7 +4013,7 @@ var init_rescore = __esm({
2945
4013
  });
2946
4014
 
2947
4015
  // src/db/discovery.ts
2948
- import { eq as eq6, inArray as inArray2, sql as sql6 } from "drizzle-orm";
4016
+ import { eq as eq7 } from "drizzle-orm";
2949
4017
  async function getKnownNames(db) {
2950
4018
  const [tracked, queued] = await Promise.all([
2951
4019
  db.select({ name: packages.name }).from(packages),
@@ -2960,10 +4028,10 @@ async function enqueueCandidates(db, candidates) {
2960
4028
  return inserted.length;
2961
4029
  }
2962
4030
  async function getPendingCandidates(db, limit) {
2963
- return db.select().from(discoveryQueue).where(eq6(discoveryQueue.status, "pending")).limit(limit);
4031
+ return db.select().from(discoveryQueue).where(eq7(discoveryQueue.status, "pending")).limit(limit);
2964
4032
  }
2965
4033
  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));
4034
+ await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq7(discoveryQueue.name, name));
2967
4035
  }
2968
4036
  var init_discovery = __esm({
2969
4037
  "src/db/discovery.ts"() {
@@ -2974,7 +4042,7 @@ var init_discovery = __esm({
2974
4042
  });
2975
4043
 
2976
4044
  // src/pipeline/discovery.ts
2977
- import { isNotNull as isNotNull4 } from "drizzle-orm";
4045
+ import { isNotNull as isNotNull5 } from "drizzle-orm";
2978
4046
  function selectCandidates(raw, known) {
2979
4047
  const seen = new Set(known);
2980
4048
  const out = [];
@@ -3007,7 +4075,7 @@ async function preScorePackage(name, fetchImpl) {
3007
4075
  }
3008
4076
  }
3009
4077
  async function graphChannel(db) {
3010
- const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull4(packages.latestVersion));
4078
+ const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull5(packages.latestVersion));
3011
4079
  const out = [];
3012
4080
  for (const t of tracked) {
3013
4081
  if (!t.version) continue;
@@ -3177,14 +4245,460 @@ var init_format = __esm({
3177
4245
  }
3178
4246
  });
3179
4247
 
4248
+ // src/cli/planView.ts
4249
+ var planView_exports = {};
4250
+ __export(planView_exports, {
4251
+ renderPlanHtml: () => renderPlanHtml
4252
+ });
4253
+ function esc(s) {
4254
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4255
+ }
4256
+ function slotRows(plan) {
4257
+ return plan.slots.map((s) => {
4258
+ const rec = s.recommended;
4259
+ const name = rec ? rec.repoUrl ? `<a href="${esc(rec.repoUrl)}" target="_blank" rel="noreferrer">${esc(rec.name)}</a>` : esc(rec.name) : '<span class="muted">\u2014 no match \u2014</span>';
4260
+ const alts = s.alternatives.map((a) => esc(a.name)).join(", ") || '<span class="muted">\u2014</span>';
4261
+ return `<tr>
4262
+ <td>${esc(s.need)}</td>
4263
+ <td><span class="layer">${esc(s.layer)}</span></td>
4264
+ <td class="pkg">${name}</td>
4265
+ <td class="num">${rec ? rec.healthScore : "\u2014"}</td>
4266
+ <td>${rec ? esc(rec.confidence) : "\u2014"}</td>
4267
+ <td class="alts">${alts}</td>
4268
+ </tr>`;
4269
+ }).join("\n");
4270
+ }
4271
+ function renderPlanHtml(plan) {
4272
+ return `<!doctype html>
4273
+ <html lang="en">
4274
+ <head>
4275
+ <meta charset="utf-8" />
4276
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4277
+ <title>lurq plan \u2014 roadmap</title>
4278
+ <style>
4279
+ :root { color-scheme: dark; }
4280
+ * { box-sizing: border-box; }
4281
+ body { margin: 0; font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
4282
+ background: #0b0b0f; color: #e7e7ea; padding: 2.5rem clamp(1rem, 5vw, 4rem); }
4283
+ h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
4284
+ h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: .08em; color: #9a9aa6; margin: 2.5rem 0 .75rem; }
4285
+ .sub { color: #9a9aa6; margin: 0 0 1rem; max-width: 70ch; }
4286
+ .diagram { background: #141419; border: 1px solid #26262e; border-radius: 14px; padding: 1.5rem; overflow: auto; }
4287
+ table { width: 100%; border-collapse: collapse; margin-top: .5rem; }
4288
+ th, td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid #1e1e25; vertical-align: top; }
4289
+ th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #9a9aa6; }
4290
+ .pkg a { color: #d98cff; text-decoration: none; } .pkg a:hover { text-decoration: underline; }
4291
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
4292
+ .muted { color: #6a6a76; } .alts { color: #b8b8c2; font-size: .92rem; }
4293
+ .layer { font-size: .8rem; color: #8ad; background: #15202b; border-radius: 6px; padding: .1rem .45rem; }
4294
+ footer { margin-top: 2.5rem; color: #6a6a76; font-size: .85rem; }
4295
+ </style>
4296
+ </head>
4297
+ <body>
4298
+ <h1>lurq plan <span class="muted">\xB7 ${esc(plan.optimize)} \xB7 ${esc(plan.source)}${plan.framework ? ` \xB7 ${esc(plan.framework)} ecosystem` : ""}</span></h1>
4299
+ <p class="sub">${esc(plan.note)}</p>
4300
+
4301
+ <h2>Roadmap</h2>
4302
+ <div class="diagram"><pre class="mermaid">${esc(plan.mermaid)}</pre></div>
4303
+
4304
+ <h2>Components</h2>
4305
+ <table>
4306
+ <thead><tr><th>Component</th><th>Layer</th><th>Recommended</th><th>Health</th><th>Confidence</th><th>Alternatives</th></tr></thead>
4307
+ <tbody>
4308
+ ${slotRows(plan)}
4309
+ </tbody>
4310
+ </table>
4311
+
4312
+ <footer>data as of ${esc(plan.dataAsOf)} \xB7 generated by lurq</footer>
4313
+
4314
+ <script type="module">
4315
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
4316
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
4317
+ </script>
4318
+ </body>
4319
+ </html>
4320
+ `;
4321
+ }
4322
+ var init_planView = __esm({
4323
+ "src/cli/planView.ts"() {
4324
+ "use strict";
4325
+ init_esm_shims();
4326
+ }
4327
+ });
4328
+
4329
+ // src/db/watch.ts
4330
+ import { eq as eq8 } from "drizzle-orm";
4331
+ async function getWatchCursor(db, id) {
4332
+ const rows = await db.select({ seq: watchState.seq }).from(watchState).where(eq8(watchState.id, id)).limit(1);
4333
+ return rows[0]?.seq ?? null;
4334
+ }
4335
+ async function setWatchCursor(db, id, seq) {
4336
+ await db.insert(watchState).values({ id, seq, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({ target: watchState.id, set: { seq, updatedAt: /* @__PURE__ */ new Date() } });
4337
+ }
4338
+ var init_watch = __esm({
4339
+ "src/db/watch.ts"() {
4340
+ "use strict";
4341
+ init_esm_shims();
4342
+ init_schema();
4343
+ }
4344
+ });
4345
+
4346
+ // src/pipeline/watch.ts
4347
+ var watch_exports = {};
4348
+ __export(watch_exports, {
4349
+ parseChangeLine: () => parseChangeLine,
4350
+ watchNpmChanges: () => watchNpmChanges
4351
+ });
4352
+ function parseChangeLine(line) {
4353
+ const trimmed = line.trim();
4354
+ if (!trimmed) return null;
4355
+ try {
4356
+ const obj = JSON.parse(trimmed);
4357
+ if (typeof obj?.id !== "string" || obj.seq == null) return null;
4358
+ return { seq: obj.seq, id: obj.id, deleted: Boolean(obj.deleted) };
4359
+ } catch {
4360
+ return null;
4361
+ }
4362
+ }
4363
+ async function watchNpmChanges(db, opts = {}) {
4364
+ const { signal } = opts;
4365
+ let backoff2 = 1e3;
4366
+ while (!signal?.aborted) {
4367
+ let tracked = new Set(await getAllPackageNames(db));
4368
+ let trackedAt = Date.now();
4369
+ const since = await getWatchCursor(db, FEED_ID) ?? opts.since ?? "now";
4370
+ const url = `${FEED_URL}?feed=continuous&since=${encodeURIComponent(since)}&heartbeat=${HEARTBEAT_MS}`;
4371
+ logger.info(`watch: connecting from seq=${since} (${tracked.size} tracked packages)`);
4372
+ try {
4373
+ const res = await fetch(url, { signal });
4374
+ if (!res.ok || !res.body) throw new Error(`feed responded ${res.status}`);
4375
+ backoff2 = 1e3;
4376
+ let sinceCheckpoint = 0;
4377
+ for await (const line of ndjsonLines(res.body, signal)) {
4378
+ const change = parseChangeLine(line);
4379
+ if (!change) continue;
4380
+ const seq = String(change.seq);
4381
+ sinceCheckpoint++;
4382
+ if (Date.now() - trackedAt > TRACKED_REFRESH_MS) {
4383
+ tracked = new Set(await getAllPackageNames(db));
4384
+ trackedAt = Date.now();
4385
+ }
4386
+ if (!change.deleted && tracked.has(change.id)) {
4387
+ logger.info(`watch: re-syncing ${change.id} (seq=${seq})`);
4388
+ await syncOnePackage(db, change.id).catch(
4389
+ (err) => logger.warn(`watch: re-sync failed for ${change.id}: ${String(err)}`)
4390
+ );
4391
+ await setWatchCursor(db, FEED_ID, seq);
4392
+ sinceCheckpoint = 0;
4393
+ } else if (sinceCheckpoint >= CHECKPOINT_EVERY) {
4394
+ await setWatchCursor(db, FEED_ID, seq);
4395
+ sinceCheckpoint = 0;
4396
+ }
4397
+ }
4398
+ logger.info("watch: feed stream ended; reconnecting");
4399
+ } catch (err) {
4400
+ if (signal?.aborted) break;
4401
+ logger.warn(`watch: ${String(err)} \u2014 retrying in ${backoff2}ms`);
4402
+ await sleep(backoff2, signal);
4403
+ backoff2 = Math.min(backoff2 * 2, MAX_BACKOFF_MS);
4404
+ }
4405
+ }
4406
+ }
4407
+ async function* ndjsonLines(body, signal) {
4408
+ const reader = body.getReader();
4409
+ const decoder = new TextDecoder();
4410
+ let buffer = "";
4411
+ try {
4412
+ while (!signal?.aborted) {
4413
+ const { value, done } = await reader.read();
4414
+ if (done) break;
4415
+ buffer += decoder.decode(value, { stream: true });
4416
+ let nl;
4417
+ while ((nl = buffer.indexOf("\n")) >= 0) {
4418
+ yield buffer.slice(0, nl);
4419
+ buffer = buffer.slice(nl + 1);
4420
+ }
4421
+ }
4422
+ } finally {
4423
+ reader.releaseLock();
4424
+ }
4425
+ }
4426
+ function sleep(ms, signal) {
4427
+ return new Promise((resolve) => {
4428
+ const timer = setTimeout(resolve, ms);
4429
+ signal?.addEventListener(
4430
+ "abort",
4431
+ () => {
4432
+ clearTimeout(timer);
4433
+ resolve();
4434
+ },
4435
+ { once: true }
4436
+ );
4437
+ });
4438
+ }
4439
+ var FEED_ID, FEED_URL, HEARTBEAT_MS, TRACKED_REFRESH_MS, CHECKPOINT_EVERY, MAX_BACKOFF_MS;
4440
+ var init_watch2 = __esm({
4441
+ "src/pipeline/watch.ts"() {
4442
+ "use strict";
4443
+ init_esm_shims();
4444
+ init_logger();
4445
+ init_packages();
4446
+ init_watch();
4447
+ init_single();
4448
+ FEED_ID = "npm-changes";
4449
+ FEED_URL = "https://replicate.npmjs.com/_changes";
4450
+ HEARTBEAT_MS = 3e4;
4451
+ TRACKED_REFRESH_MS = 5 * 6e4;
4452
+ CHECKPOINT_EVERY = 200;
4453
+ MAX_BACKOFF_MS = 3e4;
4454
+ }
4455
+ });
4456
+
4457
+ // src/sandbox/types.ts
4458
+ var DEFAULT_TARGET;
4459
+ var init_types2 = __esm({
4460
+ "src/sandbox/types.ts"() {
4461
+ "use strict";
4462
+ init_esm_shims();
4463
+ DEFAULT_TARGET = {
4464
+ node: "20",
4465
+ moduleSystem: "cjs"
4466
+ };
4467
+ }
4468
+ });
4469
+
4470
+ // src/sandbox/local.ts
4471
+ import { execFile } from "child_process";
4472
+ import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
4473
+ import { tmpdir } from "os";
4474
+ import { join as join3 } from "path";
4475
+ import { promisify } from "util";
4476
+ function npmInstallArgs(specs, opts) {
4477
+ const bad = specs.find((s) => s.startsWith("-"));
4478
+ if (bad) throw new Error(`Invalid package spec: ${bad}`);
4479
+ const args = ["install", ...specs, "--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
4480
+ if (!opts.allowScripts) args.push("--ignore-scripts");
4481
+ return args;
4482
+ }
4483
+ function smokeScript(pkg, moduleSystem) {
4484
+ return moduleSystem === "esm" ? ["--input-type=module", "-e", `await import(${JSON.stringify(pkg)})`] : ["-e", `require(${JSON.stringify(pkg)})`];
4485
+ }
4486
+ function condense(s) {
4487
+ return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX);
4488
+ }
4489
+ function stderrOf(err) {
4490
+ if (err && typeof err === "object") {
4491
+ const e = err;
4492
+ if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
4493
+ if (typeof e.message === "string") return e.message;
4494
+ }
4495
+ return String(err);
4496
+ }
4497
+ var execFileAsync, INSTALL_TIMEOUT_MS, SMOKE_TIMEOUT_MS, ERROR_MAX, toSpec, LocalSandbox;
4498
+ var init_local = __esm({
4499
+ "src/sandbox/local.ts"() {
4500
+ "use strict";
4501
+ init_esm_shims();
4502
+ init_types2();
4503
+ execFileAsync = promisify(execFile);
4504
+ INSTALL_TIMEOUT_MS = 12e4;
4505
+ SMOKE_TIMEOUT_MS = 3e4;
4506
+ ERROR_MAX = 500;
4507
+ toSpec = (p) => p.version ? `${p.name}@${p.version}` : p.name;
4508
+ LocalSandbox = class {
4509
+ name = "local";
4510
+ async verify(pkg, version, opts = {}) {
4511
+ const set = await this.verifySet([{ name: pkg, version }], opts);
4512
+ return {
4513
+ driver: this.name,
4514
+ moduleSystem: set.moduleSystem,
4515
+ installed: set.installed,
4516
+ imported: set.loaded[0]?.loaded ?? null,
4517
+ ranScripts: opts.allowScripts ?? false,
4518
+ durationMs: set.durationMs,
4519
+ error: set.error
4520
+ };
4521
+ }
4522
+ async verifySet(packages2, opts = {}) {
4523
+ const target = opts.target ?? DEFAULT_TARGET;
4524
+ const allowScripts = opts.allowScripts ?? false;
4525
+ const specs = packages2.map(toSpec);
4526
+ const dir = await mkdtemp(join3(tmpdir(), "lurq-sandbox-"));
4527
+ const started = Date.now();
4528
+ const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
4529
+ let installed = false;
4530
+ let error = null;
4531
+ try {
4532
+ await writeFile2(
4533
+ join3(dir, "package.json"),
4534
+ JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
4535
+ );
4536
+ await execFileAsync("npm", npmInstallArgs(specs, { allowScripts }), {
4537
+ cwd: dir,
4538
+ timeout: opts.timeoutMs ?? INSTALL_TIMEOUT_MS,
4539
+ signal: opts.signal
4540
+ });
4541
+ installed = true;
4542
+ for (let i = 0; i < packages2.length; i++) {
4543
+ try {
4544
+ await execFileAsync("node", smokeScript(packages2[i].name, target.moduleSystem), {
4545
+ cwd: dir,
4546
+ timeout: SMOKE_TIMEOUT_MS,
4547
+ signal: opts.signal
4548
+ });
4549
+ loaded[i].loaded = true;
4550
+ } catch (err) {
4551
+ loaded[i].loaded = false;
4552
+ if (!error) error = condense(stderrOf(err));
4553
+ }
4554
+ }
4555
+ } catch (err) {
4556
+ error = condense(stderrOf(err));
4557
+ } finally {
4558
+ await rm(dir, { recursive: true, force: true }).catch(() => {
4559
+ });
4560
+ }
4561
+ return {
4562
+ driver: this.name,
4563
+ moduleSystem: target.moduleSystem,
4564
+ installed,
4565
+ loaded,
4566
+ durationMs: Date.now() - started,
4567
+ error
4568
+ };
4569
+ }
4570
+ };
4571
+ }
4572
+ });
4573
+
4574
+ // src/sandbox/index.ts
4575
+ function getSandbox() {
4576
+ return new LocalSandbox();
4577
+ }
4578
+ var init_sandbox = __esm({
4579
+ "src/sandbox/index.ts"() {
4580
+ "use strict";
4581
+ init_esm_shims();
4582
+ init_local();
4583
+ init_types2();
4584
+ init_local();
4585
+ }
4586
+ });
4587
+
4588
+ // src/pipeline/sandbox.ts
4589
+ var sandbox_exports = {};
4590
+ __export(sandbox_exports, {
4591
+ verifyPackageInSandbox: () => verifyPackageInSandbox
4592
+ });
4593
+ async function verifyPackageInSandbox(db, pkg, version, opts = {}) {
4594
+ const result = await getSandbox().verify(pkg, version, opts);
4595
+ await storeVerificationRun(db, {
4596
+ packageName: pkg,
4597
+ version: version ?? "latest",
4598
+ driver: result.driver,
4599
+ moduleSystem: result.moduleSystem,
4600
+ installed: result.installed,
4601
+ imported: result.imported,
4602
+ ranScripts: result.ranScripts,
4603
+ durationMs: result.durationMs,
4604
+ error: result.error,
4605
+ ranAt: /* @__PURE__ */ new Date()
4606
+ }).catch(() => {
4607
+ });
4608
+ return result;
4609
+ }
4610
+ var init_sandbox2 = __esm({
4611
+ "src/pipeline/sandbox.ts"() {
4612
+ "use strict";
4613
+ init_esm_shims();
4614
+ init_verification();
4615
+ init_sandbox();
4616
+ }
4617
+ });
4618
+
4619
+ // src/pipeline/compat.ts
4620
+ var compat_exports = {};
4621
+ __export(compat_exports, {
4622
+ deriveCompatEdges: () => deriveCompatEdges,
4623
+ verifyCompatibility: () => verifyCompatibility
4624
+ });
4625
+ function deriveCompatEdges(resolved, result) {
4626
+ const allLoaded = result.loaded.every((l) => l.loaded === true);
4627
+ const edges = [];
4628
+ if (result.installed && allLoaded) {
4629
+ for (let i = 0; i < resolved.length; i++) {
4630
+ for (let j = i + 1; j < resolved.length; j++) {
4631
+ edges.push({
4632
+ a: resolved[i].name,
4633
+ aVersion: resolved[i].version,
4634
+ b: resolved[j].name,
4635
+ bVersion: resolved[j].version,
4636
+ status: "compatible"
4637
+ });
4638
+ }
4639
+ }
4640
+ } else if (resolved.length === 2) {
4641
+ edges.push({
4642
+ a: resolved[0].name,
4643
+ aVersion: resolved[0].version,
4644
+ b: resolved[1].name,
4645
+ bVersion: resolved[1].version,
4646
+ status: "conflict"
4647
+ });
4648
+ }
4649
+ return edges;
4650
+ }
4651
+ async function verifyCompatibility(db, packages2, opts = {}) {
4652
+ const resolved = await Promise.all(
4653
+ packages2.map(async (name) => ({
4654
+ name,
4655
+ version: (await getPackageByName(db, name))?.latestVersion ?? "latest"
4656
+ }))
4657
+ );
4658
+ const result = await getSandbox().verifySet(
4659
+ resolved.map((r) => ({ name: r.name, version: r.version === "latest" ? null : r.version })),
4660
+ { allowScripts: opts.allowScripts }
4661
+ );
4662
+ const edges = deriveCompatEdges(resolved, result);
4663
+ for (const e of edges) {
4664
+ const pair = canonicalPair(
4665
+ { name: e.a, version: e.aVersion },
4666
+ { name: e.b, version: e.bVersion }
4667
+ );
4668
+ await upsertCompatEdge(db, {
4669
+ ...pair,
4670
+ status: e.status,
4671
+ driver: result.driver,
4672
+ ranAt: /* @__PURE__ */ new Date()
4673
+ }).catch(() => {
4674
+ });
4675
+ }
4676
+ const failed = !result.installed || !result.loaded.every((l) => l.loaded === true);
4677
+ return { result, edges, unattributedConflict: failed && edges.length === 0 };
4678
+ }
4679
+ var init_compat2 = __esm({
4680
+ "src/pipeline/compat.ts"() {
4681
+ "use strict";
4682
+ init_esm_shims();
4683
+ init_compat();
4684
+ init_packages();
4685
+ init_sandbox();
4686
+ }
4687
+ });
4688
+
3180
4689
  // src/cli/commands.ts
3181
4690
  var commands_exports = {};
3182
4691
  __export(commands_exports, {
3183
4692
  runCompare: () => runCompare,
4693
+ runCompat: () => runCompat,
3184
4694
  runEditWeights: () => runEditWeights,
3185
4695
  runEvaluate: () => runEvaluate,
4696
+ runPlan: () => runPlan,
3186
4697
  runRecommend: () => runRecommend,
4698
+ runSandbox: () => runSandbox,
3187
4699
  runVerify: () => runVerify,
4700
+ runVersions: () => runVersions,
4701
+ runWatch: () => runWatch,
3188
4702
  runWeights: () => runWeights
3189
4703
  });
3190
4704
  async function withDb(fn) {
@@ -3257,6 +4771,12 @@ async function runEvaluate(pkg, opts) {
3257
4771
  ["repo", res.repoUrl ?? "\u2014"]
3258
4772
  ])
3259
4773
  );
4774
+ if (res.buildVerified) {
4775
+ const bv = res.buildVerified;
4776
+ const state = !bv.installed ? red("install failed") : bv.loaded === false ? yellow("installs, load failed") : green("installs and loads");
4777
+ console.log(`
4778
+ sandbox: ${state} ${dim(`${bv.version} \xB7 ${bv.driver}`)}`);
4779
+ }
3260
4780
  if (res.summary) console.log("\n" + res.summary);
3261
4781
  if (res.usageGuide) {
3262
4782
  const g = res.usageGuide;
@@ -3332,11 +4852,13 @@ function runWeights(opts = {}) {
3332
4852
  console.log(dim(`
3333
4853
  Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
3334
4854
  }
3335
- function runEditWeights(opts) {
4855
+ async function runEditWeights(opts) {
4856
+ const { invalidateCache: invalidateCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
3336
4857
  if (opts.reset) {
3337
4858
  const removed = resetWeights();
3338
4859
  console.log(removed.length ? `Removed overrides:
3339
4860
  ${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
4861
+ if (removed.length) await invalidateCache2();
3340
4862
  return;
3341
4863
  }
3342
4864
  if (opts.explain) {
@@ -3352,6 +4874,7 @@ function runEditWeights(opts) {
3352
4874
  const next = applyOverrides(loadWeights(), opts.set);
3353
4875
  const { weights, normalized } = validateWeights(next);
3354
4876
  const path2 = saveWeights(weights, opts.project ? "project" : "user");
4877
+ await invalidateCache2();
3355
4878
  console.log(`Saved overrides to ${path2}`);
3356
4879
  if (normalized) {
3357
4880
  console.log(
@@ -3365,11 +4888,83 @@ function runEditWeights(opts) {
3365
4888
  `));
3366
4889
  runWeights(opts);
3367
4890
  }
4891
+ async function openInBrowser(target) {
4892
+ const { spawn } = await import("child_process");
4893
+ const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", target]] : ["xdg-open", [target]];
4894
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
4895
+ }
4896
+ async function runPlan(file, opts) {
4897
+ const { readFileSync: readFileSync4 } = await import("fs");
4898
+ let document;
4899
+ try {
4900
+ document = readFileSync4(file, "utf8");
4901
+ } catch {
4902
+ throw new Error(`Could not read "${file}".`);
4903
+ }
4904
+ if (opts.optimize && opts.optimize !== "speed" && opts.optimize !== "balanced") {
4905
+ throw new Error("--optimize must be 'speed' or 'balanced'.");
4906
+ }
4907
+ await withDb(async (db) => {
4908
+ const { handlePlan: handlePlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
4909
+ const res = await handlePlan2(db, {
4910
+ document,
4911
+ optimize: opts.optimize
4912
+ });
4913
+ if (opts.json) return console.log(JSON.stringify(res, null, 2));
4914
+ if (!("slots" in res)) {
4915
+ console.log(res.note);
4916
+ return;
4917
+ }
4918
+ if (opts.html || opts.open) {
4919
+ const { writeFileSync: writeFileSync3 } = await import("fs");
4920
+ const { tmpdir: tmpdir2 } = await import("os");
4921
+ const { join: join5 } = await import("path");
4922
+ const { renderPlanHtml: renderPlanHtml2 } = await Promise.resolve().then(() => (init_planView(), planView_exports));
4923
+ const out = opts.html ?? join5(tmpdir2(), `lurq-plan-${Date.now()}.html`);
4924
+ writeFileSync3(out, renderPlanHtml2(res), "utf8");
4925
+ console.log(`Roadmap written to ${out}`);
4926
+ if (opts.open) await openInBrowser(out);
4927
+ }
4928
+ console.log(
4929
+ table(
4930
+ ["Component", "Layer", "Recommended", "Health", "Confidence", "Alternatives"],
4931
+ res.slots.map((s) => [
4932
+ s.need.length > 32 ? s.need.slice(0, 31) + "\u2026" : s.need,
4933
+ s.layer,
4934
+ s.recommended ? `${s.recommended.name}@${s.recommended.latestVersion ?? "?"}` : dim("\u2014"),
4935
+ s.recommended ? String(s.recommended.healthScore) : "\u2014",
4936
+ s.recommended ? confidenceLabel(s.recommended.confidence) : "\u2014",
4937
+ s.alternatives.map((a) => a.name).join(", ") || "\u2014"
4938
+ ])
4939
+ )
4940
+ );
4941
+ if (res.unmatched.length) console.log(yellow(`
4942
+ No match for: ${res.unmatched.join(", ")}`));
4943
+ if (res.compatibility) {
4944
+ const c = res.compatibility;
4945
+ const col = c.overall === "compatible" ? green : c.overall === "conflict" ? red : dim;
4946
+ console.log("\n" + bold("Compatibility: ") + col(c.overall));
4947
+ for (const s of res.slots) {
4948
+ if (s.swappedFrom && s.recommended) {
4949
+ console.log(green(` \u2713 swapped ${s.swappedFrom} \u2192 ${s.recommended.name} for compatibility`));
4950
+ }
4951
+ }
4952
+ for (const cf of c.conflicts) console.log(red(` \u2717 ${cf.detail} (no compatible alternative)`));
4953
+ if (c.unverified.length) console.log(dim(` unverified: ${c.unverified.join(", ")}`));
4954
+ }
4955
+ console.log("\n" + bold("Roadmap (Mermaid):"));
4956
+ console.log(res.mermaid);
4957
+ console.log(dim(`
4958
+ ${res.note}`));
4959
+ console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
4960
+ });
4961
+ }
3368
4962
  async function runVerify(pkg, opts) {
3369
4963
  await withDb(async (db) => {
3370
4964
  const res = await handleVerify(db, { package: pkg });
3371
4965
  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");
4966
+ const riskColor = res.risk === "high" ? red : res.risk === "medium" ? yellow : green;
4967
+ const verdict = !res.exists ? red("\u2717 NOT FOUND on npm") : res.risk === "high" ? red("\u2717 high supply-chain risk") : res.risk === "medium" ? yellow("\u26A0 exists, but risky") : green("\u2713 looks safe");
3373
4968
  console.log(`${bold(pkg)} ${verdict}`);
3374
4969
  console.log(
3375
4970
  detail([
@@ -3377,11 +4972,107 @@ async function runVerify(pkg, opts) {
3377
4972
  ["weekly dl", formatNumber(res.weeklyDownloads)],
3378
4973
  ["confidence", res.confidence ? confidenceLabel(res.confidence) : "\u2014"],
3379
4974
  ["advisories", String(res.advisoryCount)],
4975
+ ["risk", riskColor(res.risk)],
3380
4976
  ["risk flags", res.riskFlags.length ? yellow(res.riskFlags.join(", ")) : "none"]
3381
4977
  ])
3382
4978
  );
3383
4979
  });
3384
4980
  }
4981
+ async function runVersions(pkg, opts) {
4982
+ const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10) || 30) : 30;
4983
+ await withDb(async (db) => {
4984
+ const versions = await getPackageVersions(db, pkg, limit);
4985
+ if (opts.json) return console.log(JSON.stringify(versions, null, 2));
4986
+ if (versions.length === 0) {
4987
+ console.log(`No stored versions for ${bold(pkg)}. Run \`lurq sync --package ${pkg}\` first.`);
4988
+ return;
4989
+ }
4990
+ console.log(bold(pkg));
4991
+ console.log(
4992
+ table(
4993
+ ["Version", "Published"],
4994
+ versions.map((v) => [
4995
+ v.version,
4996
+ v.publishedAt ? v.publishedAt.toISOString().slice(0, 10) : "\u2014"
4997
+ ])
4998
+ )
4999
+ );
5000
+ });
5001
+ }
5002
+ async function runWatch() {
5003
+ const { watchNpmChanges: watchNpmChanges2 } = await Promise.resolve().then(() => (init_watch2(), watch_exports));
5004
+ await withDb(async (db) => {
5005
+ const controller = new AbortController();
5006
+ const stop = () => controller.abort();
5007
+ process.once("SIGINT", stop);
5008
+ process.once("SIGTERM", stop);
5009
+ console.log(dim("watching npm for releases of tracked packages \u2014 Ctrl-C to stop"));
5010
+ try {
5011
+ await watchNpmChanges2(db, { signal: controller.signal });
5012
+ } finally {
5013
+ process.off("SIGINT", stop);
5014
+ process.off("SIGTERM", stop);
5015
+ }
5016
+ });
5017
+ }
5018
+ async function runSandbox(pkg, version, opts) {
5019
+ if (opts.allowScripts) {
5020
+ console.error(
5021
+ yellow("warning: running install scripts and loading the package locally without isolation")
5022
+ );
5023
+ }
5024
+ const { verifyPackageInSandbox: verifyPackageInSandbox2 } = await Promise.resolve().then(() => (init_sandbox2(), sandbox_exports));
5025
+ await withDb(async (db) => {
5026
+ const result = await verifyPackageInSandbox2(db, pkg, version ?? null, {
5027
+ target: { node: "20", moduleSystem: opts.esm ? "esm" : "cjs" },
5028
+ allowScripts: opts.allowScripts
5029
+ });
5030
+ if (opts.json) return console.log(JSON.stringify(result, null, 2));
5031
+ const ok = result.installed && result.imported !== false;
5032
+ const label = version ? `${pkg}@${version}` : pkg;
5033
+ const verdict = ok ? green("\u2713 installs and loads") : red("\u2717 failed");
5034
+ console.log(`${bold(label)} ${verdict} ${dim(`(${result.durationMs}ms \xB7 ${result.driver})`)}`);
5035
+ console.log(
5036
+ detail([
5037
+ ["installed", result.installed ? "yes" : "no"],
5038
+ ["loaded", result.imported === null ? "\u2014" : result.imported ? "yes" : "no"],
5039
+ ["module", result.moduleSystem],
5040
+ ["scripts", result.ranScripts ? "ran" : "skipped"],
5041
+ ["error", result.error ?? "none"]
5042
+ ])
5043
+ );
5044
+ });
5045
+ }
5046
+ async function runCompat(pkgs, opts) {
5047
+ await withDb(async (db) => {
5048
+ if (opts.run) {
5049
+ console.error(
5050
+ yellow("co-installing in the sandbox (loads package code locally without isolation)")
5051
+ );
5052
+ const { verifyCompatibility: verifyCompatibility2 } = await Promise.resolve().then(() => (init_compat2(), compat_exports));
5053
+ await verifyCompatibility2(db, pkgs);
5054
+ }
5055
+ const { handleCompat: handleCompat2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
5056
+ const res = await handleCompat2(db, { packages: pkgs });
5057
+ if (opts.json) return console.log(JSON.stringify(res, null, 2));
5058
+ const color = res.overall === "compatible" ? green : res.overall === "conflict" ? red : dim;
5059
+ console.log(`${bold(res.packages.join(" + "))} ${color(res.overall)}`);
5060
+ if (res.conflicts.length) {
5061
+ console.log(
5062
+ table(
5063
+ ["Source", "Detail"],
5064
+ res.conflicts.map((c) => [c.source, c.detail])
5065
+ )
5066
+ );
5067
+ } else if (res.overall === "compatible") {
5068
+ console.log(dim("no peer-dependency or engine conflicts across the set"));
5069
+ }
5070
+ if (res.unverified.length) {
5071
+ console.log(dim(`
5072
+ unverified (no metadata): ${res.unverified.join(", ")}`));
5073
+ }
5074
+ });
5075
+ }
3385
5076
  var init_commands = __esm({
3386
5077
  "src/cli/commands.ts"() {
3387
5078
  "use strict";
@@ -3389,6 +5080,7 @@ var init_commands = __esm({
3389
5080
  init_config();
3390
5081
  init_types();
3391
5082
  init_client();
5083
+ init_packages();
3392
5084
  init_handlers();
3393
5085
  init_weights();
3394
5086
  init_weights();
@@ -3414,9 +5106,9 @@ __export(installSkill_exports, {
3414
5106
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3415
5107
  import { copyFileSync } from "fs";
3416
5108
  import { homedir as homedir3 } from "os";
3417
- import { dirname as dirname4, join as join3 } from "path";
5109
+ import { dirname as dirname4, join as join4 } from "path";
3418
5110
  function home(...p) {
3419
- return join3(homedir3(), ...p);
5111
+ return join4(homedir3(), ...p);
3420
5112
  }
3421
5113
  function agentSpecs() {
3422
5114
  return [
@@ -3550,10 +5242,10 @@ function installAgent(spec, mode) {
3550
5242
  }
3551
5243
  }
3552
5244
  function installInstructionsFile() {
3553
- const src = join3(packageRoot(), "templates", "skill-instructions.md");
5245
+ const src = join4(packageRoot(), "templates", "skill-instructions.md");
3554
5246
  if (!existsSync4(src)) return null;
3555
5247
  const destDir = home(".lurq");
3556
- const dest = join3(destDir, "skill-instructions.md");
5248
+ const dest = join4(destDir, "skill-instructions.md");
3557
5249
  mkdirSync2(destDir, { recursive: true });
3558
5250
  copyFileSync(src, dest);
3559
5251
  return dest;
@@ -3654,9 +5346,11 @@ async function validateKey(url, apiKey) {
3654
5346
  },
3655
5347
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
3656
5348
  });
3657
- return res.ok;
5349
+ if (res.ok) return "valid";
5350
+ if (res.status === 401 || res.status === 403) return "invalid";
5351
+ return "unreachable";
3658
5352
  } catch {
3659
- return false;
5353
+ return "unreachable";
3660
5354
  }
3661
5355
  }
3662
5356
  async function runInstallWizard(opts) {
@@ -3664,7 +5358,7 @@ async function runInstallWizard(opts) {
3664
5358
  const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
3665
5359
  let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
3666
5360
  if (interactive) {
3667
- const { input, checkbox, confirm } = await import("@inquirer/prompts");
5361
+ const { input, checkbox, confirm, select } = await import("@inquirer/prompts");
3668
5362
  console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
3669
5363
  if (!apiKey) {
3670
5364
  console.log(` Need a key? Get one at ${GET_KEY_URL}
@@ -3675,11 +5369,13 @@ async function runInstallWizard(opts) {
3675
5369
  })).trim();
3676
5370
  }
3677
5371
  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) {
5372
+ const check = await validateKey(url, apiKey);
5373
+ console.log(
5374
+ check === "valid" ? "ok" : check === "invalid" ? "rejected" : "could not reach endpoint"
5375
+ );
5376
+ if (check !== "valid") {
3681
5377
  const proceed = await confirm({
3682
- message: `Couldn't validate the key against ${url}. Continue anyway?`,
5378
+ message: check === "invalid" ? `That key was rejected (401) by ${url}. Continue anyway?` : `Couldn't reach ${url} to validate the key. Continue anyway?`,
3683
5379
  default: false
3684
5380
  });
3685
5381
  if (!proceed) {
@@ -3687,22 +5383,45 @@ async function runInstallWizard(opts) {
3687
5383
  return;
3688
5384
  }
3689
5385
  }
3690
- let selected2;
3691
5386
  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
- }))
5387
+ await finish(resolveAgents(opts.agent), { url, apiKey });
5388
+ return;
5389
+ }
5390
+ const specs = agentSpecs();
5391
+ const detected = specs.filter((s) => s.detected);
5392
+ const primary = detected.find((s) => s.id === "claude-code") ?? detected[0];
5393
+ if (primary) {
5394
+ const others = detected.length - 1;
5395
+ const choice = await select({
5396
+ message: others > 0 ? `Looks like you have ${detected.map((s) => s.label).join(", ")}. Connect lurq to them?` : `Looks like you have ${primary.label}. Connect lurq to it?`,
5397
+ default: "yes",
5398
+ choices: [
5399
+ {
5400
+ name: others > 0 ? `Yes \u2014 set up all ${detected.length} detected agents` : `Yes \u2014 set up ${primary.label} for me`,
5401
+ value: "yes"
5402
+ },
5403
+ { name: "No \u2014 let me choose which agent(s)", value: "other" },
5404
+ { name: "Cancel \u2014 change nothing", value: "cancel" }
5405
+ ]
3702
5406
  });
3703
- selected2 = specs.filter((s) => ids.includes(s.id));
5407
+ if (choice === "cancel") {
5408
+ console.log("No problem \u2014 nothing was changed. Run `lurq install` again anytime.");
5409
+ return;
5410
+ }
5411
+ if (choice === "yes") {
5412
+ await finish(detected, { url, apiKey });
5413
+ return;
5414
+ }
3704
5415
  }
3705
- await finish(selected2, { url, apiKey });
5416
+ const ids = await checkbox({
5417
+ message: "Which assistant(s) should I configure?",
5418
+ choices: specs.map((s) => ({
5419
+ name: `${s.label}${s.detected ? " (detected)" : ""}`,
5420
+ value: s.id,
5421
+ checked: s.detected
5422
+ }))
5423
+ });
5424
+ await finish(specs.filter((s) => ids.includes(s.id)), { url, apiKey });
3706
5425
  return;
3707
5426
  }
3708
5427
  if (!apiKey) {
@@ -3742,7 +5461,8 @@ var keys_exports = {};
3742
5461
  __export(keys_exports, {
3743
5462
  runKeysCreate: () => runKeysCreate,
3744
5463
  runKeysList: () => runKeysList,
3745
- runKeysRevoke: () => runKeysRevoke
5464
+ runKeysRevoke: () => runKeysRevoke,
5465
+ runKeysRotate: () => runKeysRotate
3746
5466
  });
3747
5467
  function waitForEnter(prompt) {
3748
5468
  return new Promise((resolve) => {
@@ -3754,28 +5474,51 @@ function waitForEnter(prompt) {
3754
5474
  });
3755
5475
  });
3756
5476
  }
5477
+ async function presentNewKey(key, row, opts) {
5478
+ if (opts.json) {
5479
+ console.log(
5480
+ JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label, ...opts.extraJson })
5481
+ );
5482
+ return;
5483
+ }
5484
+ const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
5485
+ const block = [bold(opts.header ?? "API key created."), "", ` ${green(key)}`, "", dim(meta)];
5486
+ console.log(block.join("\n"));
5487
+ if (process.stdout.isTTY && process.stdin.isTTY) {
5488
+ await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
5489
+ process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
5490
+ console.log(
5491
+ dim(`New key (prefix ${row.prefix}) erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
5492
+ );
5493
+ } else {
5494
+ console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
5495
+ }
5496
+ }
3757
5497
  async function runKeysCreate(opts) {
3758
5498
  requireConfig(["DATABASE_URL"]);
3759
5499
  const { db, close } = createDb({ max: 1 });
3760
5500
  try {
3761
5501
  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 }));
5502
+ await presentNewKey(key, row, opts);
5503
+ } finally {
5504
+ await close();
5505
+ }
5506
+ }
5507
+ async function runKeysRotate(prefixOrId, opts) {
5508
+ requireConfig(["DATABASE_URL"]);
5509
+ const { db, close } = createDb({ max: 1 });
5510
+ try {
5511
+ const result = await rotateKey(db, prefixOrId);
5512
+ if (!result) {
5513
+ logger.warn(`No active key matched "${prefixOrId}".`);
5514
+ process.exitCode = 1;
3764
5515
  return;
3765
5516
  }
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
- }
5517
+ await presentNewKey(result.key, result.row, {
5518
+ json: opts.json,
5519
+ header: `API key rotated \u2014 replaces ${result.previous.prefix} (now revoked).`,
5520
+ extraJson: { replaced: result.previous.prefix }
5521
+ });
3779
5522
  } finally {
3780
5523
  await close();
3781
5524
  }
@@ -3856,7 +5599,7 @@ var init_keys = __esm({
3856
5599
 
3857
5600
  // src/db/seed.ts
3858
5601
  import { readFileSync as readFileSync3 } from "fs";
3859
- import { sql as sql7 } from "drizzle-orm";
5602
+ import { sql as sql5 } from "drizzle-orm";
3860
5603
  import { z as z3 } from "zod";
3861
5604
  function loadSeedFile(path2 = seedJsonPath()) {
3862
5605
  const raw = JSON.parse(readFileSync3(path2, "utf8"));
@@ -3873,7 +5616,7 @@ async function loadSeedPackages(db, path2) {
3873
5616
  await db.insert(seedPackages).values(entries.map((e) => ({ name: e.name, category: e.category ?? null }))).onConflictDoUpdate({
3874
5617
  target: seedPackages.name,
3875
5618
  // Refresh category to the incoming value on conflict (EXCLUDED.category).
3876
- set: { category: sql7`excluded.category` }
5619
+ set: { category: sql5`excluded.category` }
3877
5620
  });
3878
5621
  logger.info(`Loaded ${entries.length} packages into seed_packages.`);
3879
5622
  return entries.length;
@@ -3994,13 +5737,21 @@ function buildProgram() {
3994
5737
  const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
3995
5738
  await runVerify2(pkg, opts);
3996
5739
  });
5740
+ program.command("versions").argument("<package>", "npm package name").description("show the stored version timeline for a package").option("--json", "output JSON instead of a table").option("-n, --limit <n>", "how many versions to show (default 30)").action(async (pkg, opts) => {
5741
+ const { runVersions: runVersions2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5742
+ await runVersions2(pkg, opts);
5743
+ });
5744
+ program.command("plan").argument("<file>", "path to a markdown file describing your program").description("turn a program description into an evidence-scored package plan + roadmap").option("--optimize <mode>", "ranking bias: 'speed' (lightest bundle) or 'balanced'").option("--html <path>", "write the roadmap as a self-contained HTML visualization").option("--open", "render the roadmap to HTML and open it in your browser").option("--json", "output the full plan as JSON").action(async (file, opts) => {
5745
+ const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5746
+ await runPlan2(file, opts);
5747
+ });
3997
5748
  program.command("weights").description("show and explain the scoring weight model (health, quality, composite \u03BB)").option("--json", "output the weight model as JSON").action(async (opts) => {
3998
5749
  const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
3999
5750
  runWeights2(opts);
4000
5751
  });
4001
5752
  program.command("edit-weights").description("override, reset, or explain the scoring weights (layered over defaults)").option("--set <pair>", "override key=value, e.g. composite.lambda=0.5 (repeatable)", (v, acc) => acc.concat(v), []).option("--reset", "remove all overrides and restore defaults").option("--explain <component>", "explain a component (e.g. adoption, quality, lambda)").option("--project", "write to project-local .lurq/weights.json instead of the user config").action(async (opts) => {
4002
5753
  const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
4003
- runEditWeights2(opts);
5754
+ await runEditWeights2(opts);
4004
5755
  });
4005
5756
  program.command("discover").description("operator-side: proactively crawl for new packages and queue/gate them (\xA72B)").option("--cap <n>", "max candidates to fully ingest this run", (v) => parseInt(v, 10)).option("--dry-run", "discover, queue, and gate, but do not ingest survivors").option("--json", "output the discovery summary as JSON").action(async (opts) => {
4006
5757
  const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
@@ -4016,6 +5767,24 @@ function buildProgram() {
4016
5767
  const summary = await runRescore2();
4017
5768
  if (opts.json) console.log(JSON.stringify(summary, null, 2));
4018
5769
  });
5770
+ program.command("watch").description(
5771
+ "operator-side: follow the npm changes feed, re-syncing tracked packages on new releases"
5772
+ ).action(async () => {
5773
+ const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5774
+ await runWatch2();
5775
+ });
5776
+ program.command("sandbox").argument("<package>", "npm package name").argument("[version]", "specific version (default: latest)").description(
5777
+ "operator-side: install + smoke-load a package in a sandbox to verify it actually works"
5778
+ ).option("--esm", "load via ESM import instead of CJS require").option("--allow-scripts", "run install scripts (UNSAFE without VM isolation)").option("--json", "output JSON").action(
5779
+ async (pkg, version, opts) => {
5780
+ const { runSandbox: runSandbox2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5781
+ await runSandbox2(pkg, version, opts);
5782
+ }
5783
+ );
5784
+ program.command("compat").argument("<packages...>", "npm package names to check together").description("check pairwise compatibility of packages from the sandbox matrix").option("--run", "operator-side: co-install them in the sandbox first (UNSAFE without VM isolation)").option("--json", "output JSON").action(async (pkgs, opts) => {
5785
+ const { runCompat: runCompat2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
5786
+ await runCompat2(pkgs, opts);
5787
+ });
4019
5788
  program.command("install").description("guided setup: connect lurq to your AI assistant(s)").option("--api-key <key>", "hosted API key (skips the prompt)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--agent <agent>", "claude-code | cursor | copilot | windsurf | codex | all").option("--yes", "non-interactive: use flags/env and detected agents without prompting").action(async (opts) => {
4020
5789
  const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
4021
5790
  await runInstallWizard2(opts);
@@ -4037,6 +5806,10 @@ function buildProgram() {
4037
5806
  const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4038
5807
  await runKeysList2(opts);
4039
5808
  });
5809
+ keys.command("rotate").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id to replace").description("issue a replacement key (same label/tier) and revoke the old one").option("--json", "print the new key as JSON and skip the interactive erase (for scripts)").action(async (prefixOrId, opts) => {
5810
+ const { runKeysRotate: runKeysRotate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
5811
+ await runKeysRotate2(prefixOrId, opts);
5812
+ });
4040
5813
  keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
4041
5814
  const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
4042
5815
  await runKeysRevoke2(prefixOrId);