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