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