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