lurqrun 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -78
- package/dist/bin/lurq.js +3509 -440
- package/dist/bin/lurq.js.map +1 -1
- package/dist/index.d.ts +102 -6
- package/dist/index.js +3428 -433
- package/dist/index.js.map +1 -1
- package/drizzle/0001_aspiring_timeslip.sql +15 -0
- package/drizzle/0002_lyrical_lila_cheney.sql +14 -0
- 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/meta/0001_snapshot.json +482 -0
- package/drizzle/meta/0002_snapshot.json +575 -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/_journal.json +56 -0
- package/package.json +10 -1
package/dist/index.js
CHANGED
|
@@ -70,14 +70,16 @@ var init_types = __esm({
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
// src/core/constants.ts
|
|
73
|
-
var SERVER_NAME, PACKAGE_NAME, VERSION, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
|
|
73
|
+
var SERVER_NAME, PACKAGE_NAME, VERSION, DEFAULT_ENDPOINT, API_KEY_PREFIX, EMBEDDING_DIM, STALENESS_DAYS, HOUR, DAY, CACHE_TTL;
|
|
74
74
|
var init_constants = __esm({
|
|
75
75
|
"src/core/constants.ts"() {
|
|
76
76
|
"use strict";
|
|
77
77
|
init_esm_shims();
|
|
78
78
|
SERVER_NAME = "lurq";
|
|
79
79
|
PACKAGE_NAME = "lurqrun";
|
|
80
|
-
VERSION = "0.0.
|
|
80
|
+
VERSION = "0.0.2";
|
|
81
|
+
DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
|
|
82
|
+
API_KEY_PREFIX = "lurq_live_";
|
|
81
83
|
EMBEDDING_DIM = 1536;
|
|
82
84
|
STALENESS_DAYS = 7;
|
|
83
85
|
HOUR = 60 * 60 * 1e3;
|
|
@@ -93,6 +95,14 @@ var init_constants = __esm({
|
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
// src/core/config.ts
|
|
98
|
+
var config_exports = {};
|
|
99
|
+
__export(config_exports, {
|
|
100
|
+
ConfigError: () => ConfigError,
|
|
101
|
+
getConfig: () => getConfig,
|
|
102
|
+
loadEnv: () => loadEnv,
|
|
103
|
+
requireConfig: () => requireConfig,
|
|
104
|
+
resetConfigCache: () => resetConfigCache
|
|
105
|
+
});
|
|
96
106
|
import { config as dotenvConfig } from "dotenv";
|
|
97
107
|
import { z } from "zod";
|
|
98
108
|
function loadEnv() {
|
|
@@ -112,6 +122,9 @@ ${issues}`);
|
|
|
112
122
|
cached = parsed.data;
|
|
113
123
|
return cached;
|
|
114
124
|
}
|
|
125
|
+
function resetConfigCache() {
|
|
126
|
+
cached = void 0;
|
|
127
|
+
}
|
|
115
128
|
function requireConfig(keys) {
|
|
116
129
|
const config = getConfig();
|
|
117
130
|
const missing = keys.filter((k) => config[k] === void 0 || config[k] === "");
|
|
@@ -139,7 +152,18 @@ var init_config = __esm({
|
|
|
139
152
|
SUMMARY_API_KEY: z.string().min(1).optional(),
|
|
140
153
|
SUMMARY_MODEL: z.string().min(1).default("gpt-4o-mini"),
|
|
141
154
|
LURQ_SYNC_CONCURRENCY: z.coerce.number().int().positive().max(50).default(5),
|
|
142
|
-
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info")
|
|
155
|
+
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
|
|
156
|
+
// Hosted HTTP service (`serve-http`). Server-side only.
|
|
157
|
+
PORT: z.coerce.number().int().positive().default(8080),
|
|
158
|
+
/** Per-API-key rate limit: max requests per window. */
|
|
159
|
+
LURQ_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(120),
|
|
160
|
+
/** Coarser per-IP rate limit (blunts unauthenticated floods before auth). */
|
|
161
|
+
LURQ_IP_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(240),
|
|
162
|
+
/** Rate-limit window, milliseconds (applies to both limiters). */
|
|
163
|
+
LURQ_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(6e4),
|
|
164
|
+
// Client-side (install wizard / CLI talking to a remote endpoint).
|
|
165
|
+
LURQ_ENDPOINT: z.string().url().optional(),
|
|
166
|
+
LURQ_API_KEY: z.string().min(1).optional()
|
|
143
167
|
});
|
|
144
168
|
ConfigError = class extends Error {
|
|
145
169
|
constructor(message) {
|
|
@@ -188,29 +212,44 @@ var init_logger = __esm({
|
|
|
188
212
|
// src/db/schema.ts
|
|
189
213
|
var schema_exports = {};
|
|
190
214
|
__export(schema_exports, {
|
|
215
|
+
apiKeys: () => apiKeys,
|
|
216
|
+
compatEdges: () => compatEdges,
|
|
217
|
+
discoveryQueue: () => discoveryQueue,
|
|
218
|
+
packageVersions: () => packageVersions,
|
|
191
219
|
packages: () => packages,
|
|
192
220
|
seedPackages: () => seedPackages,
|
|
193
|
-
syncRuns: () => syncRuns
|
|
221
|
+
syncRuns: () => syncRuns,
|
|
222
|
+
verificationRuns: () => verificationRuns,
|
|
223
|
+
watchState: () => watchState
|
|
194
224
|
});
|
|
225
|
+
import { sql } from "drizzle-orm";
|
|
195
226
|
import {
|
|
196
227
|
bigint,
|
|
197
228
|
boolean,
|
|
229
|
+
customType,
|
|
198
230
|
index,
|
|
199
231
|
integer,
|
|
200
232
|
jsonb,
|
|
201
233
|
pgTable,
|
|
234
|
+
primaryKey,
|
|
202
235
|
real,
|
|
203
236
|
serial,
|
|
204
237
|
text,
|
|
205
238
|
timestamp,
|
|
239
|
+
uniqueIndex,
|
|
206
240
|
vector
|
|
207
241
|
} from "drizzle-orm/pg-core";
|
|
208
|
-
var ts, packages, syncRuns, seedPackages;
|
|
242
|
+
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges;
|
|
209
243
|
var init_schema = __esm({
|
|
210
244
|
"src/db/schema.ts"() {
|
|
211
245
|
"use strict";
|
|
212
246
|
init_esm_shims();
|
|
213
247
|
init_constants();
|
|
248
|
+
tsvector = customType({
|
|
249
|
+
dataType() {
|
|
250
|
+
return "tsvector";
|
|
251
|
+
}
|
|
252
|
+
});
|
|
214
253
|
ts = (name) => timestamp(name, { withTimezone: true, mode: "date" });
|
|
215
254
|
packages = pgTable(
|
|
216
255
|
"packages",
|
|
@@ -220,6 +259,8 @@ var init_schema = __esm({
|
|
|
220
259
|
ecosystem: text("ecosystem").notNull().default("npm"),
|
|
221
260
|
// Classification + descriptive text
|
|
222
261
|
category: text("category").$type(),
|
|
262
|
+
/** Whether `category` was hand-curated or inferred at ingest (§2A). */
|
|
263
|
+
categorySource: text("category_source").$type(),
|
|
223
264
|
description: text("description"),
|
|
224
265
|
summary: text("summary"),
|
|
225
266
|
repoUrl: text("repo_url"),
|
|
@@ -235,7 +276,6 @@ var init_schema = __esm({
|
|
|
235
276
|
// Adoption signals
|
|
236
277
|
weeklyDownloads: bigint("weekly_downloads", { mode: "number" }),
|
|
237
278
|
downloadGrowth90d: real("download_growth_90d"),
|
|
238
|
-
dependentsCount: integer("dependents_count"),
|
|
239
279
|
stars: integer("stars"),
|
|
240
280
|
openIssues: integer("open_issues"),
|
|
241
281
|
closedIssues: integer("closed_issues"),
|
|
@@ -243,12 +283,30 @@ var init_schema = __esm({
|
|
|
243
283
|
scorecard: real("scorecard"),
|
|
244
284
|
bundleMinGzipKb: real("bundle_min_gzip_kb"),
|
|
245
285
|
advisories: jsonb("advisories").$type(),
|
|
286
|
+
// Compatibility metadata (Tier-1): declared peer-deps + engines of the
|
|
287
|
+
// latest version, so a whole-stack peer-range check is one indexed query.
|
|
288
|
+
peerDependencies: jsonb("peer_dependencies").$type(),
|
|
289
|
+
peerDependenciesMeta: jsonb("peer_dependencies_meta").$type(),
|
|
290
|
+
engines: jsonb("engines").$type(),
|
|
246
291
|
// Computed outputs
|
|
247
292
|
healthScore: integer("health_score"),
|
|
293
|
+
/** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
|
|
294
|
+
* ranking time (composite); never folded into health_score itself. */
|
|
295
|
+
qualityScore: integer("quality_score"),
|
|
248
296
|
confidence: text("confidence").$type(),
|
|
249
297
|
scoreBreakdown: jsonb("score_breakdown").$type(),
|
|
250
298
|
usageGuide: jsonb("usage_guide").$type(),
|
|
251
299
|
embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
|
|
300
|
+
// Identity of the vector space `embedding` was produced in (e.g.
|
|
301
|
+
// `openai:text-embedding-3-small`, `local`). Vector search filters on the
|
|
302
|
+
// active provider so switching models can't compare incompatible spaces.
|
|
303
|
+
embeddingProvider: text("embedding_provider"),
|
|
304
|
+
// Lexical search vector (§3): name weighted highest (A), then category (B),
|
|
305
|
+
// then summary/description (C). Generated + STORED so it stays in sync with
|
|
306
|
+
// the row automatically; indexed with GIN for fast `@@` matching.
|
|
307
|
+
searchVector: tsvector("search_vector").generatedAlwaysAs(
|
|
308
|
+
sql`setweight(to_tsvector('english', coalesce(name, '')), 'A') || setweight(to_tsvector('english', coalesce(category, '')), 'B') || setweight(to_tsvector('english', coalesce(summary, description, '')), 'C')`
|
|
309
|
+
),
|
|
252
310
|
// Freshness + bookkeeping
|
|
253
311
|
dataAsOf: timestamp("data_as_of", { withTimezone: true, mode: "date" }),
|
|
254
312
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
@@ -258,7 +316,9 @@ var init_schema = __esm({
|
|
|
258
316
|
index("packages_category_idx").on(table2.category),
|
|
259
317
|
index("packages_health_score_idx").on(table2.healthScore),
|
|
260
318
|
// pgvector HNSW index for cosine similarity search (§11).
|
|
261
|
-
index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops"))
|
|
319
|
+
index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops")),
|
|
320
|
+
// GIN index for lexical full-text search (§3).
|
|
321
|
+
index("packages_search_vector_idx").using("gin", table2.searchVector)
|
|
262
322
|
]
|
|
263
323
|
);
|
|
264
324
|
syncRuns = pgTable("sync_runs", {
|
|
@@ -275,6 +335,93 @@ var init_schema = __esm({
|
|
|
275
335
|
category: text("category").$type(),
|
|
276
336
|
addedAt: ts("added_at").notNull().defaultNow()
|
|
277
337
|
});
|
|
338
|
+
discoveryQueue = pgTable(
|
|
339
|
+
"discovery_queue",
|
|
340
|
+
{
|
|
341
|
+
id: serial("id").primaryKey(),
|
|
342
|
+
name: text("name").notNull().unique(),
|
|
343
|
+
discoveredVia: text("discovered_via").$type().notNull(),
|
|
344
|
+
/** Lightweight quality-only pre-score (§2B). Null until the gate runs. */
|
|
345
|
+
preScore: integer("pre_score"),
|
|
346
|
+
status: text("status").$type().notNull().default("pending"),
|
|
347
|
+
discoveredAt: ts("discovered_at").notNull().defaultNow()
|
|
348
|
+
},
|
|
349
|
+
(table2) => [index("discovery_queue_status_idx").on(table2.status)]
|
|
350
|
+
);
|
|
351
|
+
apiKeys = pgTable(
|
|
352
|
+
"api_keys",
|
|
353
|
+
{
|
|
354
|
+
id: serial("id").primaryKey(),
|
|
355
|
+
/** sha256 hex of the full key — the only form persisted. */
|
|
356
|
+
keyHash: text("key_hash").notNull().unique(),
|
|
357
|
+
/** Identifiable display prefix, e.g. `lurq_live_ab12cd` (safe to show/log). */
|
|
358
|
+
prefix: text("prefix").notNull(),
|
|
359
|
+
/** Free-text label (owner / org / purpose). */
|
|
360
|
+
label: text("label"),
|
|
361
|
+
/** Reserved for self-serve issuance: maps to a Clerk user id. */
|
|
362
|
+
ownerId: text("owner_id"),
|
|
363
|
+
tier: text("tier").notNull().default("free"),
|
|
364
|
+
createdAt: ts("created_at").notNull().defaultNow(),
|
|
365
|
+
lastUsedAt: ts("last_used_at"),
|
|
366
|
+
revokedAt: ts("revoked_at")
|
|
367
|
+
},
|
|
368
|
+
(table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
|
|
369
|
+
);
|
|
370
|
+
packageVersions = pgTable(
|
|
371
|
+
"package_versions",
|
|
372
|
+
{
|
|
373
|
+
packageName: text("package_name").notNull(),
|
|
374
|
+
version: text("version").notNull(),
|
|
375
|
+
publishedAt: ts("published_at")
|
|
376
|
+
},
|
|
377
|
+
(table2) => [
|
|
378
|
+
primaryKey({ columns: [table2.packageName, table2.version] }),
|
|
379
|
+
index("package_versions_name_published_idx").on(table2.packageName, table2.publishedAt)
|
|
380
|
+
]
|
|
381
|
+
);
|
|
382
|
+
watchState = pgTable("watch_state", {
|
|
383
|
+
id: text("id").primaryKey(),
|
|
384
|
+
seq: text("seq").notNull(),
|
|
385
|
+
updatedAt: ts("updated_at")
|
|
386
|
+
});
|
|
387
|
+
verificationRuns = pgTable(
|
|
388
|
+
"verification_runs",
|
|
389
|
+
{
|
|
390
|
+
id: serial("id").primaryKey(),
|
|
391
|
+
packageName: text("package_name").notNull(),
|
|
392
|
+
version: text("version").notNull(),
|
|
393
|
+
driver: text("driver").notNull(),
|
|
394
|
+
moduleSystem: text("module_system").notNull(),
|
|
395
|
+
installed: boolean("installed").notNull(),
|
|
396
|
+
imported: boolean("imported"),
|
|
397
|
+
ranScripts: boolean("ran_scripts").notNull().default(false),
|
|
398
|
+
durationMs: integer("duration_ms"),
|
|
399
|
+
error: text("error"),
|
|
400
|
+
ranAt: ts("ran_at")
|
|
401
|
+
},
|
|
402
|
+
(table2) => [index("verification_runs_pkg_idx").on(table2.packageName, table2.version)]
|
|
403
|
+
);
|
|
404
|
+
compatEdges = pgTable(
|
|
405
|
+
"compat_edges",
|
|
406
|
+
{
|
|
407
|
+
id: serial("id").primaryKey(),
|
|
408
|
+
packageA: text("package_a").notNull(),
|
|
409
|
+
versionA: text("version_a").notNull(),
|
|
410
|
+
packageB: text("package_b").notNull(),
|
|
411
|
+
versionB: text("version_b").notNull(),
|
|
412
|
+
status: text("status").$type().notNull(),
|
|
413
|
+
driver: text("driver").notNull(),
|
|
414
|
+
ranAt: ts("ran_at")
|
|
415
|
+
},
|
|
416
|
+
(table2) => [
|
|
417
|
+
uniqueIndex("compat_edges_pair_idx").on(
|
|
418
|
+
table2.packageA,
|
|
419
|
+
table2.versionA,
|
|
420
|
+
table2.packageB,
|
|
421
|
+
table2.versionB
|
|
422
|
+
)
|
|
423
|
+
]
|
|
424
|
+
);
|
|
278
425
|
}
|
|
279
426
|
});
|
|
280
427
|
|
|
@@ -297,6 +444,131 @@ var init_client = __esm({
|
|
|
297
444
|
}
|
|
298
445
|
});
|
|
299
446
|
|
|
447
|
+
// src/core/cache.ts
|
|
448
|
+
var cache_exports = {};
|
|
449
|
+
__export(cache_exports, {
|
|
450
|
+
cached: () => cached2,
|
|
451
|
+
invalidateCache: () => invalidateCache
|
|
452
|
+
});
|
|
453
|
+
function ttlSeconds() {
|
|
454
|
+
const n = Number(process.env.LURQ_CACHE_TTL_SEC);
|
|
455
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_TTL_SEC;
|
|
456
|
+
}
|
|
457
|
+
async function getClient() {
|
|
458
|
+
if (!process.env.REDIS_URL) return null;
|
|
459
|
+
if (!clientPromise) {
|
|
460
|
+
clientPromise = (async () => {
|
|
461
|
+
try {
|
|
462
|
+
const { default: Redis } = await import("ioredis");
|
|
463
|
+
const client = new Redis(process.env.REDIS_URL, {
|
|
464
|
+
maxRetriesPerRequest: 1,
|
|
465
|
+
enableOfflineQueue: false,
|
|
466
|
+
lazyConnect: false,
|
|
467
|
+
// Railway's private network (redis.railway.internal) is IPv6-only and
|
|
468
|
+
// ioredis defaults to IPv4 — family:0 lets it resolve either stack, so
|
|
469
|
+
// both the private URL and a public/Upstash URL work unchanged.
|
|
470
|
+
family: 0
|
|
471
|
+
});
|
|
472
|
+
client.on("error", (err) => logger.warn(`redis: ${err.message}`));
|
|
473
|
+
logger.info("Response cache enabled (REDIS_URL set).");
|
|
474
|
+
return client;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
logger.warn(`redis disabled (init failed): ${err.message}`);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
})();
|
|
480
|
+
}
|
|
481
|
+
return clientPromise;
|
|
482
|
+
}
|
|
483
|
+
async function namespaceVersion(client) {
|
|
484
|
+
if (versionMemo && Date.now() - versionMemo.at < VERSION_MEMO_MS) return versionMemo.value;
|
|
485
|
+
const value = await client.get(VERSION_KEY).catch(() => null) ?? "0";
|
|
486
|
+
versionMemo = { value, at: Date.now() };
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
async function cached2(namespace, key, compute, opts = {}) {
|
|
490
|
+
const client = await getClient();
|
|
491
|
+
if (!client) return compute();
|
|
492
|
+
let fullKey;
|
|
493
|
+
try {
|
|
494
|
+
const version = await namespaceVersion(client);
|
|
495
|
+
fullKey = `lurq:${namespace}:${version}:${key}`;
|
|
496
|
+
const hit = await client.get(fullKey);
|
|
497
|
+
if (hit != null) return JSON.parse(hit);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
logger.warn(`redis read failed, bypassing cache: ${err.message}`);
|
|
500
|
+
return compute();
|
|
501
|
+
}
|
|
502
|
+
const value = await compute();
|
|
503
|
+
if (!opts.skipCache?.(value)) {
|
|
504
|
+
client.set(fullKey, JSON.stringify(value), "EX", ttlSeconds()).catch(() => {
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
async function invalidateCache() {
|
|
510
|
+
const client = await getClient();
|
|
511
|
+
if (!client) return;
|
|
512
|
+
try {
|
|
513
|
+
await client.incr(VERSION_KEY);
|
|
514
|
+
versionMemo = null;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
logger.warn(`redis invalidate failed: ${err.message}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
var VERSION_KEY, DEFAULT_TTL_SEC, VERSION_MEMO_MS, clientPromise, versionMemo;
|
|
520
|
+
var init_cache = __esm({
|
|
521
|
+
"src/core/cache.ts"() {
|
|
522
|
+
"use strict";
|
|
523
|
+
init_esm_shims();
|
|
524
|
+
init_logger();
|
|
525
|
+
VERSION_KEY = "lurq:cachever";
|
|
526
|
+
DEFAULT_TTL_SEC = 3600;
|
|
527
|
+
VERSION_MEMO_MS = 1e4;
|
|
528
|
+
clientPromise = null;
|
|
529
|
+
versionMemo = null;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// src/db/compat.ts
|
|
534
|
+
import { and, inArray } from "drizzle-orm";
|
|
535
|
+
async function getCompatMetadata(db, names) {
|
|
536
|
+
if (names.length === 0) return [];
|
|
537
|
+
return db.select({
|
|
538
|
+
name: packages.name,
|
|
539
|
+
latestVersion: packages.latestVersion,
|
|
540
|
+
peerDependencies: packages.peerDependencies,
|
|
541
|
+
peerDependenciesMeta: packages.peerDependenciesMeta,
|
|
542
|
+
engines: packages.engines
|
|
543
|
+
}).from(packages).where(inArray(packages.name, names));
|
|
544
|
+
}
|
|
545
|
+
function canonicalPair(a, b) {
|
|
546
|
+
const [low, high] = a.name <= b.name ? [a, b] : [b, a];
|
|
547
|
+
return { packageA: low.name, versionA: low.version, packageB: high.name, versionB: high.version };
|
|
548
|
+
}
|
|
549
|
+
async function upsertCompatEdge(db, edge) {
|
|
550
|
+
await db.insert(compatEdges).values(edge).onConflictDoUpdate({
|
|
551
|
+
target: [
|
|
552
|
+
compatEdges.packageA,
|
|
553
|
+
compatEdges.versionA,
|
|
554
|
+
compatEdges.packageB,
|
|
555
|
+
compatEdges.versionB
|
|
556
|
+
],
|
|
557
|
+
set: { status: edge.status, driver: edge.driver, ranAt: edge.ranAt }
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
async function getCompatEdges(db, names) {
|
|
561
|
+
if (names.length === 0) return [];
|
|
562
|
+
return db.select().from(compatEdges).where(and(inArray(compatEdges.packageA, names), inArray(compatEdges.packageB, names)));
|
|
563
|
+
}
|
|
564
|
+
var init_compat = __esm({
|
|
565
|
+
"src/db/compat.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
init_esm_shims();
|
|
568
|
+
init_schema();
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
300
572
|
// src/core/http.ts
|
|
301
573
|
import { createHash } from "crypto";
|
|
302
574
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -361,9 +633,9 @@ async function httpRequest(url, opts) {
|
|
|
361
633
|
} = opts;
|
|
362
634
|
const key = opts.cacheKey ?? `${method} ${url} ${body ?? ""}`;
|
|
363
635
|
if (ttlMs > 0 && !bypassCacheRead) {
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
return { status:
|
|
636
|
+
const cached3 = await readCache(key, ttlMs);
|
|
637
|
+
if (cached3) {
|
|
638
|
+
return { status: cached3.status, data: decode(cached3.body, accept), fromCache: true };
|
|
367
639
|
}
|
|
368
640
|
}
|
|
369
641
|
const limiter = limiterFor(host);
|
|
@@ -530,8 +802,9 @@ function parseNpmRegistry(json2) {
|
|
|
530
802
|
const { repoUrl, repo } = normalizeRepoUrl(repository, homepage);
|
|
531
803
|
const deprecated = Boolean(latestManifest?.deprecated ?? json2?.deprecated);
|
|
532
804
|
const maintainers = Array.isArray(json2?.maintainers) ? json2.maintainers.length : null;
|
|
805
|
+
const name = json2?.name ?? "";
|
|
533
806
|
return {
|
|
534
|
-
name
|
|
807
|
+
name,
|
|
535
808
|
description: json2?.description ?? latestManifest?.description ?? null,
|
|
536
809
|
latestVersion: latest,
|
|
537
810
|
license: pickLicense(json2?.license ?? latestManifest?.license),
|
|
@@ -542,9 +815,76 @@ function parseNpmRegistry(json2) {
|
|
|
542
815
|
lastReleaseAt: toDate(latest ? time?.[latest] : null) ?? toDate(time?.modified),
|
|
543
816
|
deprecated,
|
|
544
817
|
maintainersCount: maintainers,
|
|
545
|
-
readme: typeof json2?.readme === "string" ? json2.readme : null
|
|
818
|
+
readme: typeof json2?.readme === "string" ? json2.readme : null,
|
|
819
|
+
keywords: parseKeywords(json2?.keywords ?? latestManifest?.keywords),
|
|
820
|
+
hasTypes: detectTypes(name, latestManifest),
|
|
821
|
+
hasTestScript: detectTestScript(latestManifest),
|
|
822
|
+
directDependenciesCount: countDeps(latestManifest?.dependencies),
|
|
823
|
+
hasProvenance: detectProvenance(latestManifest),
|
|
824
|
+
hasInstallScripts: detectInstallScripts(latestManifest),
|
|
825
|
+
peerDependencies: parseDepMap(latestManifest?.peerDependencies),
|
|
826
|
+
peerDependenciesMeta: parsePeerMeta(latestManifest?.peerDependenciesMeta),
|
|
827
|
+
engines: parseDepMap(latestManifest?.engines),
|
|
828
|
+
versionTimeline: parseVersionTimeline(json2)
|
|
546
829
|
};
|
|
547
830
|
}
|
|
831
|
+
function parseDepMap(value) {
|
|
832
|
+
if (!value || typeof value !== "object") return null;
|
|
833
|
+
const out = {};
|
|
834
|
+
for (const [name, range] of Object.entries(value)) {
|
|
835
|
+
if (typeof range === "string" && range.trim() !== "") out[name] = range;
|
|
836
|
+
}
|
|
837
|
+
return Object.keys(out).length ? out : null;
|
|
838
|
+
}
|
|
839
|
+
function parsePeerMeta(value) {
|
|
840
|
+
if (!value || typeof value !== "object") return null;
|
|
841
|
+
const out = {};
|
|
842
|
+
for (const [name, meta] of Object.entries(value)) {
|
|
843
|
+
if (meta && typeof meta === "object" && "optional" in meta) {
|
|
844
|
+
out[name] = { optional: Boolean(meta.optional) };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return Object.keys(out).length ? out : null;
|
|
848
|
+
}
|
|
849
|
+
function detectInstallScripts(manifest) {
|
|
850
|
+
const scripts = manifest?.scripts;
|
|
851
|
+
if (!scripts || typeof scripts !== "object") return false;
|
|
852
|
+
return ["preinstall", "install", "postinstall"].some(
|
|
853
|
+
(hook) => typeof scripts[hook] === "string" && scripts[hook].trim() !== ""
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
function parseVersionTimeline(json2) {
|
|
857
|
+
const versions = json2?.versions;
|
|
858
|
+
if (!versions || typeof versions !== "object") return [];
|
|
859
|
+
const time = json2?.time ?? {};
|
|
860
|
+
return Object.keys(versions).map((version) => ({ version, publishedAt: toDate(time?.[version]) })).sort((a, b) => (b.publishedAt?.getTime() ?? 0) - (a.publishedAt?.getTime() ?? 0));
|
|
861
|
+
}
|
|
862
|
+
function parseKeywords(value) {
|
|
863
|
+
if (!Array.isArray(value)) return [];
|
|
864
|
+
return value.filter((k) => typeof k === "string");
|
|
865
|
+
}
|
|
866
|
+
function detectTypes(name, manifest) {
|
|
867
|
+
if (typeof manifest?.types === "string" || typeof manifest?.typings === "string") return true;
|
|
868
|
+
if (name.startsWith("@types/")) return true;
|
|
869
|
+
const exportsField = manifest?.exports;
|
|
870
|
+
if (exportsField && typeof exportsField === "object") {
|
|
871
|
+
const serialized = JSON.stringify(exportsField);
|
|
872
|
+
if (serialized.includes('"types"')) return true;
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
function detectTestScript(manifest) {
|
|
877
|
+
const test = manifest?.scripts?.test;
|
|
878
|
+
if (typeof test !== "string" || test.trim() === "") return false;
|
|
879
|
+
return !/no test specified/i.test(test);
|
|
880
|
+
}
|
|
881
|
+
function countDeps(deps) {
|
|
882
|
+
if (!deps || typeof deps !== "object") return 0;
|
|
883
|
+
return Object.keys(deps).length;
|
|
884
|
+
}
|
|
885
|
+
function detectProvenance(manifest) {
|
|
886
|
+
return Boolean(manifest?.dist?.attestations);
|
|
887
|
+
}
|
|
548
888
|
async function fetchNpmRegistry(name, fetchImpl) {
|
|
549
889
|
const url = `https://${HOST}/${encodeNpmName(name)}`;
|
|
550
890
|
const { data } = await httpGetJson(url, {
|
|
@@ -582,6 +922,32 @@ var init_npmRegistry = __esm({
|
|
|
582
922
|
}
|
|
583
923
|
});
|
|
584
924
|
|
|
925
|
+
// src/ingestion/sources/npmSearch.ts
|
|
926
|
+
async function searchNpm(query, size = 20, fetchImpl) {
|
|
927
|
+
const url = `https://${HOST2}/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`;
|
|
928
|
+
try {
|
|
929
|
+
const { data } = await httpGetJson(url, {
|
|
930
|
+
host: HOST2,
|
|
931
|
+
ttlMs: CACHE_TTL.npmRegistry,
|
|
932
|
+
fetchImpl
|
|
933
|
+
});
|
|
934
|
+
const objects = Array.isArray(data?.objects) ? data.objects : [];
|
|
935
|
+
return objects.map((o) => ({ name: o?.package?.name, date: o?.package?.date ?? null })).filter((h) => typeof h.name === "string");
|
|
936
|
+
} catch {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
var HOST2;
|
|
941
|
+
var init_npmSearch = __esm({
|
|
942
|
+
"src/ingestion/sources/npmSearch.ts"() {
|
|
943
|
+
"use strict";
|
|
944
|
+
init_esm_shims();
|
|
945
|
+
init_constants();
|
|
946
|
+
init_http();
|
|
947
|
+
HOST2 = "registry.npmjs.org";
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
585
951
|
// src/ingestion/sources/npmDownloads.ts
|
|
586
952
|
function parseWeeklyDownloads(json2) {
|
|
587
953
|
const value = json2?.downloads;
|
|
@@ -627,8 +993,8 @@ function chunk(items, size) {
|
|
|
627
993
|
async function fetchWeeklyDownloads(name, fetchImpl) {
|
|
628
994
|
try {
|
|
629
995
|
const { data } = await httpGetJson(
|
|
630
|
-
`https://${
|
|
631
|
-
{ host:
|
|
996
|
+
`https://${HOST3}/downloads/point/last-week/${name}`,
|
|
997
|
+
{ host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
|
|
632
998
|
);
|
|
633
999
|
return parseWeeklyDownloads(data);
|
|
634
1000
|
} catch (err) {
|
|
@@ -639,8 +1005,8 @@ async function fetchWeeklyDownloads(name, fetchImpl) {
|
|
|
639
1005
|
async function fetchDownloadGrowth(name, fetchImpl) {
|
|
640
1006
|
try {
|
|
641
1007
|
const { data } = await httpGetJson(
|
|
642
|
-
`https://${
|
|
643
|
-
{ host:
|
|
1008
|
+
`https://${HOST3}/downloads/range/${last90DayRange()}/${name}`,
|
|
1009
|
+
{ host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
|
|
644
1010
|
);
|
|
645
1011
|
return parseDownloadGrowth(data);
|
|
646
1012
|
} catch {
|
|
@@ -654,8 +1020,8 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
|
|
|
654
1020
|
for (const batch of chunk(unscoped, BULK_CHUNK)) {
|
|
655
1021
|
try {
|
|
656
1022
|
const { data } = await httpGetJson(
|
|
657
|
-
`https://${
|
|
658
|
-
{ host:
|
|
1023
|
+
`https://${HOST3}/downloads/point/last-week/${batch.join(",")}`,
|
|
1024
|
+
{ host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
|
|
659
1025
|
);
|
|
660
1026
|
for (const [k, v] of parseBulkWeekly(data, batch)) result.set(k, v);
|
|
661
1027
|
} catch {
|
|
@@ -669,14 +1035,14 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
|
|
|
669
1035
|
}
|
|
670
1036
|
return result;
|
|
671
1037
|
}
|
|
672
|
-
var
|
|
1038
|
+
var HOST3, BULK_CHUNK;
|
|
673
1039
|
var init_npmDownloads = __esm({
|
|
674
1040
|
"src/ingestion/sources/npmDownloads.ts"() {
|
|
675
1041
|
"use strict";
|
|
676
1042
|
init_esm_shims();
|
|
677
1043
|
init_constants();
|
|
678
1044
|
init_http();
|
|
679
|
-
|
|
1045
|
+
HOST3 = "api.npmjs.org";
|
|
680
1046
|
BULK_CHUNK = 128;
|
|
681
1047
|
}
|
|
682
1048
|
});
|
|
@@ -700,7 +1066,7 @@ function parseGithub(json2, now = /* @__PURE__ */ new Date()) {
|
|
|
700
1066
|
async function fetchGithubRepo(owner, repo, token, fetchImpl) {
|
|
701
1067
|
const body = JSON.stringify({ query: QUERY, variables: { owner, name: repo } });
|
|
702
1068
|
const { data } = await httpRequest(ENDPOINT, {
|
|
703
|
-
host:
|
|
1069
|
+
host: HOST4,
|
|
704
1070
|
ttlMs: CACHE_TTL.github,
|
|
705
1071
|
method: "POST",
|
|
706
1072
|
headers: {
|
|
@@ -715,15 +1081,15 @@ async function fetchGithubRepo(owner, repo, token, fetchImpl) {
|
|
|
715
1081
|
});
|
|
716
1082
|
return parseGithub(data);
|
|
717
1083
|
}
|
|
718
|
-
var
|
|
1084
|
+
var HOST4, ENDPOINT, QUERY, ONE_YEAR_MS;
|
|
719
1085
|
var init_github = __esm({
|
|
720
1086
|
"src/ingestion/sources/github.ts"() {
|
|
721
1087
|
"use strict";
|
|
722
1088
|
init_esm_shims();
|
|
723
1089
|
init_constants();
|
|
724
1090
|
init_http();
|
|
725
|
-
|
|
726
|
-
ENDPOINT = `https://${
|
|
1091
|
+
HOST4 = "api.github.com";
|
|
1092
|
+
ENDPOINT = `https://${HOST4}/graphql`;
|
|
727
1093
|
QUERY = `query($owner:String!, $name:String!) {
|
|
728
1094
|
repository(owner:$owner, name:$name) {
|
|
729
1095
|
stargazerCount
|
|
@@ -769,10 +1135,10 @@ function parseAdvisoryDetail(json2) {
|
|
|
769
1135
|
}
|
|
770
1136
|
async function fetchScorecard(owner, repo, fetchImpl) {
|
|
771
1137
|
const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
|
|
772
|
-
const url = `https://${
|
|
1138
|
+
const url = `https://${HOST5}/v3/projects/${projectKey}`;
|
|
773
1139
|
try {
|
|
774
1140
|
const { data } = await httpGetJson(url, {
|
|
775
|
-
host:
|
|
1141
|
+
host: HOST5,
|
|
776
1142
|
ttlMs: CACHE_TTL.depsDev,
|
|
777
1143
|
fetchImpl
|
|
778
1144
|
});
|
|
@@ -782,11 +1148,11 @@ async function fetchScorecard(owner, repo, fetchImpl) {
|
|
|
782
1148
|
}
|
|
783
1149
|
}
|
|
784
1150
|
async function fetchAdvisories(name, version, fetchImpl) {
|
|
785
|
-
const versionUrl = `https://${
|
|
1151
|
+
const versionUrl = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
|
|
786
1152
|
let keys = [];
|
|
787
1153
|
try {
|
|
788
1154
|
const { data } = await httpGetJson(versionUrl, {
|
|
789
|
-
host:
|
|
1155
|
+
host: HOST5,
|
|
790
1156
|
ttlMs: CACHE_TTL.depsDev,
|
|
791
1157
|
fetchImpl
|
|
792
1158
|
});
|
|
@@ -796,8 +1162,8 @@ async function fetchAdvisories(name, version, fetchImpl) {
|
|
|
796
1162
|
}
|
|
797
1163
|
const details = await Promise.allSettled(
|
|
798
1164
|
keys.map(
|
|
799
|
-
(key) => httpGetJson(`https://${
|
|
800
|
-
host:
|
|
1165
|
+
(key) => httpGetJson(`https://${HOST5}/v3/advisories/${encodeURIComponent(key)}`, {
|
|
1166
|
+
host: HOST5,
|
|
801
1167
|
ttlMs: CACHE_TTL.depsDev,
|
|
802
1168
|
fetchImpl
|
|
803
1169
|
})
|
|
@@ -807,6 +1173,21 @@ async function fetchAdvisories(name, version, fetchImpl) {
|
|
|
807
1173
|
(d) => d.status === "fulfilled"
|
|
808
1174
|
).map((d) => parseAdvisoryDetail(d.value.data));
|
|
809
1175
|
}
|
|
1176
|
+
async function fetchDependencyNames(name, version, fetchImpl) {
|
|
1177
|
+
const url = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}:dependencies`;
|
|
1178
|
+
try {
|
|
1179
|
+
const { data } = await httpGetJson(url, {
|
|
1180
|
+
host: HOST5,
|
|
1181
|
+
ttlMs: CACHE_TTL.depsDev,
|
|
1182
|
+
fetchImpl
|
|
1183
|
+
});
|
|
1184
|
+
const nodes = Array.isArray(data?.nodes) ? data.nodes : [];
|
|
1185
|
+
const names = nodes.map((n) => n?.versionKey?.name).filter((n) => typeof n === "string" && n !== name);
|
|
1186
|
+
return [...new Set(names)];
|
|
1187
|
+
} catch {
|
|
1188
|
+
return [];
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
810
1191
|
async function fetchDepsDev(name, version, repo, fetchImpl) {
|
|
811
1192
|
const [scorecard, advisories] = await Promise.all([
|
|
812
1193
|
repo ? fetchScorecard(repo.owner, repo.repo, fetchImpl) : Promise.resolve(null),
|
|
@@ -814,14 +1195,14 @@ async function fetchDepsDev(name, version, repo, fetchImpl) {
|
|
|
814
1195
|
]);
|
|
815
1196
|
return { scorecard, advisories };
|
|
816
1197
|
}
|
|
817
|
-
var
|
|
1198
|
+
var HOST5, MAX_ADVISORIES;
|
|
818
1199
|
var init_depsDev = __esm({
|
|
819
1200
|
"src/ingestion/sources/depsDev.ts"() {
|
|
820
1201
|
"use strict";
|
|
821
1202
|
init_esm_shims();
|
|
822
1203
|
init_constants();
|
|
823
1204
|
init_http();
|
|
824
|
-
|
|
1205
|
+
HOST5 = "api.deps.dev";
|
|
825
1206
|
MAX_ADVISORIES = 10;
|
|
826
1207
|
}
|
|
827
1208
|
});
|
|
@@ -834,10 +1215,10 @@ function parseBundleSize(json2) {
|
|
|
834
1215
|
}
|
|
835
1216
|
async function fetchBundlephobia(name, category, fetchImpl) {
|
|
836
1217
|
if (!isFrontendCategory(category)) return { bundleMinGzipKb: null };
|
|
837
|
-
const url = `https://${
|
|
1218
|
+
const url = `https://${HOST6}/api/size?package=${encodeURIComponent(name)}`;
|
|
838
1219
|
try {
|
|
839
1220
|
const { data } = await httpGetJson(url, {
|
|
840
|
-
host:
|
|
1221
|
+
host: HOST6,
|
|
841
1222
|
ttlMs: CACHE_TTL.bundlephobia,
|
|
842
1223
|
timeoutMs: 12e3,
|
|
843
1224
|
retries: 1,
|
|
@@ -848,7 +1229,7 @@ async function fetchBundlephobia(name, category, fetchImpl) {
|
|
|
848
1229
|
return { bundleMinGzipKb: null };
|
|
849
1230
|
}
|
|
850
1231
|
}
|
|
851
|
-
var
|
|
1232
|
+
var HOST6;
|
|
852
1233
|
var init_bundlephobia = __esm({
|
|
853
1234
|
"src/ingestion/sources/bundlephobia.ts"() {
|
|
854
1235
|
"use strict";
|
|
@@ -856,7 +1237,7 @@ var init_bundlephobia = __esm({
|
|
|
856
1237
|
init_constants();
|
|
857
1238
|
init_http();
|
|
858
1239
|
init_types();
|
|
859
|
-
|
|
1240
|
+
HOST6 = "bundlephobia.com";
|
|
860
1241
|
}
|
|
861
1242
|
});
|
|
862
1243
|
|
|
@@ -866,6 +1247,7 @@ var init_sources = __esm({
|
|
|
866
1247
|
"use strict";
|
|
867
1248
|
init_esm_shims();
|
|
868
1249
|
init_npmRegistry();
|
|
1250
|
+
init_npmSearch();
|
|
869
1251
|
init_npmDownloads();
|
|
870
1252
|
init_github();
|
|
871
1253
|
init_depsDev();
|
|
@@ -873,13 +1255,255 @@ var init_sources = __esm({
|
|
|
873
1255
|
}
|
|
874
1256
|
});
|
|
875
1257
|
|
|
1258
|
+
// src/compat/members.ts
|
|
1259
|
+
async function assembleMembers(db, names) {
|
|
1260
|
+
const tracked = new Map((await getCompatMetadata(db, names)).map((r) => [r.name, r]));
|
|
1261
|
+
const members = [];
|
|
1262
|
+
const unverified = [];
|
|
1263
|
+
await Promise.all(
|
|
1264
|
+
names.map(async (name) => {
|
|
1265
|
+
const row = tracked.get(name);
|
|
1266
|
+
if (row) {
|
|
1267
|
+
members.push({
|
|
1268
|
+
name,
|
|
1269
|
+
version: row.latestVersion,
|
|
1270
|
+
peerDependencies: row.peerDependencies,
|
|
1271
|
+
peerDependenciesMeta: row.peerDependenciesMeta,
|
|
1272
|
+
engines: row.engines
|
|
1273
|
+
});
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const reg = await fetchNpmRegistry(name).catch(() => null);
|
|
1277
|
+
if (reg) {
|
|
1278
|
+
members.push({
|
|
1279
|
+
name,
|
|
1280
|
+
version: reg.latestVersion,
|
|
1281
|
+
peerDependencies: reg.peerDependencies,
|
|
1282
|
+
peerDependenciesMeta: reg.peerDependenciesMeta,
|
|
1283
|
+
engines: reg.engines
|
|
1284
|
+
});
|
|
1285
|
+
} else {
|
|
1286
|
+
unverified.push(name);
|
|
1287
|
+
}
|
|
1288
|
+
})
|
|
1289
|
+
);
|
|
1290
|
+
return { members, unverified };
|
|
1291
|
+
}
|
|
1292
|
+
var init_members = __esm({
|
|
1293
|
+
"src/compat/members.ts"() {
|
|
1294
|
+
"use strict";
|
|
1295
|
+
init_esm_shims();
|
|
1296
|
+
init_compat();
|
|
1297
|
+
init_sources();
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// src/compat/peerCompat.ts
|
|
1302
|
+
import semver from "semver";
|
|
1303
|
+
function satisfiesRange(version, range) {
|
|
1304
|
+
if (!semver.validRange(range)) return null;
|
|
1305
|
+
const v = semver.valid(version) ? version : semver.coerce(version)?.version;
|
|
1306
|
+
if (!v) return null;
|
|
1307
|
+
return semver.satisfies(v, range, { includePrerelease: true });
|
|
1308
|
+
}
|
|
1309
|
+
function rangesIntersect(a, b) {
|
|
1310
|
+
if (!semver.validRange(a) || !semver.validRange(b)) return null;
|
|
1311
|
+
try {
|
|
1312
|
+
return semver.intersects(a, b, { includePrerelease: true });
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function resolveArchitectureCompat(members) {
|
|
1318
|
+
const conflicts = [];
|
|
1319
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const m of members) if (m.version) pinned.set(m.name, m.version);
|
|
1321
|
+
const constraints = [];
|
|
1322
|
+
for (const m of members) {
|
|
1323
|
+
if (!m.peerDependencies) continue;
|
|
1324
|
+
for (const [peer, range] of Object.entries(m.peerDependencies)) {
|
|
1325
|
+
constraints.push({
|
|
1326
|
+
requirer: m.name,
|
|
1327
|
+
peer,
|
|
1328
|
+
range,
|
|
1329
|
+
optional: Boolean(m.peerDependenciesMeta?.[peer]?.optional)
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
for (const c of constraints) {
|
|
1334
|
+
const pv = pinned.get(c.peer);
|
|
1335
|
+
if (pv && satisfiesRange(pv, c.range) === false) {
|
|
1336
|
+
conflicts.push({
|
|
1337
|
+
source: "peer-deps",
|
|
1338
|
+
packages: [c.requirer, c.peer],
|
|
1339
|
+
detail: `${c.requirer} needs peer ${c.peer}@${c.range}, but the stack uses ${c.peer}@${pv}`
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const byPeer = /* @__PURE__ */ new Map();
|
|
1344
|
+
for (const c of constraints) {
|
|
1345
|
+
if (pinned.has(c.peer) || c.optional) continue;
|
|
1346
|
+
const arr = byPeer.get(c.peer);
|
|
1347
|
+
if (arr) arr.push(c);
|
|
1348
|
+
else byPeer.set(c.peer, [c]);
|
|
1349
|
+
}
|
|
1350
|
+
for (const [peer, cs] of byPeer) {
|
|
1351
|
+
for (let i = 0; i < cs.length; i++) {
|
|
1352
|
+
for (let j = i + 1; j < cs.length; j++) {
|
|
1353
|
+
if (cs[i].range !== cs[j].range && rangesIntersect(cs[i].range, cs[j].range) === false) {
|
|
1354
|
+
conflicts.push({
|
|
1355
|
+
source: "peer-deps",
|
|
1356
|
+
packages: [cs[i].requirer, cs[j].requirer],
|
|
1357
|
+
detail: `${cs[i].requirer} needs ${peer}@${cs[i].range} but ${cs[j].requirer} needs ${peer}@${cs[j].range} \u2014 no overlapping version`
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const nodeReqs = members.map((m) => ({ name: m.name, range: m.engines?.node })).filter((r) => typeof r.range === "string");
|
|
1364
|
+
for (let i = 0; i < nodeReqs.length; i++) {
|
|
1365
|
+
for (let j = i + 1; j < nodeReqs.length; j++) {
|
|
1366
|
+
if (rangesIntersect(nodeReqs[i].range, nodeReqs[j].range) === false) {
|
|
1367
|
+
conflicts.push({
|
|
1368
|
+
source: "engines",
|
|
1369
|
+
packages: [nodeReqs[i].name, nodeReqs[j].name],
|
|
1370
|
+
detail: `${nodeReqs[i].name} needs node ${nodeReqs[i].range} but ${nodeReqs[j].name} needs node ${nodeReqs[j].range} \u2014 no overlap`
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return conflicts;
|
|
1376
|
+
}
|
|
1377
|
+
var init_peerCompat = __esm({
|
|
1378
|
+
"src/compat/peerCompat.ts"() {
|
|
1379
|
+
"use strict";
|
|
1380
|
+
init_esm_shims();
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// src/compat/check.ts
|
|
1385
|
+
async function checkCompat(db, packages2) {
|
|
1386
|
+
const names = [...new Set(packages2)];
|
|
1387
|
+
const { members, unverified } = await assembleMembers(db, names);
|
|
1388
|
+
const conflicts = resolveArchitectureCompat(members);
|
|
1389
|
+
for (const edge of await getCompatEdges(db, names)) {
|
|
1390
|
+
if (edge.status === "conflict") {
|
|
1391
|
+
conflicts.push({
|
|
1392
|
+
source: "sandbox",
|
|
1393
|
+
packages: [edge.packageA, edge.packageB],
|
|
1394
|
+
detail: `${edge.packageA}@${edge.versionA} and ${edge.packageB}@${edge.versionB} failed to co-install in the sandbox`
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const overall = conflicts.length ? "conflict" : unverified.length ? "unknown" : "compatible";
|
|
1399
|
+
return {
|
|
1400
|
+
packages: names,
|
|
1401
|
+
overall,
|
|
1402
|
+
conflicts,
|
|
1403
|
+
unverified,
|
|
1404
|
+
checked: members.map((m) => ({ name: m.name, version: m.version }))
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
var init_check = __esm({
|
|
1408
|
+
"src/compat/check.ts"() {
|
|
1409
|
+
"use strict";
|
|
1410
|
+
init_esm_shims();
|
|
1411
|
+
init_compat();
|
|
1412
|
+
init_members();
|
|
1413
|
+
init_peerCompat();
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
// src/db/packages.ts
|
|
1418
|
+
import { desc, eq, isNotNull } from "drizzle-orm";
|
|
1419
|
+
async function getSeedTargets(db) {
|
|
1420
|
+
const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
|
|
1421
|
+
return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
|
|
1422
|
+
}
|
|
1423
|
+
async function getPackageByName(db, name) {
|
|
1424
|
+
const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
|
|
1425
|
+
return rows[0] ?? null;
|
|
1426
|
+
}
|
|
1427
|
+
async function getAllPackageNames(db) {
|
|
1428
|
+
const rows = await db.select({ name: packages.name }).from(packages);
|
|
1429
|
+
return rows.map((r) => r.name);
|
|
1430
|
+
}
|
|
1431
|
+
async function upsertPackageVersions(db, name, versions) {
|
|
1432
|
+
if (versions.length === 0) return;
|
|
1433
|
+
for (let i = 0; i < versions.length; i += VERSION_CHUNK) {
|
|
1434
|
+
const rows = versions.slice(i, i + VERSION_CHUNK).map((v) => ({
|
|
1435
|
+
packageName: name,
|
|
1436
|
+
version: v.version,
|
|
1437
|
+
publishedAt: v.publishedAt
|
|
1438
|
+
}));
|
|
1439
|
+
await db.insert(packageVersions).values(rows).onConflictDoNothing();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async function getPackageVersions(db, name, limit = 50) {
|
|
1443
|
+
const rows = await db.select({ version: packageVersions.version, publishedAt: packageVersions.publishedAt }).from(packageVersions).where(eq(packageVersions.packageName, name)).orderBy(desc(packageVersions.publishedAt)).limit(limit);
|
|
1444
|
+
return rows.map((r) => ({ version: r.version, publishedAt: r.publishedAt }));
|
|
1445
|
+
}
|
|
1446
|
+
async function getTopPackageNames(db, limit = 1e3) {
|
|
1447
|
+
const rows = await db.select({ name: packages.name }).from(packages).where(isNotNull(packages.weeklyDownloads)).orderBy(desc(packages.weeklyDownloads)).limit(limit);
|
|
1448
|
+
return rows.map((r) => r.name);
|
|
1449
|
+
}
|
|
1450
|
+
async function ensureSeedEntry(db, name, category) {
|
|
1451
|
+
await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
|
|
1452
|
+
}
|
|
1453
|
+
async function upsertPackage(db, row) {
|
|
1454
|
+
const { name: _name, createdAt: _createdAt, ...mutable } = row;
|
|
1455
|
+
await db.insert(packages).values(row).onConflictDoUpdate({
|
|
1456
|
+
target: packages.name,
|
|
1457
|
+
set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
async function startSyncRun(db) {
|
|
1461
|
+
const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
|
|
1462
|
+
return row.id;
|
|
1463
|
+
}
|
|
1464
|
+
async function finishSyncRun(db, id, data) {
|
|
1465
|
+
await db.update(syncRuns).set({
|
|
1466
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
1467
|
+
packagesSeen: data.packagesSeen,
|
|
1468
|
+
packagesUpdated: data.packagesUpdated,
|
|
1469
|
+
errors: data.errors,
|
|
1470
|
+
status: data.status
|
|
1471
|
+
}).where(eq(syncRuns.id, id));
|
|
1472
|
+
}
|
|
1473
|
+
var VERSION_CHUNK;
|
|
1474
|
+
var init_packages = __esm({
|
|
1475
|
+
"src/db/packages.ts"() {
|
|
1476
|
+
"use strict";
|
|
1477
|
+
init_esm_shims();
|
|
1478
|
+
init_schema();
|
|
1479
|
+
VERSION_CHUNK = 500;
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
// src/db/verification.ts
|
|
1484
|
+
import { and as and2, desc as desc2, eq as eq2 } from "drizzle-orm";
|
|
1485
|
+
async function storeVerificationRun(db, run) {
|
|
1486
|
+
await db.insert(verificationRuns).values(run);
|
|
1487
|
+
}
|
|
1488
|
+
async function getLatestVerificationByName(db, packageName) {
|
|
1489
|
+
const rows = await db.select().from(verificationRuns).where(eq2(verificationRuns.packageName, packageName)).orderBy(desc2(verificationRuns.ranAt)).limit(1);
|
|
1490
|
+
return rows[0] ?? null;
|
|
1491
|
+
}
|
|
1492
|
+
var init_verification = __esm({
|
|
1493
|
+
"src/db/verification.ts"() {
|
|
1494
|
+
"use strict";
|
|
1495
|
+
init_esm_shims();
|
|
1496
|
+
init_schema();
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
|
|
876
1500
|
// src/ingestion/sources/githubReadme.ts
|
|
877
1501
|
async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
878
1502
|
for (const file of CANDIDATES) {
|
|
879
|
-
const url = `https://${
|
|
1503
|
+
const url = `https://${HOST7}/${owner}/${repo}/HEAD/${file}`;
|
|
880
1504
|
try {
|
|
881
1505
|
const { data } = await httpRequest(url, {
|
|
882
|
-
host:
|
|
1506
|
+
host: HOST7,
|
|
883
1507
|
ttlMs: CACHE_TTL.github,
|
|
884
1508
|
accept: "text",
|
|
885
1509
|
retries: 0,
|
|
@@ -891,14 +1515,14 @@ async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
|
891
1515
|
}
|
|
892
1516
|
return null;
|
|
893
1517
|
}
|
|
894
|
-
var
|
|
1518
|
+
var HOST7, CANDIDATES;
|
|
895
1519
|
var init_githubReadme = __esm({
|
|
896
1520
|
"src/ingestion/sources/githubReadme.ts"() {
|
|
897
1521
|
"use strict";
|
|
898
1522
|
init_esm_shims();
|
|
899
1523
|
init_constants();
|
|
900
1524
|
init_http();
|
|
901
|
-
|
|
1525
|
+
HOST7 = "raw.githubusercontent.com";
|
|
902
1526
|
CANDIDATES = ["README.md", "readme.md", "README.markdown", "README"];
|
|
903
1527
|
}
|
|
904
1528
|
});
|
|
@@ -915,6 +1539,7 @@ function truncateSentences(text2, max = 3) {
|
|
|
915
1539
|
}
|
|
916
1540
|
function buildUserPrompt(input) {
|
|
917
1541
|
const readme = (input.readme ?? "").slice(0, 4e3);
|
|
1542
|
+
const needsCategory = input.category == null;
|
|
918
1543
|
return [
|
|
919
1544
|
`Package: ${input.name}`,
|
|
920
1545
|
`Category: ${input.category ?? "unknown"}`,
|
|
@@ -928,7 +1553,8 @@ ${readme || "(none)"}`,
|
|
|
928
1553
|
'- "whenToUse": one sentence on the ideal use case.',
|
|
929
1554
|
'- "whenNotToUse": one sentence on when to pick something else (or "").',
|
|
930
1555
|
'- "whereItFits": one sentence on its architectural role.',
|
|
931
|
-
'- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".'
|
|
1556
|
+
'- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".',
|
|
1557
|
+
...needsCategory ? [`- "category": the single best fit from this list: ${CATEGORIES.join(", ")}.`] : []
|
|
932
1558
|
].join("\n");
|
|
933
1559
|
}
|
|
934
1560
|
function str(value) {
|
|
@@ -962,6 +1588,7 @@ var init_summarize = __esm({
|
|
|
962
1588
|
"src/ingestion/summarize.ts"() {
|
|
963
1589
|
"use strict";
|
|
964
1590
|
init_esm_shims();
|
|
1591
|
+
init_types();
|
|
965
1592
|
init_config();
|
|
966
1593
|
init_http();
|
|
967
1594
|
init_logger();
|
|
@@ -1003,7 +1630,7 @@ var init_summarize = __esm({
|
|
|
1003
1630
|
whereItFits: `Sits at ${role}.`,
|
|
1004
1631
|
context7Hint: context7Hint(input.name)
|
|
1005
1632
|
};
|
|
1006
|
-
return { summary, usageGuide: guide };
|
|
1633
|
+
return { summary, usageGuide: guide, inferredCategory: null };
|
|
1007
1634
|
}
|
|
1008
1635
|
};
|
|
1009
1636
|
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.";
|
|
@@ -1051,7 +1678,13 @@ var init_summarize = __esm({
|
|
|
1051
1678
|
howToWireIn: str(parsed.howToWireIn) || void 0,
|
|
1052
1679
|
context7Hint: context7Hint(input.name)
|
|
1053
1680
|
};
|
|
1054
|
-
|
|
1681
|
+
const rawCategory = str(parsed.category);
|
|
1682
|
+
const inferredCategory = rawCategory && isCategory(rawCategory) ? rawCategory : null;
|
|
1683
|
+
return {
|
|
1684
|
+
summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null,
|
|
1685
|
+
usageGuide: guide,
|
|
1686
|
+
inferredCategory
|
|
1687
|
+
};
|
|
1055
1688
|
} catch (err) {
|
|
1056
1689
|
logger.warn(`summary LLM failed for ${input.name}, using fallback: ${err.message}`);
|
|
1057
1690
|
return new FallbackSummaryProvider().generate(input);
|
|
@@ -1103,47 +1736,256 @@ var init_collect = __esm({
|
|
|
1103
1736
|
}
|
|
1104
1737
|
});
|
|
1105
1738
|
|
|
1106
|
-
// src/
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1739
|
+
// src/search/categoryInference.ts
|
|
1740
|
+
function inferCategory(need) {
|
|
1741
|
+
const text2 = need.toLowerCase();
|
|
1742
|
+
for (const rule of RULES) {
|
|
1743
|
+
if (rule.patterns.test(text2)) return rule.category;
|
|
1744
|
+
}
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
function inferCategoryFromSignals(signals) {
|
|
1748
|
+
const r = signals.registry;
|
|
1749
|
+
const keywords = (r?.keywords ?? []).join(" ");
|
|
1750
|
+
const text2 = [
|
|
1751
|
+
signals.name,
|
|
1752
|
+
keywords,
|
|
1753
|
+
keywords,
|
|
1754
|
+
// weight keywords higher via repetition
|
|
1755
|
+
r?.description ?? "",
|
|
1756
|
+
(r?.readme ?? "").slice(0, 600)
|
|
1757
|
+
].filter(Boolean).join(" ");
|
|
1758
|
+
return inferCategory(text2);
|
|
1759
|
+
}
|
|
1760
|
+
var RULES;
|
|
1761
|
+
var init_categoryInference = __esm({
|
|
1762
|
+
"src/search/categoryInference.ts"() {
|
|
1110
1763
|
"use strict";
|
|
1111
1764
|
init_esm_shims();
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1765
|
+
RULES = [
|
|
1766
|
+
{ category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
|
|
1767
|
+
{ category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
|
|
1768
|
+
{ category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
|
|
1769
|
+
{ category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
|
|
1770
|
+
{ category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
|
|
1771
|
+
{ category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
|
|
1772
|
+
{ category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
|
|
1773
|
+
{ category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
|
|
1774
|
+
{ category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
|
|
1775
|
+
{ category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
|
|
1776
|
+
{ category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
|
|
1777
|
+
{ category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
|
|
1778
|
+
{ category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
|
|
1779
|
+
{ category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
|
|
1780
|
+
{ category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
|
|
1781
|
+
// date-time precedes linting: "date formatting" must not be captured by linting's
|
|
1782
|
+
// bare "formatting" token before the date rule is reached (first-match-wins).
|
|
1783
|
+
{ category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
|
|
1784
|
+
{ category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
|
|
1785
|
+
{ category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
|
|
1786
|
+
{ category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
|
|
1787
|
+
{ category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
|
|
1788
|
+
{ category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
|
|
1789
|
+
{ category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
|
|
1790
|
+
];
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
// src/core/paths.ts
|
|
1795
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1796
|
+
import { homedir as homedir2 } from "os";
|
|
1797
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1798
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1799
|
+
function packageRoot() {
|
|
1800
|
+
if (cachedRoot) return cachedRoot;
|
|
1801
|
+
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
1802
|
+
for (; ; ) {
|
|
1803
|
+
if (existsSync2(join2(dir, "package.json"))) {
|
|
1804
|
+
cachedRoot = dir;
|
|
1805
|
+
return dir;
|
|
1806
|
+
}
|
|
1807
|
+
const parent = dirname2(dir);
|
|
1808
|
+
if (parent === dir) break;
|
|
1809
|
+
dir = parent;
|
|
1810
|
+
}
|
|
1811
|
+
cachedRoot = process.cwd();
|
|
1812
|
+
return cachedRoot;
|
|
1813
|
+
}
|
|
1814
|
+
function migrationsDir() {
|
|
1815
|
+
return join2(packageRoot(), "drizzle");
|
|
1816
|
+
}
|
|
1817
|
+
function seedJsonPath() {
|
|
1818
|
+
return join2(packageRoot(), "src", "data", "seed.json");
|
|
1819
|
+
}
|
|
1820
|
+
function userWeightsPath() {
|
|
1821
|
+
const base = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
|
|
1822
|
+
return join2(base, "lurq", "weights.json");
|
|
1823
|
+
}
|
|
1824
|
+
function projectWeightsPath() {
|
|
1825
|
+
return join2(process.cwd(), ".lurq", "weights.json");
|
|
1826
|
+
}
|
|
1827
|
+
var cachedRoot;
|
|
1828
|
+
var init_paths = __esm({
|
|
1829
|
+
"src/core/paths.ts"() {
|
|
1830
|
+
"use strict";
|
|
1831
|
+
init_esm_shims();
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// src/scoring/weights.ts
|
|
1836
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
1837
|
+
import { dirname as dirname3 } from "path";
|
|
1838
|
+
function activeWeightsPath() {
|
|
1839
|
+
if (existsSync3(projectWeightsPath())) return { path: projectWeightsPath(), source: "project-config" };
|
|
1840
|
+
if (existsSync3(userWeightsPath())) return { path: userWeightsPath(), source: "user-config" };
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
function loadWeights() {
|
|
1844
|
+
if (cachedWeights) return cachedWeights;
|
|
1845
|
+
let merged = structuredClone(DEFAULT_WEIGHTS);
|
|
1846
|
+
const active = activeWeightsPath();
|
|
1847
|
+
if (active) {
|
|
1848
|
+
try {
|
|
1849
|
+
const fromFile = JSON.parse(readFileSync(active.path, "utf8"));
|
|
1850
|
+
merged = mergeWeights(merged, fromFile);
|
|
1851
|
+
} catch {
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
const envLambda = process.env.LURQ_COMPOSITE_LAMBDA;
|
|
1855
|
+
if (envLambda !== void 0 && envLambda !== "") {
|
|
1856
|
+
const n = Number(envLambda);
|
|
1857
|
+
if (Number.isFinite(n)) merged.composite.lambda = n;
|
|
1858
|
+
}
|
|
1859
|
+
cachedWeights = validateWeights(merged).weights;
|
|
1860
|
+
return cachedWeights;
|
|
1861
|
+
}
|
|
1862
|
+
function resetWeightsCache() {
|
|
1863
|
+
cachedWeights = void 0;
|
|
1864
|
+
}
|
|
1865
|
+
function mergeWeights(base, over) {
|
|
1866
|
+
return {
|
|
1867
|
+
health: { ...base.health, ...over.health ?? {} },
|
|
1868
|
+
composite: { ...base.composite, ...over.composite ?? {} }
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function validateWeights(w) {
|
|
1872
|
+
const lambda = Math.max(0, Math.min(1, w.composite.lambda));
|
|
1873
|
+
const h = w.health;
|
|
1874
|
+
const sum = h.maintenance + h.adoption + h.reliability + h.efficiency;
|
|
1875
|
+
let normalized = false;
|
|
1876
|
+
let health = h;
|
|
1877
|
+
if (sum <= 0) {
|
|
1878
|
+
health = { ...DEFAULT_WEIGHTS.health };
|
|
1879
|
+
normalized = true;
|
|
1880
|
+
} else if (Math.abs(sum - 1) > 1e-6) {
|
|
1881
|
+
health = {
|
|
1882
|
+
maintenance: h.maintenance / sum,
|
|
1883
|
+
adoption: h.adoption / sum,
|
|
1884
|
+
reliability: h.reliability / sum,
|
|
1885
|
+
efficiency: h.efficiency / sum
|
|
1886
|
+
};
|
|
1887
|
+
normalized = true;
|
|
1888
|
+
}
|
|
1889
|
+
return { weights: { health, composite: { lambda } }, normalized };
|
|
1890
|
+
}
|
|
1891
|
+
function settableKeys() {
|
|
1892
|
+
return Object.keys(SETTABLE);
|
|
1893
|
+
}
|
|
1894
|
+
function applyOverrides(base, sets) {
|
|
1895
|
+
const next = structuredClone(base);
|
|
1896
|
+
for (const entry of sets) {
|
|
1897
|
+
const eq9 = entry.indexOf("=");
|
|
1898
|
+
if (eq9 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
|
|
1899
|
+
const key = entry.slice(0, eq9).trim();
|
|
1900
|
+
const value = Number(entry.slice(eq9 + 1).trim());
|
|
1901
|
+
const apply = SETTABLE[key];
|
|
1902
|
+
if (!apply) {
|
|
1903
|
+
throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
|
|
1904
|
+
}
|
|
1905
|
+
if (!Number.isFinite(value)) throw new Error(`Value for "${key}" must be a number.`);
|
|
1906
|
+
apply(next, value);
|
|
1907
|
+
}
|
|
1908
|
+
return next;
|
|
1909
|
+
}
|
|
1910
|
+
function saveWeights(w, scope = "user") {
|
|
1911
|
+
const path2 = scope === "project" ? projectWeightsPath() : userWeightsPath();
|
|
1912
|
+
mkdirSync(dirname3(path2), { recursive: true });
|
|
1913
|
+
writeFileSync(path2, JSON.stringify(w, null, 2) + "\n", "utf8");
|
|
1914
|
+
resetWeightsCache();
|
|
1915
|
+
return path2;
|
|
1916
|
+
}
|
|
1917
|
+
function resetWeights() {
|
|
1918
|
+
const removed = [];
|
|
1919
|
+
for (const path2 of [projectWeightsPath(), userWeightsPath()]) {
|
|
1920
|
+
if (existsSync3(path2)) {
|
|
1921
|
+
rmSync(path2);
|
|
1922
|
+
removed.push(path2);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
resetWeightsCache();
|
|
1926
|
+
return removed;
|
|
1927
|
+
}
|
|
1928
|
+
var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DISCOVERY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
|
|
1929
|
+
var init_weights = __esm({
|
|
1930
|
+
"src/scoring/weights.ts"() {
|
|
1931
|
+
"use strict";
|
|
1932
|
+
init_esm_shims();
|
|
1933
|
+
init_paths();
|
|
1934
|
+
HEALTH_WEIGHTS = {
|
|
1935
|
+
maintenance: 0.35,
|
|
1936
|
+
adoption: 0.3,
|
|
1937
|
+
reliability: 0.25,
|
|
1938
|
+
efficiency: 0.1
|
|
1939
|
+
};
|
|
1940
|
+
COMPOSITE = {
|
|
1941
|
+
lambda: 0.35
|
|
1942
|
+
};
|
|
1943
|
+
QUALITY_WEIGHTS = {
|
|
1944
|
+
types: 0.25,
|
|
1945
|
+
tests: 0.2,
|
|
1946
|
+
docs: 0.2,
|
|
1947
|
+
changelog: 0.1,
|
|
1948
|
+
deps: 0.1,
|
|
1949
|
+
license: 0.1,
|
|
1950
|
+
provenance: 0.05
|
|
1951
|
+
};
|
|
1952
|
+
QUALITY = {
|
|
1953
|
+
/** README at/above this many characters scores full marks on doc length. */
|
|
1954
|
+
docsFullLengthChars: 1200,
|
|
1955
|
+
/** Direct-dependency count at/above this scores 0 on the deps component (log-scaled). */
|
|
1956
|
+
depsLogMax: 30,
|
|
1957
|
+
/** A license string present but not OSI-recognized scores this (vs 100 / 0). */
|
|
1958
|
+
licenseUnrecognizedScore: 60
|
|
1959
|
+
};
|
|
1960
|
+
MAINTENANCE_WEIGHTS = {
|
|
1961
|
+
recency: 0.5,
|
|
1962
|
+
cadence: 0.25,
|
|
1963
|
+
closeRatio: 0.25
|
|
1964
|
+
};
|
|
1965
|
+
MAINTENANCE = {
|
|
1966
|
+
/** A release within this many days scores full recency. */
|
|
1967
|
+
freshDays: 30,
|
|
1968
|
+
/** Recency decays linearly to 0 by this many days (~24 months). */
|
|
1969
|
+
staleDays: 730,
|
|
1970
|
+
/** Releases/12mo at or above this cap full cadence. */
|
|
1971
|
+
cadenceCap: 12,
|
|
1972
|
+
/** deprecated/archived hard-cap maintenance at this value. */
|
|
1973
|
+
deadCap: 15
|
|
1974
|
+
};
|
|
1975
|
+
ADOPTION = {
|
|
1976
|
+
/** log10(weekly downloads): this maps to 0. */
|
|
1977
|
+
downloadsLogMin: 1,
|
|
1978
|
+
// 10/wk
|
|
1979
|
+
/** log10(weekly downloads): this maps to 100. */
|
|
1980
|
+
downloadsLogMax: 7,
|
|
1981
|
+
// 10M/wk
|
|
1982
|
+
/** log10(stars) that maps to 100. */
|
|
1983
|
+
starsLogMax: 5,
|
|
1984
|
+
// 100k stars
|
|
1985
|
+
/** Weight of the stars component when available (downloads gets the rest). */
|
|
1986
|
+
starsWeight: 0.2,
|
|
1987
|
+
/** Growth bonus: growth fraction × this, clamped to ±cap. */
|
|
1988
|
+
growthMultiplier: 30,
|
|
1147
1989
|
growthBonusCap: 20
|
|
1148
1990
|
};
|
|
1149
1991
|
RELIABILITY = {
|
|
@@ -1162,6 +2004,35 @@ var init_weights = __esm({
|
|
|
1162
2004
|
/** Bundle size at the category median maps to this score; smaller → higher. */
|
|
1163
2005
|
medianScore: 50
|
|
1164
2006
|
};
|
|
2007
|
+
DISCOVERY = {
|
|
2008
|
+
/** A queued candidate must clear this quality pre-score to graduate to ingest. */
|
|
2009
|
+
minPreScore: 45,
|
|
2010
|
+
/** Max candidates fully ingested per crawler run (cost bound). */
|
|
2011
|
+
perRunCap: 25,
|
|
2012
|
+
/** npm-search hits to pull per category keyword. */
|
|
2013
|
+
searchSizePerCategory: 10,
|
|
2014
|
+
/** Max dependency-graph neighbors to enqueue per tracked seed. */
|
|
2015
|
+
graphNeighborsPerSeed: 20
|
|
2016
|
+
};
|
|
2017
|
+
DEFAULT_WEIGHTS = {
|
|
2018
|
+
health: { ...HEALTH_WEIGHTS },
|
|
2019
|
+
composite: { ...COMPOSITE }
|
|
2020
|
+
};
|
|
2021
|
+
SETTABLE = {
|
|
2022
|
+
"health.maintenance": (w, v) => w.health.maintenance = v,
|
|
2023
|
+
"health.adoption": (w, v) => w.health.adoption = v,
|
|
2024
|
+
"health.reliability": (w, v) => w.health.reliability = v,
|
|
2025
|
+
"health.efficiency": (w, v) => w.health.efficiency = v,
|
|
2026
|
+
"composite.lambda": (w, v) => w.composite.lambda = v
|
|
2027
|
+
};
|
|
2028
|
+
WEIGHT_EXPLANATIONS = {
|
|
2029
|
+
maintenance: "release recency, cadence, and issue close-ratio (weights.ts MAINTENANCE_WEIGHTS).",
|
|
2030
|
+
adoption: "weekly downloads (log-scaled), stars, and 90-day growth (weights.ts ADOPTION).",
|
|
2031
|
+
reliability: "OpenSSF Scorecard scaled 0\u2013100, minus advisory penalties (weights.ts RELIABILITY).",
|
|
2032
|
+
efficiency: "bundle size vs the category median; frontend categories only (weights.ts EFFICIENCY).",
|
|
2033
|
+
quality: "intrinsic, adoption-independent: types, tests, docs, changelog, deps, license, provenance (weights.ts QUALITY_WEIGHTS). A standalone axis \u2014 it does NOT feed health.",
|
|
2034
|
+
lambda: "how much the default sort composite favors quality over health: (1\u2212\u03BB)\xB7health + \u03BB\xB7quality."
|
|
2035
|
+
};
|
|
1165
2036
|
CONFIDENCE = {
|
|
1166
2037
|
proven: {
|
|
1167
2038
|
minWeeklyDownloads: 1e5,
|
|
@@ -1172,6 +2043,11 @@ var init_weights = __esm({
|
|
|
1172
2043
|
minWeeklyDownloads: 5e3,
|
|
1173
2044
|
strongGrowth: 0.5,
|
|
1174
2045
|
maxLastReleaseMonths: 9
|
|
2046
|
+
},
|
|
2047
|
+
/** `promising` (§1): new but intrinsically high-quality, regardless of adoption. */
|
|
2048
|
+
promising: {
|
|
2049
|
+
minQuality: 70,
|
|
2050
|
+
maxLastReleaseMonths: 12
|
|
1175
2051
|
}
|
|
1176
2052
|
};
|
|
1177
2053
|
}
|
|
@@ -1180,12 +2056,11 @@ var init_weights = __esm({
|
|
|
1180
2056
|
// src/scoring/score.ts
|
|
1181
2057
|
function toScoringInput(signals, category) {
|
|
1182
2058
|
const { registry, downloads, github, depsDev, bundle } = signals;
|
|
2059
|
+
const readme = registry?.readme ?? null;
|
|
1183
2060
|
return {
|
|
1184
2061
|
weeklyDownloads: downloads?.weeklyDownloads ?? null,
|
|
1185
2062
|
downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
|
|
1186
2063
|
stars: github?.stars ?? null,
|
|
1187
|
-
dependentsCount: null,
|
|
1188
|
-
// not collected in v1 (see spec R1 note)
|
|
1189
2064
|
firstPublishedAt: registry?.firstPublishedAt ?? null,
|
|
1190
2065
|
lastReleaseAt: github?.lastReleaseAt ?? registry?.lastReleaseAt ?? null,
|
|
1191
2066
|
releasesLast12mo: github?.releasesLast12mo ?? null,
|
|
@@ -1196,7 +2071,18 @@ function toScoringInput(signals, category) {
|
|
|
1196
2071
|
deprecated: registry?.deprecated ?? false,
|
|
1197
2072
|
archived: github?.archived ?? false,
|
|
1198
2073
|
bundleMinGzipKb: bundle?.bundleMinGzipKb ?? null,
|
|
1199
|
-
category
|
|
2074
|
+
category,
|
|
2075
|
+
// Quality signals — null (not false) when the manifest itself is missing, so
|
|
2076
|
+
// weightedAverage drops the component rather than scoring it 0.
|
|
2077
|
+
hasTypes: registry ? registry.hasTypes : null,
|
|
2078
|
+
hasTestScript: registry ? registry.hasTestScript : null,
|
|
2079
|
+
readmeLength: readme !== null ? readme.length : null,
|
|
2080
|
+
hasExamples: readme !== null ? /```/.test(readme) : null,
|
|
2081
|
+
hasHomepage: registry ? registry.homepage !== null : null,
|
|
2082
|
+
hasReleaseNotes: github?.releasesLast12mo != null ? github.releasesLast12mo > 0 : null,
|
|
2083
|
+
directDependenciesCount: registry?.directDependenciesCount ?? null,
|
|
2084
|
+
license: registry?.license ?? null,
|
|
2085
|
+
hasProvenance: registry ? registry.hasProvenance : null
|
|
1200
2086
|
};
|
|
1201
2087
|
}
|
|
1202
2088
|
function computeMaintenance(input, now) {
|
|
@@ -1254,23 +2140,68 @@ function computeEfficiency(bundleMinGzipKb, category, categoryMedian) {
|
|
|
1254
2140
|
const ratio = bundleMinGzipKb / categoryMedian;
|
|
1255
2141
|
return clamp(100 - ratio * EFFICIENCY.medianScore);
|
|
1256
2142
|
}
|
|
1257
|
-
function
|
|
2143
|
+
function isRecognizedLicense(license) {
|
|
2144
|
+
if (!license) return false;
|
|
2145
|
+
const id = license.trim().toLowerCase();
|
|
2146
|
+
return RECOGNIZED_LICENSES.some((l) => id === l || id.startsWith(l));
|
|
2147
|
+
}
|
|
2148
|
+
function computeQuality(input) {
|
|
2149
|
+
const components = [];
|
|
2150
|
+
if (input.hasTypes !== null) {
|
|
2151
|
+
components.push({ value: input.hasTypes ? 100 : 0, weight: QUALITY_WEIGHTS.types });
|
|
2152
|
+
}
|
|
2153
|
+
if (input.hasTestScript !== null) {
|
|
2154
|
+
components.push({ value: input.hasTestScript ? 100 : 0, weight: QUALITY_WEIGHTS.tests });
|
|
2155
|
+
}
|
|
2156
|
+
if (input.readmeLength !== null || input.hasHomepage !== null) {
|
|
2157
|
+
const lenScore = Math.min((input.readmeLength ?? 0) / QUALITY.docsFullLengthChars, 1) * 60;
|
|
2158
|
+
const examples = input.hasExamples ? 25 : 0;
|
|
2159
|
+
const homepage = input.hasHomepage ? 15 : 0;
|
|
2160
|
+
components.push({ value: clamp(lenScore + examples + homepage), weight: QUALITY_WEIGHTS.docs });
|
|
2161
|
+
}
|
|
2162
|
+
if (input.hasReleaseNotes !== null) {
|
|
2163
|
+
components.push({ value: input.hasReleaseNotes ? 100 : 0, weight: QUALITY_WEIGHTS.changelog });
|
|
2164
|
+
}
|
|
2165
|
+
if (input.directDependenciesCount !== null) {
|
|
2166
|
+
const deps = input.directDependenciesCount;
|
|
2167
|
+
const depsScore = clamp(
|
|
2168
|
+
100 - Math.log10(deps + 1) / Math.log10(QUALITY.depsLogMax + 1) * 100
|
|
2169
|
+
);
|
|
2170
|
+
components.push({ value: depsScore, weight: QUALITY_WEIGHTS.deps });
|
|
2171
|
+
}
|
|
2172
|
+
if (input.license !== null) {
|
|
2173
|
+
components.push({
|
|
2174
|
+
value: isRecognizedLicense(input.license) ? 100 : QUALITY.licenseUnrecognizedScore,
|
|
2175
|
+
weight: QUALITY_WEIGHTS.license
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
if (input.hasProvenance) {
|
|
2179
|
+
components.push({ value: 100, weight: QUALITY_WEIGHTS.provenance });
|
|
2180
|
+
}
|
|
2181
|
+
if (components.length === 0) return null;
|
|
2182
|
+
return weightedAverage(components);
|
|
2183
|
+
}
|
|
2184
|
+
function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
|
|
1258
2185
|
const { maintenance, adoption, reliability, efficiency } = breakdown;
|
|
2186
|
+
const w = weights;
|
|
1259
2187
|
if (efficiency === null) {
|
|
1260
|
-
const w = HEALTH_WEIGHTS;
|
|
1261
2188
|
const denom = w.maintenance + w.adoption + w.reliability;
|
|
1262
2189
|
return Math.round(
|
|
1263
2190
|
(w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
|
|
1264
2191
|
);
|
|
1265
2192
|
}
|
|
1266
2193
|
return Math.round(
|
|
1267
|
-
|
|
2194
|
+
w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability + w.efficiency * efficiency
|
|
1268
2195
|
);
|
|
1269
2196
|
}
|
|
2197
|
+
function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
|
|
2198
|
+
if (qualityScore === null) return healthScore;
|
|
2199
|
+
return Math.round((1 - lambda) * healthScore + lambda * qualityScore);
|
|
2200
|
+
}
|
|
1270
2201
|
function hasCriticalOrHighAdvisory(advisories) {
|
|
1271
2202
|
return advisories.some((a) => a.severity === "critical" || a.severity === "high");
|
|
1272
2203
|
}
|
|
1273
|
-
function computeConfidence(input, now) {
|
|
2204
|
+
function computeConfidence(input, now, qualityScore = null) {
|
|
1274
2205
|
const dl = input.weeklyDownloads ?? 0;
|
|
1275
2206
|
const ageMonths = monthsSince(input.firstPublishedAt, now);
|
|
1276
2207
|
const lastReleaseMonths = monthsSince(input.lastReleaseAt, now);
|
|
@@ -1285,6 +2216,11 @@ function computeConfidence(input, now) {
|
|
|
1285
2216
|
if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
|
|
1286
2217
|
return "emerging";
|
|
1287
2218
|
}
|
|
2219
|
+
const promisingRecencyMonths = lastReleaseMonths ?? ageMonths;
|
|
2220
|
+
const promisingReleaseOk = promisingRecencyMonths !== null && promisingRecencyMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
|
|
2221
|
+
if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
|
|
2222
|
+
return "promising";
|
|
2223
|
+
}
|
|
1288
2224
|
return "unproven";
|
|
1289
2225
|
}
|
|
1290
2226
|
function weightedAverage(components) {
|
|
@@ -1299,7 +2235,7 @@ function median(values) {
|
|
|
1299
2235
|
const mid = Math.floor(sorted.length / 2);
|
|
1300
2236
|
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
1301
2237
|
}
|
|
1302
|
-
var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince;
|
|
2238
|
+
var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince, RECOGNIZED_LICENSES;
|
|
1303
2239
|
var init_score = __esm({
|
|
1304
2240
|
"src/scoring/score.ts"() {
|
|
1305
2241
|
"use strict";
|
|
@@ -1311,6 +2247,20 @@ var init_score = __esm({
|
|
|
1311
2247
|
clamp = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
|
|
1312
2248
|
daysSince = (date, now) => date ? (now.getTime() - date.getTime()) / DAY_MS : null;
|
|
1313
2249
|
monthsSince = (date, now) => date ? (now.getTime() - date.getTime()) / MONTH_MS : null;
|
|
2250
|
+
RECOGNIZED_LICENSES = [
|
|
2251
|
+
"mit",
|
|
2252
|
+
"isc",
|
|
2253
|
+
"apache-2.0",
|
|
2254
|
+
"bsd-2-clause",
|
|
2255
|
+
"bsd-3-clause",
|
|
2256
|
+
"mpl-2.0",
|
|
2257
|
+
"lgpl",
|
|
2258
|
+
"gpl",
|
|
2259
|
+
"agpl",
|
|
2260
|
+
"unlicense",
|
|
2261
|
+
"0bsd",
|
|
2262
|
+
"cc0-1.0"
|
|
2263
|
+
];
|
|
1314
2264
|
}
|
|
1315
2265
|
});
|
|
1316
2266
|
|
|
@@ -1380,6 +2330,7 @@ var init_embeddings = __esm({
|
|
|
1380
2330
|
LocalEmbeddingProvider = class {
|
|
1381
2331
|
kind = "local";
|
|
1382
2332
|
dimensions = EMBEDDING_DIM;
|
|
2333
|
+
id = "local";
|
|
1383
2334
|
async embed(texts) {
|
|
1384
2335
|
return texts.map((t) => localEmbed(t));
|
|
1385
2336
|
}
|
|
@@ -1390,17 +2341,19 @@ var init_embeddings = __esm({
|
|
|
1390
2341
|
this.apiKey = apiKey;
|
|
1391
2342
|
this.model = model;
|
|
1392
2343
|
this.fetchImpl = fetchImpl;
|
|
2344
|
+
this.id = `openai:${model}`;
|
|
1393
2345
|
}
|
|
1394
2346
|
apiKey;
|
|
1395
2347
|
model;
|
|
1396
2348
|
fetchImpl;
|
|
1397
2349
|
kind = "openai";
|
|
1398
2350
|
dimensions = EMBEDDING_DIM;
|
|
2351
|
+
id;
|
|
1399
2352
|
async embed(texts) {
|
|
1400
2353
|
const out = [];
|
|
1401
2354
|
for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
|
|
1402
2355
|
const batch = texts.slice(i, i + OPENAI_BATCH);
|
|
1403
|
-
const body = JSON.stringify({ model: this.model, input: batch });
|
|
2356
|
+
const body = JSON.stringify({ model: this.model, input: batch, dimensions: EMBEDDING_DIM });
|
|
1404
2357
|
const { data } = await httpRequest("https://api.openai.com/v1/embeddings", {
|
|
1405
2358
|
host: "api.openai.com",
|
|
1406
2359
|
method: "POST",
|
|
@@ -1413,7 +2366,14 @@ var init_embeddings = __esm({
|
|
|
1413
2366
|
});
|
|
1414
2367
|
const vectors = data?.data ?? [];
|
|
1415
2368
|
vectors.sort((a, b) => a.index - b.index);
|
|
1416
|
-
for (const v of vectors)
|
|
2369
|
+
for (const v of vectors) {
|
|
2370
|
+
if (v.embedding.length !== EMBEDDING_DIM) {
|
|
2371
|
+
throw new Error(
|
|
2372
|
+
`OpenAI model ${this.model} returned ${v.embedding.length}-dim vectors; expected ${EMBEDDING_DIM}. Set EMBEDDING_MODEL to a text-embedding-3 model or update EMBEDDING_DIM.`
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
out.push(v.embedding);
|
|
2376
|
+
}
|
|
1417
2377
|
}
|
|
1418
2378
|
return out;
|
|
1419
2379
|
}
|
|
@@ -1421,47 +2381,6 @@ var init_embeddings = __esm({
|
|
|
1421
2381
|
}
|
|
1422
2382
|
});
|
|
1423
2383
|
|
|
1424
|
-
// src/db/packages.ts
|
|
1425
|
-
import { eq, sql } from "drizzle-orm";
|
|
1426
|
-
async function getSeedTargets(db) {
|
|
1427
|
-
const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
|
|
1428
|
-
return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
|
|
1429
|
-
}
|
|
1430
|
-
async function getPackageByName(db, name) {
|
|
1431
|
-
const rows = await db.select().from(packages).where(eq(packages.name, name)).limit(1);
|
|
1432
|
-
return rows[0] ?? null;
|
|
1433
|
-
}
|
|
1434
|
-
async function ensureSeedEntry(db, name, category) {
|
|
1435
|
-
await db.insert(seedPackages).values({ name, category }).onConflictDoNothing({ target: seedPackages.name });
|
|
1436
|
-
}
|
|
1437
|
-
async function upsertPackage(db, row) {
|
|
1438
|
-
const { name: _name, createdAt: _createdAt, ...mutable } = row;
|
|
1439
|
-
await db.insert(packages).values(row).onConflictDoUpdate({
|
|
1440
|
-
target: packages.name,
|
|
1441
|
-
set: { ...mutable, updatedAt: /* @__PURE__ */ new Date() }
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
async function startSyncRun(db) {
|
|
1445
|
-
const [row] = await db.insert(syncRuns).values({ status: "running" }).returning({ id: syncRuns.id });
|
|
1446
|
-
return row.id;
|
|
1447
|
-
}
|
|
1448
|
-
async function finishSyncRun(db, id, data) {
|
|
1449
|
-
await db.update(syncRuns).set({
|
|
1450
|
-
finishedAt: /* @__PURE__ */ new Date(),
|
|
1451
|
-
packagesSeen: data.packagesSeen,
|
|
1452
|
-
packagesUpdated: data.packagesUpdated,
|
|
1453
|
-
errors: data.errors,
|
|
1454
|
-
status: data.status
|
|
1455
|
-
}).where(eq(syncRuns.id, id));
|
|
1456
|
-
}
|
|
1457
|
-
var init_packages = __esm({
|
|
1458
|
-
"src/db/packages.ts"() {
|
|
1459
|
-
"use strict";
|
|
1460
|
-
init_esm_shims();
|
|
1461
|
-
init_schema();
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
2384
|
// src/core/concurrency.ts
|
|
1466
2385
|
async function pMap(items, mapper, concurrency) {
|
|
1467
2386
|
const results = new Array(items.length);
|
|
@@ -1508,18 +2427,28 @@ async function runSync(opts = {}) {
|
|
|
1508
2427
|
prefetchedWeekly: weeklyMap.has(target.name) ? weeklyMap.get(target.name) : void 0
|
|
1509
2428
|
});
|
|
1510
2429
|
for (const e of signals.errors) allErrors.push({ package: target.name, ...e });
|
|
1511
|
-
const input = toScoringInput(signals, target.category);
|
|
1512
2430
|
const summaryInput = await buildSummaryInput(signals, target.category);
|
|
1513
|
-
const { summary, usageGuide } = await provider.generate(summaryInput);
|
|
2431
|
+
const { summary, usageGuide, inferredCategory } = await provider.generate(summaryInput);
|
|
2432
|
+
let category = target.category;
|
|
2433
|
+
let categorySource = target.category ? "curated" : null;
|
|
2434
|
+
if (!category) {
|
|
2435
|
+
category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
|
|
2436
|
+
categorySource = category ? "inferred" : null;
|
|
2437
|
+
}
|
|
2438
|
+
const input = toScoringInput(signals, category);
|
|
2439
|
+
const quality = computeQuality(input);
|
|
1514
2440
|
if (++done % 25 === 0) logger.info(` \u2026${done}/${targets.length}`);
|
|
1515
2441
|
return {
|
|
1516
2442
|
target,
|
|
2443
|
+
category,
|
|
2444
|
+
categorySource,
|
|
1517
2445
|
signals,
|
|
1518
2446
|
input,
|
|
1519
2447
|
maintenance: computeMaintenance(input, now),
|
|
1520
2448
|
adoption: computeAdoption(input),
|
|
1521
2449
|
reliability: computeReliability(input),
|
|
1522
|
-
|
|
2450
|
+
quality,
|
|
2451
|
+
confidence: computeConfidence(input, now, quality),
|
|
1523
2452
|
summary,
|
|
1524
2453
|
usageGuide
|
|
1525
2454
|
};
|
|
@@ -1542,7 +2471,7 @@ async function runSync(opts = {}) {
|
|
|
1542
2471
|
ok.map(
|
|
1543
2472
|
(c) => buildEmbeddingText({
|
|
1544
2473
|
name: c.target.name,
|
|
1545
|
-
category: c.
|
|
2474
|
+
category: c.category,
|
|
1546
2475
|
summary: c.summary,
|
|
1547
2476
|
description: c.signals.registry?.description ?? null
|
|
1548
2477
|
})
|
|
@@ -1553,21 +2482,23 @@ async function runSync(opts = {}) {
|
|
|
1553
2482
|
const c = ok[i];
|
|
1554
2483
|
const efficiency = computeEfficiency(
|
|
1555
2484
|
c.input.bundleMinGzipKb,
|
|
1556
|
-
c.
|
|
1557
|
-
c.
|
|
2485
|
+
c.category,
|
|
2486
|
+
c.category ? medians.get(c.category) ?? null : null
|
|
1558
2487
|
);
|
|
1559
2488
|
const breakdown = {
|
|
1560
2489
|
maintenance: c.maintenance,
|
|
1561
2490
|
adoption: c.adoption,
|
|
1562
2491
|
reliability: c.reliability,
|
|
1563
|
-
efficiency
|
|
2492
|
+
efficiency,
|
|
2493
|
+
quality: c.quality
|
|
1564
2494
|
};
|
|
1565
2495
|
const healthScore = computeHealthScore(breakdown);
|
|
1566
2496
|
await upsertPackage(
|
|
1567
2497
|
handle.db,
|
|
1568
2498
|
assemblePackageRow({
|
|
1569
2499
|
name: c.target.name,
|
|
1570
|
-
category: c.
|
|
2500
|
+
category: c.category,
|
|
2501
|
+
categorySource: c.categorySource,
|
|
1571
2502
|
signals: c.signals,
|
|
1572
2503
|
input: c.input,
|
|
1573
2504
|
summary: c.summary,
|
|
@@ -1575,7 +2506,9 @@ async function runSync(opts = {}) {
|
|
|
1575
2506
|
confidence: c.confidence,
|
|
1576
2507
|
breakdown,
|
|
1577
2508
|
healthScore,
|
|
2509
|
+
qualityScore: c.quality,
|
|
1578
2510
|
embedding: embeddings[i] ?? null,
|
|
2511
|
+
embeddingProvider: embProvider.id,
|
|
1579
2512
|
now
|
|
1580
2513
|
})
|
|
1581
2514
|
);
|
|
@@ -1589,6 +2522,7 @@ async function runSync(opts = {}) {
|
|
|
1589
2522
|
status
|
|
1590
2523
|
});
|
|
1591
2524
|
logger.info(`Sync ${status}: ${updated}/${targets.length} updated, ${allErrors.length} source errors.`);
|
|
2525
|
+
if (updated > 0) await invalidateCache();
|
|
1592
2526
|
return { seen: targets.length, updated, errors: allErrors.length, status };
|
|
1593
2527
|
} catch (err) {
|
|
1594
2528
|
await finishSyncRun(handle.db, runId, {
|
|
@@ -1614,7 +2548,7 @@ async function resolveTargets(db, opts) {
|
|
|
1614
2548
|
function computeCategoryMedians(computed) {
|
|
1615
2549
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1616
2550
|
for (const c of computed) {
|
|
1617
|
-
const cat = c.
|
|
2551
|
+
const cat = c.category;
|
|
1618
2552
|
const kb = c.input.bundleMinGzipKb;
|
|
1619
2553
|
if (cat && isFrontendCategory(cat) && kb !== null) {
|
|
1620
2554
|
const list = byCategory.get(cat) ?? [];
|
|
@@ -1635,6 +2569,7 @@ function assemblePackageRow(p) {
|
|
|
1635
2569
|
name: p.name,
|
|
1636
2570
|
ecosystem: "npm",
|
|
1637
2571
|
category: p.category,
|
|
2572
|
+
categorySource: p.categorySource,
|
|
1638
2573
|
description: r?.description ?? null,
|
|
1639
2574
|
summary: p.summary,
|
|
1640
2575
|
repoUrl: r?.repoUrl ?? null,
|
|
@@ -1647,18 +2582,22 @@ function assemblePackageRow(p) {
|
|
|
1647
2582
|
lastReleaseAt: p.input.lastReleaseAt,
|
|
1648
2583
|
weeklyDownloads: p.input.weeklyDownloads,
|
|
1649
2584
|
downloadGrowth90d: p.input.downloadGrowth90d,
|
|
1650
|
-
dependentsCount: p.input.dependentsCount,
|
|
1651
2585
|
stars: p.input.stars,
|
|
1652
2586
|
openIssues: p.input.openIssues,
|
|
1653
2587
|
closedIssues: p.input.closedIssues,
|
|
1654
2588
|
scorecard: p.input.scorecard,
|
|
1655
2589
|
bundleMinGzipKb: p.input.bundleMinGzipKb,
|
|
1656
2590
|
advisories: p.input.advisories,
|
|
2591
|
+
peerDependencies: r?.peerDependencies ?? null,
|
|
2592
|
+
peerDependenciesMeta: r?.peerDependenciesMeta ?? null,
|
|
2593
|
+
engines: r?.engines ?? null,
|
|
1657
2594
|
healthScore: p.healthScore,
|
|
2595
|
+
qualityScore: p.qualityScore,
|
|
1658
2596
|
confidence: p.confidence,
|
|
1659
2597
|
scoreBreakdown: p.breakdown,
|
|
1660
2598
|
usageGuide: p.usageGuide,
|
|
1661
2599
|
embedding: p.embedding,
|
|
2600
|
+
embeddingProvider: p.embedding ? p.embeddingProvider : null,
|
|
1662
2601
|
dataAsOf: p.now
|
|
1663
2602
|
};
|
|
1664
2603
|
}
|
|
@@ -1666,6 +2605,7 @@ var init_sync = __esm({
|
|
|
1666
2605
|
"src/pipeline/sync.ts"() {
|
|
1667
2606
|
"use strict";
|
|
1668
2607
|
init_esm_shims();
|
|
2608
|
+
init_cache();
|
|
1669
2609
|
init_config();
|
|
1670
2610
|
init_concurrency();
|
|
1671
2611
|
init_http();
|
|
@@ -1673,6 +2613,7 @@ var init_sync = __esm({
|
|
|
1673
2613
|
init_collect();
|
|
1674
2614
|
init_npmDownloads();
|
|
1675
2615
|
init_summarize();
|
|
2616
|
+
init_categoryInference();
|
|
1676
2617
|
init_embeddings();
|
|
1677
2618
|
init_scoring();
|
|
1678
2619
|
init_client();
|
|
@@ -1682,40 +2623,56 @@ var init_sync = __esm({
|
|
|
1682
2623
|
});
|
|
1683
2624
|
|
|
1684
2625
|
// src/pipeline/single.ts
|
|
1685
|
-
import { and, eq as
|
|
2626
|
+
import { and as and3, eq as eq3, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
|
|
1686
2627
|
async function getSeedCategory(db, name) {
|
|
1687
|
-
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(
|
|
2628
|
+
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq3(seedPackages.name, name)).limit(1);
|
|
1688
2629
|
return row?.category ?? null;
|
|
1689
2630
|
}
|
|
1690
2631
|
async function getCategoryMedianBundle(db, category) {
|
|
1691
2632
|
const [row] = await db.select({
|
|
1692
2633
|
m: sql2`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
1693
|
-
}).from(packages).where(
|
|
2634
|
+
}).from(packages).where(and3(eq3(packages.category, category), isNotNull2(packages.bundleMinGzipKb)));
|
|
1694
2635
|
return row?.m ?? null;
|
|
1695
2636
|
}
|
|
1696
2637
|
async function syncOnePackage(db, name, opts = {}) {
|
|
1697
2638
|
const config = getConfig();
|
|
1698
2639
|
const now = /* @__PURE__ */ new Date();
|
|
1699
2640
|
const existing = await getPackageByName(db, name);
|
|
1700
|
-
const
|
|
2641
|
+
const curatedCategory = opts.category ?? await getSeedCategory(db, name) ?? null;
|
|
2642
|
+
const initialCategory = curatedCategory ?? existing?.category ?? null;
|
|
1701
2643
|
const prefetchedWeekly = await fetchWeeklyDownloads(name).catch(() => null);
|
|
1702
|
-
const signals = await collectSignals(name,
|
|
2644
|
+
const signals = await collectSignals(name, initialCategory, {
|
|
1703
2645
|
githubToken: config.GITHUB_TOKEN,
|
|
1704
2646
|
prefetchedWeekly
|
|
1705
2647
|
});
|
|
2648
|
+
const summaryInput = await buildSummaryInput(signals, initialCategory);
|
|
2649
|
+
const { summary, usageGuide, inferredCategory } = await createSummaryProvider().generate(summaryInput);
|
|
2650
|
+
let category;
|
|
2651
|
+
let categorySource;
|
|
2652
|
+
if (curatedCategory) {
|
|
2653
|
+
category = curatedCategory;
|
|
2654
|
+
categorySource = "curated";
|
|
2655
|
+
} else if (existing?.category) {
|
|
2656
|
+
category = existing.category;
|
|
2657
|
+
categorySource = existing.categorySource ?? "inferred";
|
|
2658
|
+
} else {
|
|
2659
|
+
category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
|
|
2660
|
+
categorySource = category ? "inferred" : null;
|
|
2661
|
+
}
|
|
1706
2662
|
const input = toScoringInput(signals, category);
|
|
1707
|
-
const summaryInput = await buildSummaryInput(signals, category);
|
|
1708
|
-
const { summary, usageGuide } = await createSummaryProvider().generate(summaryInput);
|
|
1709
2663
|
const median2 = category ? await getCategoryMedianBundle(db, category) : null;
|
|
2664
|
+
const quality = computeQuality(input);
|
|
1710
2665
|
const breakdown = {
|
|
1711
2666
|
maintenance: computeMaintenance(input, now),
|
|
1712
2667
|
adoption: computeAdoption(input),
|
|
1713
2668
|
reliability: computeReliability(input),
|
|
1714
|
-
efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2)
|
|
2669
|
+
efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
|
|
2670
|
+
quality
|
|
1715
2671
|
};
|
|
1716
2672
|
const healthScore = computeHealthScore(breakdown);
|
|
1717
|
-
const confidence = computeConfidence(input, now);
|
|
1718
|
-
const
|
|
2673
|
+
const confidence = computeConfidence(input, now, quality);
|
|
2674
|
+
const embProvider = createEmbeddingProvider();
|
|
2675
|
+
const [embedding] = await embProvider.embed([
|
|
1719
2676
|
buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
|
|
1720
2677
|
]);
|
|
1721
2678
|
await upsertPackage(
|
|
@@ -1723,6 +2680,7 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
1723
2680
|
assemblePackageRow({
|
|
1724
2681
|
name,
|
|
1725
2682
|
category,
|
|
2683
|
+
categorySource,
|
|
1726
2684
|
signals,
|
|
1727
2685
|
input,
|
|
1728
2686
|
summary,
|
|
@@ -1730,10 +2688,16 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
1730
2688
|
confidence,
|
|
1731
2689
|
breakdown,
|
|
1732
2690
|
healthScore,
|
|
2691
|
+
qualityScore: quality,
|
|
1733
2692
|
embedding: embedding ?? null,
|
|
2693
|
+
embeddingProvider: embProvider.id,
|
|
1734
2694
|
now
|
|
1735
2695
|
})
|
|
1736
2696
|
);
|
|
2697
|
+
await upsertPackageVersions(db, name, signals.registry?.versionTimeline ?? []).catch(
|
|
2698
|
+
() => {
|
|
2699
|
+
}
|
|
2700
|
+
);
|
|
1737
2701
|
return await getPackageByName(db, name);
|
|
1738
2702
|
}
|
|
1739
2703
|
async function getOrFetchPackage(db, name) {
|
|
@@ -1742,7 +2706,9 @@ async function getOrFetchPackage(db, name) {
|
|
|
1742
2706
|
const exists = await npmPackageExists(name);
|
|
1743
2707
|
if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
|
|
1744
2708
|
const row = await syncOnePackage(db, name);
|
|
1745
|
-
|
|
2709
|
+
if (row.confidence && row.confidence !== "unproven") {
|
|
2710
|
+
await ensureSeedEntry(db, name, row.category);
|
|
2711
|
+
}
|
|
1746
2712
|
return { row, wasTracked: false, existsOnNpm: true };
|
|
1747
2713
|
}
|
|
1748
2714
|
var init_single = __esm({
|
|
@@ -1753,6 +2719,7 @@ var init_single = __esm({
|
|
|
1753
2719
|
init_collect();
|
|
1754
2720
|
init_sources();
|
|
1755
2721
|
init_summarize();
|
|
2722
|
+
init_categoryInference();
|
|
1756
2723
|
init_scoring();
|
|
1757
2724
|
init_embeddings();
|
|
1758
2725
|
init_packages();
|
|
@@ -1761,103 +2728,87 @@ var init_single = __esm({
|
|
|
1761
2728
|
}
|
|
1762
2729
|
});
|
|
1763
2730
|
|
|
1764
|
-
// src/search/categoryInference.ts
|
|
1765
|
-
function inferCategory(need) {
|
|
1766
|
-
const text2 = need.toLowerCase();
|
|
1767
|
-
for (const rule of RULES) {
|
|
1768
|
-
if (rule.patterns.test(text2)) return rule.category;
|
|
1769
|
-
}
|
|
1770
|
-
return null;
|
|
1771
|
-
}
|
|
1772
|
-
var RULES;
|
|
1773
|
-
var init_categoryInference = __esm({
|
|
1774
|
-
"src/search/categoryInference.ts"() {
|
|
1775
|
-
"use strict";
|
|
1776
|
-
init_esm_shims();
|
|
1777
|
-
RULES = [
|
|
1778
|
-
{ category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
|
|
1779
|
-
{ category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
|
|
1780
|
-
{ category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
|
|
1781
|
-
{ category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
|
|
1782
|
-
{ category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
|
|
1783
|
-
{ category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
|
|
1784
|
-
{ category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
|
|
1785
|
-
{ category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
|
|
1786
|
-
{ category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
|
|
1787
|
-
{ category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
|
|
1788
|
-
{ category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
|
|
1789
|
-
{ category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
|
|
1790
|
-
{ category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
|
|
1791
|
-
{ category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
|
|
1792
|
-
{ category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
|
|
1793
|
-
// date-time precedes linting: "date formatting" must not be captured by linting's
|
|
1794
|
-
// bare "formatting" token before the date rule is reached (first-match-wins).
|
|
1795
|
-
{ category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
|
|
1796
|
-
{ category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
|
|
1797
|
-
{ category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
|
|
1798
|
-
{ category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
|
|
1799
|
-
{ category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
|
|
1800
|
-
{ category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
|
|
1801
|
-
{ category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
|
|
1802
|
-
];
|
|
1803
|
-
}
|
|
1804
|
-
});
|
|
1805
|
-
|
|
1806
2731
|
// src/search/recommend.ts
|
|
1807
|
-
import { and as
|
|
2732
|
+
import { and as and4, cosineDistance, eq as eq4, isNotNull as isNotNull3, lte, sql as sql3 } from "drizzle-orm";
|
|
1808
2733
|
async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
1809
2734
|
const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
|
|
1810
2735
|
const [queryVec] = await provider.embed([opts.need]);
|
|
1811
2736
|
if (!queryVec) return [];
|
|
1812
2737
|
const category = opts.category ?? inferCategory(opts.need);
|
|
1813
2738
|
const pool = Math.max(limit * 5, 25);
|
|
1814
|
-
let
|
|
1815
|
-
if (category &&
|
|
1816
|
-
const broad = await
|
|
1817
|
-
const seen = new Set(
|
|
1818
|
-
|
|
1819
|
-
}
|
|
1820
|
-
if (
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2739
|
+
let fused = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, category, pool);
|
|
2740
|
+
if (category && fused.length < limit) {
|
|
2741
|
+
const broad = await hybridSearch(db, queryVec, provider.id, opts.need, opts.constraints, null, pool);
|
|
2742
|
+
const seen = new Set(fused.map((f) => f.row.name));
|
|
2743
|
+
fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
|
|
2744
|
+
}
|
|
2745
|
+
if (fused.length === 0) return [];
|
|
2746
|
+
const lambda = loadWeights().composite.lambda;
|
|
2747
|
+
const maxRrf = Math.max(...fused.map((f) => f.rrf));
|
|
2748
|
+
const ranked = fused.map((f) => {
|
|
2749
|
+
const relevance = maxRrf > 0 ? f.rrf / maxRrf : 0;
|
|
2750
|
+
const composite = computeComposite(f.row.healthScore ?? 0, f.row.qualityScore, lambda) / 100;
|
|
2751
|
+
return { row: f.row, score: RELEVANCE_WEIGHT * relevance + COMPOSITE_WEIGHT * composite };
|
|
1825
2752
|
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1826
2753
|
return ranked.map(({ row }) => toCandidate(row));
|
|
1827
2754
|
}
|
|
1828
|
-
async function
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2755
|
+
async function hybridSearch(db, queryVec, providerId, need, constraints, category, pool) {
|
|
2756
|
+
const [vectorRows, lexicalRows] = await Promise.all([
|
|
2757
|
+
runVectorQuery(db, queryVec, providerId, constraints, category, pool),
|
|
2758
|
+
runLexicalQuery(db, need, constraints, category, pool)
|
|
2759
|
+
]);
|
|
2760
|
+
return rrfFuse([vectorRows, lexicalRows]);
|
|
2761
|
+
}
|
|
2762
|
+
function rrfFuse(lists, k = RRF_K) {
|
|
2763
|
+
const fused = /* @__PURE__ */ new Map();
|
|
2764
|
+
for (const list of lists) {
|
|
2765
|
+
list.forEach((row, idx) => {
|
|
2766
|
+
const contribution = 1 / (k + idx + 1);
|
|
2767
|
+
const prev = fused.get(row.name);
|
|
2768
|
+
if (prev) prev.rrf += contribution;
|
|
2769
|
+
else fused.set(row.name, { row, rrf: contribution });
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
return [...fused.values()].sort((a, b) => b.rrf - a.rrf);
|
|
2773
|
+
}
|
|
2774
|
+
function buildConditions(constraints, category) {
|
|
2775
|
+
const conditions = [];
|
|
2776
|
+
if (category) conditions.push(eq4(packages.category, category));
|
|
2777
|
+
if (constraints?.license) conditions.push(eq4(packages.license, constraints.license));
|
|
1833
2778
|
if (constraints?.maxBundleKb !== void 0) {
|
|
1834
2779
|
conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
|
|
1835
2780
|
}
|
|
1836
2781
|
if (constraints?.minConfidence) {
|
|
1837
|
-
const allowed = ["proven", "emerging", "unproven"].filter(
|
|
2782
|
+
const allowed = ["proven", "emerging", "promising", "unproven"].filter(
|
|
1838
2783
|
(c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
|
|
1839
2784
|
);
|
|
1840
2785
|
conditions.push(
|
|
1841
2786
|
sql3`${packages.confidence} in ${sql3.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
|
|
1842
2787
|
);
|
|
1843
2788
|
}
|
|
1844
|
-
return
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2789
|
+
return conditions;
|
|
2790
|
+
}
|
|
2791
|
+
async function runVectorQuery(db, queryVec, providerId, constraints, category, pool) {
|
|
2792
|
+
const distance = cosineDistance(packages.embedding, queryVec);
|
|
2793
|
+
const conditions = [
|
|
2794
|
+
isNotNull3(packages.embedding),
|
|
2795
|
+
eq4(packages.embeddingProvider, providerId),
|
|
2796
|
+
...buildConditions(constraints, category)
|
|
2797
|
+
];
|
|
2798
|
+
return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(distance).limit(pool);
|
|
2799
|
+
}
|
|
2800
|
+
async function runLexicalQuery(db, need, constraints, category, pool) {
|
|
2801
|
+
const tsquery = sql3`websearch_to_tsquery('english', ${need})`;
|
|
2802
|
+
const rank = sql3`ts_rank(${packages.searchVector}, ${tsquery})`;
|
|
2803
|
+
const conditions = [sql3`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
|
|
2804
|
+
return db.select(ROW_COLUMNS).from(packages).where(and4(...conditions)).orderBy(sql3`${rank} desc`).limit(pool);
|
|
1855
2805
|
}
|
|
1856
2806
|
function toCandidate(row) {
|
|
1857
2807
|
return {
|
|
1858
2808
|
name: row.name,
|
|
1859
2809
|
category: row.category,
|
|
1860
2810
|
healthScore: row.healthScore ?? 0,
|
|
2811
|
+
qualityScore: row.qualityScore,
|
|
1861
2812
|
confidence: row.confidence ?? "unproven",
|
|
1862
2813
|
why: buildWhy(row),
|
|
1863
2814
|
latestVersion: row.latestVersion,
|
|
@@ -1878,26 +2829,132 @@ function formatDownloads(n) {
|
|
|
1878
2829
|
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
1879
2830
|
return String(n);
|
|
1880
2831
|
}
|
|
1881
|
-
var CONFIDENCE_RANK,
|
|
2832
|
+
var CONFIDENCE_RANK, RELEVANCE_WEIGHT, COMPOSITE_WEIGHT, RRF_K, ROW_COLUMNS;
|
|
1882
2833
|
var init_recommend = __esm({
|
|
1883
2834
|
"src/search/recommend.ts"() {
|
|
1884
2835
|
"use strict";
|
|
1885
2836
|
init_esm_shims();
|
|
1886
2837
|
init_schema();
|
|
2838
|
+
init_scoring();
|
|
2839
|
+
init_weights();
|
|
1887
2840
|
init_categoryInference();
|
|
1888
2841
|
init_embeddings();
|
|
1889
|
-
CONFIDENCE_RANK = {
|
|
1890
|
-
|
|
1891
|
-
|
|
2842
|
+
CONFIDENCE_RANK = {
|
|
2843
|
+
unproven: 0,
|
|
2844
|
+
promising: 1,
|
|
2845
|
+
emerging: 2,
|
|
2846
|
+
proven: 3
|
|
2847
|
+
};
|
|
2848
|
+
RELEVANCE_WEIGHT = 0.6;
|
|
2849
|
+
COMPOSITE_WEIGHT = 0.4;
|
|
2850
|
+
RRF_K = 60;
|
|
2851
|
+
ROW_COLUMNS = {
|
|
2852
|
+
name: packages.name,
|
|
2853
|
+
category: packages.category,
|
|
2854
|
+
healthScore: packages.healthScore,
|
|
2855
|
+
qualityScore: packages.qualityScore,
|
|
2856
|
+
confidence: packages.confidence,
|
|
2857
|
+
latestVersion: packages.latestVersion,
|
|
2858
|
+
weeklyDownloads: packages.weeklyDownloads,
|
|
2859
|
+
lastReleaseAt: packages.lastReleaseAt,
|
|
2860
|
+
repoUrl: packages.repoUrl
|
|
2861
|
+
};
|
|
1892
2862
|
}
|
|
1893
2863
|
});
|
|
1894
2864
|
|
|
1895
|
-
// src/
|
|
2865
|
+
// src/security/risk.ts
|
|
2866
|
+
function assessRisk(i) {
|
|
2867
|
+
const malwarePattern = i.installScripts && i.brandNew && i.lowTrust;
|
|
2868
|
+
if (i.typosquat || i.hasCriticalOrHighAdvisory || malwarePattern) return "high";
|
|
2869
|
+
if (i.deprecatedOrArchived || i.installScripts && (i.lowTrust || i.brandNew) || i.lowTrust && i.flags.includes("single-maintainer")) {
|
|
2870
|
+
return "medium";
|
|
2871
|
+
}
|
|
2872
|
+
return "low";
|
|
2873
|
+
}
|
|
2874
|
+
var init_risk = __esm({
|
|
2875
|
+
"src/security/risk.ts"() {
|
|
2876
|
+
"use strict";
|
|
2877
|
+
init_esm_shims();
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
|
|
2881
|
+
// src/security/typosquat.ts
|
|
2882
|
+
function bareName(name) {
|
|
2883
|
+
const slash = name.indexOf("/");
|
|
2884
|
+
return (slash >= 0 ? name.slice(slash + 1) : name).toLowerCase();
|
|
2885
|
+
}
|
|
2886
|
+
function editDistance(a, b) {
|
|
2887
|
+
const m = a.length;
|
|
2888
|
+
const n = b.length;
|
|
2889
|
+
if (m === 0) return n;
|
|
2890
|
+
if (n === 0) return m;
|
|
2891
|
+
let prevPrev = new Array(n + 1).fill(0);
|
|
2892
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2893
|
+
for (let i = 1; i <= m; i++) {
|
|
2894
|
+
const cur = new Array(n + 1).fill(0);
|
|
2895
|
+
cur[0] = i;
|
|
2896
|
+
for (let j = 1; j <= n; j++) {
|
|
2897
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2898
|
+
let val = Math.min(
|
|
2899
|
+
(prev[j] ?? 0) + 1,
|
|
2900
|
+
(cur[j - 1] ?? 0) + 1,
|
|
2901
|
+
(prev[j - 1] ?? 0) + cost
|
|
2902
|
+
);
|
|
2903
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
2904
|
+
val = Math.min(val, (prevPrev[j - 2] ?? 0) + 1);
|
|
2905
|
+
}
|
|
2906
|
+
cur[j] = val;
|
|
2907
|
+
}
|
|
2908
|
+
prevPrev = prev;
|
|
2909
|
+
prev = cur;
|
|
2910
|
+
}
|
|
2911
|
+
return prev[n] ?? 0;
|
|
2912
|
+
}
|
|
2913
|
+
function detectTyposquat(name, popular, maxDistance = 2) {
|
|
2914
|
+
const target = bareName(name);
|
|
2915
|
+
if (target.length < 4) return null;
|
|
2916
|
+
if (popular.some((p) => p.toLowerCase() === name.toLowerCase())) return null;
|
|
2917
|
+
let best = null;
|
|
2918
|
+
for (const p of popular) {
|
|
2919
|
+
const cand = bareName(p);
|
|
2920
|
+
if (cand === target) continue;
|
|
2921
|
+
if (Math.abs(cand.length - target.length) > maxDistance) continue;
|
|
2922
|
+
const dist = editDistance(target, cand);
|
|
2923
|
+
if (dist >= 1 && dist <= maxDistance && (!best || dist < best.distance)) {
|
|
2924
|
+
best = { target: p, distance: dist };
|
|
2925
|
+
if (dist === 1) break;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return best;
|
|
2929
|
+
}
|
|
2930
|
+
var init_typosquat = __esm({
|
|
2931
|
+
"src/security/typosquat.ts"() {
|
|
2932
|
+
"use strict";
|
|
2933
|
+
init_esm_shims();
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
// src/mcp/handlers.ts
|
|
2938
|
+
var handlers_exports = {};
|
|
2939
|
+
__export(handlers_exports, {
|
|
2940
|
+
handleCompare: () => handleCompare,
|
|
2941
|
+
handleCompat: () => handleCompat,
|
|
2942
|
+
handleEvaluate: () => handleEvaluate,
|
|
2943
|
+
handleRecommend: () => handleRecommend,
|
|
2944
|
+
handleVerify: () => handleVerify,
|
|
2945
|
+
latestDataAsOf: () => latestDataAsOf,
|
|
2946
|
+
rowToEvaluate: () => rowToEvaluate
|
|
2947
|
+
});
|
|
2948
|
+
import { createHash as createHash3 } from "crypto";
|
|
1896
2949
|
import { sql as sql4 } from "drizzle-orm";
|
|
1897
2950
|
function isStale(dataAsOf) {
|
|
1898
2951
|
if (!dataAsOf) return true;
|
|
1899
2952
|
return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
|
|
1900
2953
|
}
|
|
2954
|
+
function refreshStale(out) {
|
|
2955
|
+
out.stale = isStale(out.dataAsOf ? new Date(out.dataAsOf) : null) || void 0;
|
|
2956
|
+
return out;
|
|
2957
|
+
}
|
|
1901
2958
|
function withinDays(date, days) {
|
|
1902
2959
|
return date ? Date.now() - date.getTime() <= days * DAY_MS2 : false;
|
|
1903
2960
|
}
|
|
@@ -1910,7 +2967,8 @@ function rowToEvaluate(row) {
|
|
|
1910
2967
|
maintenance: 0,
|
|
1911
2968
|
adoption: 0,
|
|
1912
2969
|
reliability: 0,
|
|
1913
|
-
efficiency: null
|
|
2970
|
+
efficiency: null,
|
|
2971
|
+
quality: null
|
|
1914
2972
|
};
|
|
1915
2973
|
return {
|
|
1916
2974
|
dataAsOf: (row.dataAsOf ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1918,13 +2976,13 @@ function rowToEvaluate(row) {
|
|
|
1918
2976
|
name: row.name,
|
|
1919
2977
|
category: row.category,
|
|
1920
2978
|
healthScore: row.healthScore ?? 0,
|
|
2979
|
+
qualityScore: row.qualityScore ?? null,
|
|
1921
2980
|
confidence: row.confidence ?? "unproven",
|
|
1922
2981
|
scoreBreakdown: breakdown,
|
|
1923
2982
|
latestVersion: row.latestVersion,
|
|
1924
2983
|
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
1925
2984
|
weeklyDownloads: row.weeklyDownloads,
|
|
1926
2985
|
downloadGrowth90d: row.downloadGrowth90d,
|
|
1927
|
-
dependentsCount: row.dependentsCount,
|
|
1928
2986
|
scorecard: row.scorecard,
|
|
1929
2987
|
bundleMinGzipKb: row.bundleMinGzipKb,
|
|
1930
2988
|
deprecated: row.deprecated,
|
|
@@ -1939,37 +2997,80 @@ async function latestDataAsOf(db) {
|
|
|
1939
2997
|
const [row] = await db.select({ m: sql4`max(${packages.dataAsOf})` }).from(packages);
|
|
1940
2998
|
return new Date(row?.m ?? Date.now()).toISOString();
|
|
1941
2999
|
}
|
|
3000
|
+
function cacheKey(parts) {
|
|
3001
|
+
return createHash3("sha1").update(JSON.stringify(parts)).digest("hex").slice(0, 24);
|
|
3002
|
+
}
|
|
1942
3003
|
async function handleRecommend(db, input) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
3004
|
+
return cached2(
|
|
3005
|
+
"rec",
|
|
3006
|
+
cacheKey([input.need, input.category ?? null, input.constraints ?? null]),
|
|
3007
|
+
async () => {
|
|
3008
|
+
const candidates = await recommend(db, {
|
|
3009
|
+
need: input.need,
|
|
3010
|
+
category: input.category,
|
|
3011
|
+
constraints: input.constraints,
|
|
3012
|
+
limit: 5
|
|
3013
|
+
});
|
|
3014
|
+
return { dataAsOf: await latestDataAsOf(db), candidates };
|
|
3015
|
+
},
|
|
3016
|
+
// Don't cache empty results — the index may still be populating.
|
|
3017
|
+
{ skipCache: (r) => r.candidates.length === 0 }
|
|
3018
|
+
);
|
|
1950
3019
|
}
|
|
1951
3020
|
async function handleEvaluate(db, input) {
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
3021
|
+
const out = await cached2(
|
|
3022
|
+
"eval",
|
|
3023
|
+
cacheKey([input.package]),
|
|
3024
|
+
async () => {
|
|
3025
|
+
const { row, existsOnNpm } = await getOrFetchPackage(db, input.package);
|
|
3026
|
+
if (!row) {
|
|
3027
|
+
return {
|
|
3028
|
+
tracked: false,
|
|
3029
|
+
suggestion: existsOnNpm ? `"${input.package}" exists on npm but could not be scored right now; try again.` : `"${input.package}" was not found on the npm registry. Check the package name.`
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
const evaluated = rowToEvaluate(row);
|
|
3033
|
+
const verification = await getLatestVerificationByName(db, row.name);
|
|
3034
|
+
return verification ? { ...evaluated, buildVerified: toBuildVerified(verification) } : evaluated;
|
|
3035
|
+
},
|
|
3036
|
+
// Don't cache "not found / not scored yet" — it may resolve on a later fetch.
|
|
3037
|
+
{ skipCache: (r) => "tracked" in r }
|
|
3038
|
+
);
|
|
3039
|
+
return "tracked" in out ? out : refreshStale(out);
|
|
1960
3040
|
}
|
|
1961
3041
|
async function handleCompare(db, input) {
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
3042
|
+
const out = await cached2(
|
|
3043
|
+
"cmp",
|
|
3044
|
+
cacheKey(input.packages),
|
|
3045
|
+
async () => {
|
|
3046
|
+
const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
|
|
3047
|
+
const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
|
|
3048
|
+
const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
|
|
3049
|
+
return { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
|
|
3050
|
+
},
|
|
3051
|
+
// Don't cache a transient miss (a package that momentarily failed to fetch).
|
|
3052
|
+
{ skipCache: (r) => Boolean(r.missing?.length) }
|
|
1966
3053
|
);
|
|
1967
|
-
|
|
3054
|
+
out.rows = out.rows.map(refreshStale);
|
|
3055
|
+
return out;
|
|
3056
|
+
}
|
|
3057
|
+
function toBuildVerified(v) {
|
|
3058
|
+
return {
|
|
3059
|
+
version: v.version,
|
|
3060
|
+
installed: v.installed,
|
|
3061
|
+
loaded: v.imported,
|
|
3062
|
+
driver: v.driver,
|
|
3063
|
+
ranAt: v.ranAt ? v.ranAt.toISOString() : ""
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
async function handleCompat(db, input) {
|
|
3067
|
+
return checkCompat(db, input.packages);
|
|
1968
3068
|
}
|
|
1969
3069
|
async function handleVerify(db, input) {
|
|
1970
3070
|
const name = input.package;
|
|
1971
3071
|
const exists = await npmPackageExists(name);
|
|
1972
3072
|
if (!exists) {
|
|
3073
|
+
const typo2 = detectTyposquat(name, await getTopPackageNames(db).catch(() => []));
|
|
1973
3074
|
return {
|
|
1974
3075
|
exists: false,
|
|
1975
3076
|
tracked: false,
|
|
@@ -1977,25 +3078,46 @@ async function handleVerify(db, input) {
|
|
|
1977
3078
|
archived: false,
|
|
1978
3079
|
latestVersion: null,
|
|
1979
3080
|
weeklyDownloads: null,
|
|
1980
|
-
riskFlags: ["not-found-on-registry"],
|
|
3081
|
+
riskFlags: typo2 ? ["not-found-on-registry", `possible-typosquat-of:${typo2.target}`] : ["not-found-on-registry"],
|
|
3082
|
+
risk: "high",
|
|
3083
|
+
typosquatOf: typo2?.target ?? null,
|
|
1981
3084
|
confidence: null,
|
|
1982
3085
|
advisoryCount: 0
|
|
1983
3086
|
};
|
|
1984
3087
|
}
|
|
1985
|
-
const registry = await
|
|
1986
|
-
|
|
3088
|
+
const [registry, { row, wasTracked }, popular] = await Promise.all([
|
|
3089
|
+
fetchNpmRegistry(name).catch(() => null),
|
|
3090
|
+
getOrFetchPackage(db, name),
|
|
3091
|
+
getTopPackageNames(db).catch(() => [])
|
|
3092
|
+
]);
|
|
1987
3093
|
const weeklyDownloads = row?.weeklyDownloads ?? null;
|
|
1988
|
-
const
|
|
3094
|
+
const advisories = row?.advisories ?? [];
|
|
3095
|
+
const advisoryCount = advisories.length;
|
|
1989
3096
|
const deprecated = Boolean(row?.deprecated || registry?.deprecated);
|
|
1990
3097
|
const archived = Boolean(row?.archived);
|
|
3098
|
+
const brandNew = withinDays(registry?.firstPublishedAt ?? null, 7);
|
|
3099
|
+
const lowTrust = weeklyDownloads === null || weeklyDownloads < 1e3;
|
|
3100
|
+
const installScripts = registry?.hasInstallScripts ?? false;
|
|
3101
|
+
const typo = detectTyposquat(name, popular);
|
|
1991
3102
|
const riskFlags = [];
|
|
3103
|
+
if (typo) riskFlags.push(`possible-typosquat-of:${typo.target}`);
|
|
1992
3104
|
if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
|
|
1993
3105
|
else if (weeklyDownloads < 1e3) riskFlags.push("low-downloads");
|
|
1994
|
-
if (
|
|
3106
|
+
if (brandNew) riskFlags.push("published-within-7-days");
|
|
1995
3107
|
if (registry?.maintainersCount === 1) riskFlags.push("single-maintainer");
|
|
3108
|
+
if (installScripts) riskFlags.push("runs-install-scripts");
|
|
1996
3109
|
if (advisoryCount > 0) riskFlags.push("has-known-advisory");
|
|
1997
3110
|
if (deprecated) riskFlags.push("deprecated");
|
|
1998
3111
|
if (archived) riskFlags.push("archived");
|
|
3112
|
+
const risk = assessRisk({
|
|
3113
|
+
flags: riskFlags,
|
|
3114
|
+
hasCriticalOrHighAdvisory: hasCriticalOrHighAdvisory(advisories),
|
|
3115
|
+
typosquat: Boolean(typo),
|
|
3116
|
+
installScripts,
|
|
3117
|
+
brandNew,
|
|
3118
|
+
lowTrust,
|
|
3119
|
+
deprecatedOrArchived: deprecated || archived
|
|
3120
|
+
});
|
|
1999
3121
|
return {
|
|
2000
3122
|
exists: true,
|
|
2001
3123
|
tracked: wasTracked,
|
|
@@ -2004,6 +3126,8 @@ async function handleVerify(db, input) {
|
|
|
2004
3126
|
latestVersion: registry?.latestVersion ?? row?.latestVersion ?? null,
|
|
2005
3127
|
weeklyDownloads,
|
|
2006
3128
|
riskFlags,
|
|
3129
|
+
risk,
|
|
3130
|
+
typosquatOf: typo?.target ?? null,
|
|
2007
3131
|
confidence: row?.confidence ?? null,
|
|
2008
3132
|
advisoryCount
|
|
2009
3133
|
};
|
|
@@ -2013,12 +3137,19 @@ var init_handlers = __esm({
|
|
|
2013
3137
|
"src/mcp/handlers.ts"() {
|
|
2014
3138
|
"use strict";
|
|
2015
3139
|
init_esm_shims();
|
|
3140
|
+
init_cache();
|
|
2016
3141
|
init_constants();
|
|
3142
|
+
init_check();
|
|
3143
|
+
init_packages();
|
|
3144
|
+
init_verification();
|
|
2017
3145
|
init_schema();
|
|
2018
3146
|
init_sources();
|
|
2019
3147
|
init_summarize();
|
|
2020
3148
|
init_single();
|
|
3149
|
+
init_score();
|
|
2021
3150
|
init_recommend();
|
|
3151
|
+
init_risk();
|
|
3152
|
+
init_typosquat();
|
|
2022
3153
|
SEVERITY_RANK = {
|
|
2023
3154
|
critical: 4,
|
|
2024
3155
|
high: 3,
|
|
@@ -2031,7 +3162,7 @@ var init_handlers = __esm({
|
|
|
2031
3162
|
});
|
|
2032
3163
|
|
|
2033
3164
|
// src/mcp/diagram.ts
|
|
2034
|
-
import { inArray } from "drizzle-orm";
|
|
3165
|
+
import { inArray as inArray2 } from "drizzle-orm";
|
|
2035
3166
|
function layerFor(item) {
|
|
2036
3167
|
if (!item.category) return UNCLASSIFIED;
|
|
2037
3168
|
if (item.category === "framework" && BACKEND_FRAMEWORKS.has(item.label)) return "Backend";
|
|
@@ -2084,7 +3215,7 @@ async function handleDiagram(db, input) {
|
|
|
2084
3215
|
note: "Provide a `stack` of package names to diagram. lurq labels a stack you choose; it does not infer an architecture from a description."
|
|
2085
3216
|
};
|
|
2086
3217
|
}
|
|
2087
|
-
const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(
|
|
3218
|
+
const rows = await db.select({ name: packages.name, category: packages.category }).from(packages).where(inArray2(packages.name, input.stack));
|
|
2088
3219
|
const known = new Map(rows.map((r) => [r.name, r.category]));
|
|
2089
3220
|
const items = input.stack.map((name) => ({
|
|
2090
3221
|
label: name,
|
|
@@ -2138,10 +3269,407 @@ var init_diagram = __esm({
|
|
|
2138
3269
|
}
|
|
2139
3270
|
});
|
|
2140
3271
|
|
|
3272
|
+
// src/compat/optimize.ts
|
|
3273
|
+
function conflictsFor(members, sandboxConflicts) {
|
|
3274
|
+
const out = resolveArchitectureCompat(members);
|
|
3275
|
+
for (let i = 0; i < members.length; i++) {
|
|
3276
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
3277
|
+
const a = members[i].name;
|
|
3278
|
+
const b = members[j].name;
|
|
3279
|
+
const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
|
|
3280
|
+
if (sandboxConflicts.has(key)) {
|
|
3281
|
+
out.push({
|
|
3282
|
+
source: "sandbox",
|
|
3283
|
+
packages: [a, b],
|
|
3284
|
+
detail: `${a} and ${b} are recorded as incompatible (sandbox)`
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return out;
|
|
3290
|
+
}
|
|
3291
|
+
function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
|
|
3292
|
+
if (slots.some((s) => s.length === 0)) {
|
|
3293
|
+
throw new Error("optimizeStack: every slot must have at least one candidate");
|
|
3294
|
+
}
|
|
3295
|
+
const n = slots.length;
|
|
3296
|
+
const budget = 5e4;
|
|
3297
|
+
let nodes = 0;
|
|
3298
|
+
let bestSelection = new Array(n).fill(0);
|
|
3299
|
+
let bestRegret = conflictsFor(
|
|
3300
|
+
slots.map((c) => c[0]).filter((m) => Boolean(m)),
|
|
3301
|
+
sandboxConflicts
|
|
3302
|
+
).length === 0 ? 0 : Infinity;
|
|
3303
|
+
const chosen = new Array(n).fill(0);
|
|
3304
|
+
const dfs = (slot, regret) => {
|
|
3305
|
+
if (nodes++ > budget || regret >= bestRegret) return;
|
|
3306
|
+
if (slot === n) {
|
|
3307
|
+
bestSelection = chosen.slice();
|
|
3308
|
+
bestRegret = regret;
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
const candidates = slots[slot];
|
|
3312
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
3313
|
+
chosen[slot] = i;
|
|
3314
|
+
const assigned = chosen.slice(0, slot + 1).map((idx, s) => slots[s][idx]);
|
|
3315
|
+
if (conflictsFor(assigned, sandboxConflicts).length === 0) {
|
|
3316
|
+
dfs(slot + 1, regret + i);
|
|
3317
|
+
}
|
|
3318
|
+
if (nodes > budget) return;
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
dfs(0, 0);
|
|
3322
|
+
const members = bestSelection.map((idx, s) => slots[s][idx]);
|
|
3323
|
+
return {
|
|
3324
|
+
selection: bestSelection,
|
|
3325
|
+
conflicts: conflictsFor(members, sandboxConflicts),
|
|
3326
|
+
regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
var init_optimize = __esm({
|
|
3330
|
+
"src/compat/optimize.ts"() {
|
|
3331
|
+
"use strict";
|
|
3332
|
+
init_esm_shims();
|
|
3333
|
+
init_peerCompat();
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3337
|
+
// src/mcp/plan.ts
|
|
3338
|
+
var plan_exports = {};
|
|
3339
|
+
__export(plan_exports, {
|
|
3340
|
+
decomposeHeuristic: () => decomposeHeuristic,
|
|
3341
|
+
familyOf: () => familyOf,
|
|
3342
|
+
flagSlotConflicts: () => flagSlotConflicts,
|
|
3343
|
+
handlePlan: () => handlePlan,
|
|
3344
|
+
orderCandidates: () => orderCandidates,
|
|
3345
|
+
resolvePins: () => resolvePins
|
|
3346
|
+
});
|
|
3347
|
+
import { createHash as createHash4 } from "crypto";
|
|
3348
|
+
import { inArray as inArray3 } from "drizzle-orm";
|
|
3349
|
+
function packageToCandidate(row) {
|
|
3350
|
+
return {
|
|
3351
|
+
name: row.name,
|
|
3352
|
+
category: row.category,
|
|
3353
|
+
healthScore: row.healthScore ?? 0,
|
|
3354
|
+
qualityScore: row.qualityScore,
|
|
3355
|
+
confidence: row.confidence ?? "unproven",
|
|
3356
|
+
why: "pinned by you",
|
|
3357
|
+
latestVersion: row.latestVersion,
|
|
3358
|
+
weeklyDownloads: row.weeklyDownloads,
|
|
3359
|
+
lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
|
|
3360
|
+
repoUrl: row.repoUrl
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
async function resolvePins(db, using, recommendedNames) {
|
|
3364
|
+
const slots = [];
|
|
3365
|
+
const unresolved = [];
|
|
3366
|
+
for (const name of new Set(using ?? [])) {
|
|
3367
|
+
if (recommendedNames.has(name)) continue;
|
|
3368
|
+
const { row } = await getOrFetchPackage(db, name);
|
|
3369
|
+
if (!row) {
|
|
3370
|
+
unresolved.push(name);
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
slots.push({
|
|
3374
|
+
need: `using ${name}`,
|
|
3375
|
+
category: row.category,
|
|
3376
|
+
layer: layerFor({ label: name, category: row.category }),
|
|
3377
|
+
recommended: packageToCandidate(row),
|
|
3378
|
+
alternatives: [],
|
|
3379
|
+
// fixed: the user chose this, so it never gets swapped
|
|
3380
|
+
note: "pinned by you"
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
return { slots, unresolved };
|
|
3384
|
+
}
|
|
3385
|
+
async function handlePlan(db, input) {
|
|
3386
|
+
const optimize = input.optimize ?? "balanced";
|
|
3387
|
+
const decomposed = input.needs?.length ? { needs: dedupeNeeds(input.needs), source: "needs" } : input.document?.trim() ? await decompose(input.document) : null;
|
|
3388
|
+
const hasPins = Boolean(input.using?.length);
|
|
3389
|
+
if ((!decomposed || decomposed.needs.length === 0) && !hasPins) {
|
|
3390
|
+
return {
|
|
3391
|
+
note: "Provide a `document` (a detailed description of your program), a `needs` array, or a `using` list of packages you have already chosen. lurq recommends evidence-scored packages per component \u2014 it does not invent an architecture from a bare prompt."
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
const needs = (decomposed?.needs ?? []).slice(0, MAX_SLOTS);
|
|
3395
|
+
const source = decomposed?.source ?? "needs";
|
|
3396
|
+
const safeRecommend = (need, category) => recommend(db, { need, category, limit: PER_SLOT }).catch((err) => {
|
|
3397
|
+
logger.warn(`plan: recommend failed for "${need}": ${err.message}`);
|
|
3398
|
+
return [];
|
|
3399
|
+
});
|
|
3400
|
+
const effCat = (n) => n.category ?? inferCategory(n.need) ?? void 0;
|
|
3401
|
+
const anchorFamily = familyOf(`${input.document ?? ""} ${needs.map((n) => n.need).join(" ")}`);
|
|
3402
|
+
const metaIdx = needs.findIndex((n) => effCat(n) === "meta-framework");
|
|
3403
|
+
const anchorIdx = metaIdx >= 0 ? metaIdx : needs.findIndex((n) => effCat(n) === "framework");
|
|
3404
|
+
const recs = new Array(needs.length);
|
|
3405
|
+
let framework = anchorFamily;
|
|
3406
|
+
if (anchorIdx >= 0) {
|
|
3407
|
+
recs[anchorIdx] = await safeRecommend(needs[anchorIdx].need, effCat(needs[anchorIdx]));
|
|
3408
|
+
framework = recs[anchorIdx][0]?.name ?? anchorFamily;
|
|
3409
|
+
}
|
|
3410
|
+
const [, dataAsOf] = await Promise.all([
|
|
3411
|
+
Promise.all(
|
|
3412
|
+
needs.map(async (n, i) => {
|
|
3413
|
+
if (i === anchorIdx) return;
|
|
3414
|
+
const need = framework ? `${n.need} (for a ${framework} app)` : n.need;
|
|
3415
|
+
recs[i] = await safeRecommend(need, effCat(n));
|
|
3416
|
+
})
|
|
3417
|
+
),
|
|
3418
|
+
latestDataAsOf(db)
|
|
3419
|
+
]);
|
|
3420
|
+
const bundleByName = optimize === "speed" ? await bundleSizes(db, recs.flat()) : /* @__PURE__ */ new Map();
|
|
3421
|
+
const recSlots = needs.map((n, i) => {
|
|
3422
|
+
const candidates = orderCandidates(recs[i], anchorFamily, optimize, bundleByName);
|
|
3423
|
+
const recommended = candidates[0] ?? null;
|
|
3424
|
+
const category = n.category ?? recommended?.category ?? inferCategory(n.need);
|
|
3425
|
+
return {
|
|
3426
|
+
need: n.need,
|
|
3427
|
+
category,
|
|
3428
|
+
layer: layerFor({ label: recommended?.name ?? n.need, category }),
|
|
3429
|
+
recommended,
|
|
3430
|
+
alternatives: candidates.slice(1),
|
|
3431
|
+
note: recommended ? void 0 : "no tracked package matched this need yet"
|
|
3432
|
+
};
|
|
3433
|
+
});
|
|
3434
|
+
const recommendedNames = new Set(
|
|
3435
|
+
recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
|
|
3436
|
+
);
|
|
3437
|
+
const { slots: pinnedSlots, unresolved: unresolvedPins } = await resolvePins(
|
|
3438
|
+
db,
|
|
3439
|
+
input.using,
|
|
3440
|
+
recommendedNames
|
|
3441
|
+
);
|
|
3442
|
+
const slots = [...pinnedSlots, ...recSlots];
|
|
3443
|
+
const compatibility = await resolveCompat(db, slots);
|
|
3444
|
+
const unmatched = [
|
|
3445
|
+
...slots.filter((s) => !s.recommended).map((s) => s.need),
|
|
3446
|
+
...unresolvedPins.map((n) => `${n} (pinned, but not found on npm)`)
|
|
3447
|
+
];
|
|
3448
|
+
const mermaid = buildMermaid(
|
|
3449
|
+
slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
|
|
3450
|
+
);
|
|
3451
|
+
return {
|
|
3452
|
+
dataAsOf,
|
|
3453
|
+
optimize,
|
|
3454
|
+
source,
|
|
3455
|
+
framework,
|
|
3456
|
+
slots,
|
|
3457
|
+
unmatched,
|
|
3458
|
+
mermaid,
|
|
3459
|
+
note: planNote(source, unmatched.length, optimize, framework),
|
|
3460
|
+
compatibility
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
function flagSlotConflicts(picks, conflicts) {
|
|
3464
|
+
const out = /* @__PURE__ */ new Map();
|
|
3465
|
+
for (const c of conflicts) {
|
|
3466
|
+
for (const p of picks) {
|
|
3467
|
+
if (p.name && c.packages.includes(p.name)) {
|
|
3468
|
+
const others = c.packages.filter((n) => n !== p.name);
|
|
3469
|
+
out.set(p.need, [.../* @__PURE__ */ new Set([...out.get(p.need) ?? [], ...others])]);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
return out;
|
|
3474
|
+
}
|
|
3475
|
+
async function resolveCompat(db, slots) {
|
|
3476
|
+
const eligible = slots.filter((s) => s.recommended);
|
|
3477
|
+
if (eligible.length < 2) return null;
|
|
3478
|
+
try {
|
|
3479
|
+
const allNames = [
|
|
3480
|
+
...new Set(eligible.flatMap((s) => [s.recommended, ...s.alternatives].map((c) => c.name)))
|
|
3481
|
+
];
|
|
3482
|
+
const [{ members }, edges] = await Promise.all([
|
|
3483
|
+
assembleMembers(db, allNames),
|
|
3484
|
+
getCompatEdges(db, allNames)
|
|
3485
|
+
]);
|
|
3486
|
+
const metaByName = new Map(members.map((m) => [m.name, m]));
|
|
3487
|
+
const sandboxConflicts = new Set(
|
|
3488
|
+
edges.filter((e) => e.status === "conflict").map((e) => `${e.packageA}|${e.packageB}`)
|
|
3489
|
+
);
|
|
3490
|
+
const slotCandidates = eligible.map(
|
|
3491
|
+
(s) => [s.recommended, ...s.alternatives].map((c) => metaByName.get(c.name) ?? NO_META(c))
|
|
3492
|
+
);
|
|
3493
|
+
const { selection } = optimizeStack(slotCandidates, sandboxConflicts);
|
|
3494
|
+
eligible.forEach((s, i) => {
|
|
3495
|
+
const idx = selection[i] ?? 0;
|
|
3496
|
+
if (idx <= 0) return;
|
|
3497
|
+
const all = [s.recommended, ...s.alternatives];
|
|
3498
|
+
const chosen = all[idx];
|
|
3499
|
+
if (!chosen) return;
|
|
3500
|
+
s.recommended = chosen;
|
|
3501
|
+
s.alternatives = all.filter((c) => c.name !== chosen.name);
|
|
3502
|
+
s.swappedFrom = all[0].name;
|
|
3503
|
+
});
|
|
3504
|
+
} catch (err) {
|
|
3505
|
+
logger.warn(`plan: compat optimization failed: ${String(err)}`);
|
|
3506
|
+
}
|
|
3507
|
+
const compat = await checkCompat(
|
|
3508
|
+
db,
|
|
3509
|
+
eligible.map((s) => s.recommended.name)
|
|
3510
|
+
).catch(() => null);
|
|
3511
|
+
if (compat) {
|
|
3512
|
+
const flags = flagSlotConflicts(
|
|
3513
|
+
slots.map((s) => ({ need: s.need, name: s.recommended?.name ?? null })),
|
|
3514
|
+
compat.conflicts
|
|
3515
|
+
);
|
|
3516
|
+
for (const s of slots) {
|
|
3517
|
+
const cw = flags.get(s.need);
|
|
3518
|
+
s.conflictsWith = cw?.length ? cw : void 0;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return compat;
|
|
3522
|
+
}
|
|
3523
|
+
function planNote(source, unmatched, optimize, framework) {
|
|
3524
|
+
const base = source === "heuristic" ? "Components were extracted from your document with a keyword heuristic (no summary LLM configured) \u2014 coarse; pass a `needs` array or set SUMMARY_API_KEY for sharper decomposition." : source === "llm" ? "Components were extracted from your document by the summary model." : "Components taken from the supplied needs.";
|
|
3525
|
+
const grounding = " Each package is recommended from lurq\u2019s scored index \u2014 a labeled, evidence-backed starting point, not a validated architecture.";
|
|
3526
|
+
const ctx = framework ? ` Anchored to the ${framework} ecosystem so sibling libraries stay coherent across the stack.` : "";
|
|
3527
|
+
const tail = unmatched ? ` ${unmatched} need(s) had no tracked match (listed in \`unmatched\`).` : "";
|
|
3528
|
+
const opt = optimize === "speed" ? " Ranking favored the lightest-bundle option per slot." : "";
|
|
3529
|
+
return base + grounding + ctx + opt + tail;
|
|
3530
|
+
}
|
|
3531
|
+
function dedupeNeeds(needs) {
|
|
3532
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3533
|
+
for (const n of needs) {
|
|
3534
|
+
const key = n.need.trim().toLowerCase();
|
|
3535
|
+
if (!key) continue;
|
|
3536
|
+
if (!seen.has(key)) seen.set(key, { need: n.need.trim(), category: n.category });
|
|
3537
|
+
}
|
|
3538
|
+
return [...seen.values()];
|
|
3539
|
+
}
|
|
3540
|
+
function familyOf(name) {
|
|
3541
|
+
const n = name.toLowerCase();
|
|
3542
|
+
for (const f of FAMILY_TOKENS) if (f.re.test(n)) return f.family;
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
|
|
3546
|
+
const coherence = (c) => {
|
|
3547
|
+
const fam = familyOf(c.name);
|
|
3548
|
+
if (!fam) return 0;
|
|
3549
|
+
return fam === anchorFamily ? 1 : -1;
|
|
3550
|
+
};
|
|
3551
|
+
return [...cands].sort((a, b) => {
|
|
3552
|
+
if (anchorFamily) {
|
|
3553
|
+
const d = coherence(b) - coherence(a);
|
|
3554
|
+
if (d) return d;
|
|
3555
|
+
}
|
|
3556
|
+
if (optimize === "speed") {
|
|
3557
|
+
return (bundleByName.get(a.name) ?? Infinity) - (bundleByName.get(b.name) ?? Infinity);
|
|
3558
|
+
}
|
|
3559
|
+
return 0;
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
async function bundleSizes(db, candidates) {
|
|
3563
|
+
const names = [...new Set(candidates.map((c) => c.name))];
|
|
3564
|
+
if (names.length === 0) return /* @__PURE__ */ new Map();
|
|
3565
|
+
const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(inArray3(packages.name, names));
|
|
3566
|
+
return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
|
|
3567
|
+
}
|
|
3568
|
+
async function decompose(document) {
|
|
3569
|
+
const config = getConfig();
|
|
3570
|
+
if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
|
|
3571
|
+
const llm = await decomposeWithLlm(document, config.SUMMARY_API_KEY, config.SUMMARY_MODEL).catch(
|
|
3572
|
+
(err) => {
|
|
3573
|
+
logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
|
|
3574
|
+
return null;
|
|
3575
|
+
}
|
|
3576
|
+
);
|
|
3577
|
+
if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
|
|
3578
|
+
}
|
|
3579
|
+
return { needs: decomposeHeuristic(document), source: "heuristic" };
|
|
3580
|
+
}
|
|
3581
|
+
async function decomposeWithLlm(document, apiKey, model) {
|
|
3582
|
+
const prompt = [
|
|
3583
|
+
"Project description:",
|
|
3584
|
+
document.slice(0, 8e3),
|
|
3585
|
+
"",
|
|
3586
|
+
'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
|
|
3587
|
+
"One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
|
|
3588
|
+
].join("\n");
|
|
3589
|
+
const { data } = await httpRequest("https://api.openai.com/v1/chat/completions", {
|
|
3590
|
+
host: "api.openai.com",
|
|
3591
|
+
method: "POST",
|
|
3592
|
+
ttlMs: 24 * 60 * 60 * 1e3,
|
|
3593
|
+
// Hash the FULL document — length + a 64-char prefix collide for same-length
|
|
3594
|
+
// edits or shared templated headers, which would serve a stale decomposition.
|
|
3595
|
+
cacheKey: `openai-plan ${model} ${createHash4("sha1").update(document).digest("hex")}`,
|
|
3596
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
3597
|
+
body: JSON.stringify({
|
|
3598
|
+
model,
|
|
3599
|
+
messages: [
|
|
3600
|
+
{ role: "system", content: DECOMPOSE_SYSTEM },
|
|
3601
|
+
{ role: "user", content: prompt }
|
|
3602
|
+
],
|
|
3603
|
+
response_format: { type: "json_object" },
|
|
3604
|
+
temperature: 0.2
|
|
3605
|
+
})
|
|
3606
|
+
});
|
|
3607
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
3608
|
+
const parsed = content ? JSON.parse(content) : {};
|
|
3609
|
+
const raw = Array.isArray(parsed?.needs) ? parsed.needs : [];
|
|
3610
|
+
return raw.map((n) => {
|
|
3611
|
+
const need = typeof n?.need === "string" ? n.need.trim() : "";
|
|
3612
|
+
if (!need) return null;
|
|
3613
|
+
const cat = typeof n?.category === "string" && isCategory(n.category) ? n.category : void 0;
|
|
3614
|
+
return { need, category: cat };
|
|
3615
|
+
}).filter(Boolean);
|
|
3616
|
+
}
|
|
3617
|
+
function decomposeHeuristic(document) {
|
|
3618
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
3619
|
+
for (const rawLine of document.split("\n")) {
|
|
3620
|
+
const line = rawLine.replace(/^[#>*\-\s]+/, "").replace(/^\d+\.\s+/, "").trim();
|
|
3621
|
+
if (line.length < 3) continue;
|
|
3622
|
+
const category = inferCategory(line);
|
|
3623
|
+
if (category && !byCategory.has(category)) {
|
|
3624
|
+
byCategory.set(category, line.slice(0, 120));
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return [...byCategory.entries()].map(([category, need]) => ({ need, category }));
|
|
3628
|
+
}
|
|
3629
|
+
var PER_SLOT, MAX_SLOTS, NO_META, FAMILY_TOKENS, DECOMPOSE_SYSTEM;
|
|
3630
|
+
var init_plan = __esm({
|
|
3631
|
+
"src/mcp/plan.ts"() {
|
|
3632
|
+
"use strict";
|
|
3633
|
+
init_esm_shims();
|
|
3634
|
+
init_config();
|
|
3635
|
+
init_http();
|
|
3636
|
+
init_check();
|
|
3637
|
+
init_members();
|
|
3638
|
+
init_optimize();
|
|
3639
|
+
init_compat();
|
|
3640
|
+
init_logger();
|
|
3641
|
+
init_types();
|
|
3642
|
+
init_schema();
|
|
3643
|
+
init_categoryInference();
|
|
3644
|
+
init_recommend();
|
|
3645
|
+
init_single();
|
|
3646
|
+
init_diagram();
|
|
3647
|
+
init_handlers();
|
|
3648
|
+
PER_SLOT = 3;
|
|
3649
|
+
MAX_SLOTS = 24;
|
|
3650
|
+
NO_META = (c) => ({
|
|
3651
|
+
name: c.name,
|
|
3652
|
+
version: c.latestVersion,
|
|
3653
|
+
peerDependencies: null,
|
|
3654
|
+
peerDependenciesMeta: null,
|
|
3655
|
+
engines: null
|
|
3656
|
+
});
|
|
3657
|
+
FAMILY_TOKENS = [
|
|
3658
|
+
{ family: "react", re: /\breact\b|preact/ },
|
|
3659
|
+
{ family: "vue", re: /\bvue\b|nuxt/ },
|
|
3660
|
+
{ family: "angular", re: /angular/ },
|
|
3661
|
+
{ family: "svelte", re: /svelte/ },
|
|
3662
|
+
{ family: "solid", re: /\bsolid(-?js)?\b/ }
|
|
3663
|
+
];
|
|
3664
|
+
DECOMPOSE_SYSTEM = "You break a software project description into the distinct technical components that each need a library. Return ONLY components the project actually requires, grounded in the description. Respond with a JSON object.";
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
3667
|
+
|
|
2141
3668
|
// src/mcp/server.ts
|
|
2142
3669
|
var server_exports = {};
|
|
2143
3670
|
__export(server_exports, {
|
|
2144
3671
|
buildMcpServer: () => buildMcpServer,
|
|
3672
|
+
npmName: () => npmName,
|
|
2145
3673
|
startMcpServer: () => startMcpServer
|
|
2146
3674
|
});
|
|
2147
3675
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -2171,7 +3699,7 @@ function buildMcpServer(db) {
|
|
|
2171
3699
|
title: "Evaluate a package",
|
|
2172
3700
|
description: "Full evidence read for one npm package: scores, signals, advisories, summary, and a usage guide. Fetches & scores on demand if not yet tracked.",
|
|
2173
3701
|
inputSchema: {
|
|
2174
|
-
package:
|
|
3702
|
+
package: npmName.describe("npm package name")
|
|
2175
3703
|
}
|
|
2176
3704
|
},
|
|
2177
3705
|
async (args) => json(await handleEvaluate(db, args))
|
|
@@ -2182,18 +3710,29 @@ function buildMcpServer(db) {
|
|
|
2182
3710
|
title: "Compare packages",
|
|
2183
3711
|
description: "Side-by-side comparison of 2\u20135 npm packages, ranked by health score.",
|
|
2184
3712
|
inputSchema: {
|
|
2185
|
-
packages: z2.array(
|
|
3713
|
+
packages: z2.array(npmName).min(2).max(5).describe("2\u20135 npm package names")
|
|
2186
3714
|
}
|
|
2187
3715
|
},
|
|
2188
3716
|
async (args) => json(await handleCompare(db, args))
|
|
2189
3717
|
);
|
|
3718
|
+
server.registerTool(
|
|
3719
|
+
"compat",
|
|
3720
|
+
{
|
|
3721
|
+
title: "Check package compatibility",
|
|
3722
|
+
description: "Check whether a set of packages forms a coherent stack: peer-dependency and engine-range compatibility across the whole set (instant, from declared metadata), plus any recorded sandbox-verified conflicts. Returns the exact clashing constraints. Read-only \u2014 does not run installs. Call before committing to a multi-package stack.",
|
|
3723
|
+
inputSchema: {
|
|
3724
|
+
packages: z2.array(npmName).min(2).max(8).describe("2\u20138 npm package names to check together")
|
|
3725
|
+
}
|
|
3726
|
+
},
|
|
3727
|
+
async (args) => json(await handleCompat(db, args))
|
|
3728
|
+
);
|
|
2190
3729
|
server.registerTool(
|
|
2191
3730
|
"verify",
|
|
2192
3731
|
{
|
|
2193
3732
|
title: "Verify a package",
|
|
2194
3733
|
description: "Confirm an npm package is real, healthy, and not risky before installing \u2014 guards against hallucinated or typosquatted dependency names. Checks the live registry.",
|
|
2195
3734
|
inputSchema: {
|
|
2196
|
-
package:
|
|
3735
|
+
package: npmName.describe("npm package name to verify")
|
|
2197
3736
|
}
|
|
2198
3737
|
},
|
|
2199
3738
|
async (args) => json(await handleVerify(db, args))
|
|
@@ -2204,11 +3743,32 @@ function buildMcpServer(db) {
|
|
|
2204
3743
|
title: "Reference architecture diagram",
|
|
2205
3744
|
description: "Emit a reference-architecture Mermaid diagram for a stack you have already chosen (package names). A labeled starting point keyed by layer \u2014 not a validated architecture, and not an architecture designer.",
|
|
2206
3745
|
inputSchema: {
|
|
2207
|
-
stack: z2.array(
|
|
3746
|
+
stack: z2.array(npmName).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
|
|
2208
3747
|
}
|
|
2209
3748
|
},
|
|
2210
3749
|
async (args) => json(await handleDiagram(db, args))
|
|
2211
3750
|
);
|
|
3751
|
+
server.registerTool(
|
|
3752
|
+
"plan",
|
|
3753
|
+
{
|
|
3754
|
+
title: "Plan a stack from a program description",
|
|
3755
|
+
description: "Turn a detailed program description (spec/README) or a list of component needs into an evidence-scored build plan: a real, lurq-scored package recommended per component, plus a Mermaid roadmap other agents can parse. Recommends building blocks slot-by-slot from the index \u2014 it does not invent an architecture from a bare prompt.",
|
|
3756
|
+
inputSchema: {
|
|
3757
|
+
document: z2.string().optional().describe("Detailed description of the program (spec/README); lurq decomposes it into components"),
|
|
3758
|
+
needs: z2.array(
|
|
3759
|
+
z2.object({
|
|
3760
|
+
need: z2.string().min(1).describe("A component that needs a library"),
|
|
3761
|
+
category: categoryEnum.optional()
|
|
3762
|
+
})
|
|
3763
|
+
).optional().describe("Pre-decomposed components (skip if you pass a document)"),
|
|
3764
|
+
using: z2.array(npmName).max(12).optional().describe(
|
|
3765
|
+
"Packages you have already decided on. lurq pins these as fixed slots, recommends only the remaining needs, and checks/optimizes the whole stack around your picks."
|
|
3766
|
+
),
|
|
3767
|
+
optimize: z2.enum(["speed", "balanced"]).optional().describe("'speed' prefers the lightest-bundle option per slot; default 'balanced'")
|
|
3768
|
+
}
|
|
3769
|
+
},
|
|
3770
|
+
async (args) => json(await handlePlan(db, args))
|
|
3771
|
+
);
|
|
2212
3772
|
return server;
|
|
2213
3773
|
}
|
|
2214
3774
|
async function startMcpServer() {
|
|
@@ -2227,7 +3787,7 @@ async function startMcpServer() {
|
|
|
2227
3787
|
await server.connect(transport);
|
|
2228
3788
|
logger.info(`${SERVER_NAME} MCP server v${VERSION} running on stdio.`);
|
|
2229
3789
|
}
|
|
2230
|
-
var categoryEnum, confidenceEnum, constraintsSchema;
|
|
3790
|
+
var categoryEnum, confidenceEnum, npmName, constraintsSchema;
|
|
2231
3791
|
var init_server = __esm({
|
|
2232
3792
|
"src/mcp/server.ts"() {
|
|
2233
3793
|
"use strict";
|
|
@@ -2238,8 +3798,10 @@ var init_server = __esm({
|
|
|
2238
3798
|
init_logger();
|
|
2239
3799
|
init_handlers();
|
|
2240
3800
|
init_diagram();
|
|
3801
|
+
init_plan();
|
|
2241
3802
|
categoryEnum = z2.enum(CATEGORIES);
|
|
2242
|
-
confidenceEnum = z2.enum(["proven", "emerging", "unproven"]);
|
|
3803
|
+
confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
|
|
3804
|
+
npmName = z2.string().trim().min(1).max(214).regex(/^(?:@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/i, "Invalid npm package name");
|
|
2243
3805
|
constraintsSchema = z2.object({
|
|
2244
3806
|
runtime: z2.enum(["browser", "node", "both"]).optional(),
|
|
2245
3807
|
license: z2.string().optional(),
|
|
@@ -2249,75 +3811,895 @@ var init_server = __esm({
|
|
|
2249
3811
|
}
|
|
2250
3812
|
});
|
|
2251
3813
|
|
|
2252
|
-
// src/
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
3814
|
+
// src/auth/apiKeys.ts
|
|
3815
|
+
import { createHash as createHash5, randomBytes } from "crypto";
|
|
3816
|
+
import { and as and5, desc as desc3, eq as eq5, isNull } from "drizzle-orm";
|
|
3817
|
+
function hashKey(key) {
|
|
3818
|
+
return createHash5("sha256").update(key).digest("hex");
|
|
3819
|
+
}
|
|
3820
|
+
function generateApiKey() {
|
|
3821
|
+
const body = randomBytes(24).toString("base64url");
|
|
3822
|
+
const key = API_KEY_PREFIX + body;
|
|
3823
|
+
return { key, hash: hashKey(key), prefix: API_KEY_PREFIX + body.slice(0, DISPLAY_BODY) };
|
|
3824
|
+
}
|
|
3825
|
+
async function createKey(db, input = {}) {
|
|
3826
|
+
const { key, hash, prefix } = generateApiKey();
|
|
3827
|
+
const [row] = await db.insert(apiKeys).values({
|
|
3828
|
+
keyHash: hash,
|
|
3829
|
+
prefix,
|
|
3830
|
+
label: input.label,
|
|
3831
|
+
tier: input.tier ?? "free",
|
|
3832
|
+
ownerId: input.ownerId
|
|
3833
|
+
}).returning();
|
|
3834
|
+
return { key, row };
|
|
3835
|
+
}
|
|
3836
|
+
async function lookupActiveKey(db, key) {
|
|
3837
|
+
const hash = hashKey(key);
|
|
3838
|
+
const [row] = await db.select().from(apiKeys).where(and5(eq5(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
|
|
3839
|
+
if (!row) return null;
|
|
3840
|
+
db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, row.id)).then(void 0, () => {
|
|
3841
|
+
});
|
|
3842
|
+
return row;
|
|
3843
|
+
}
|
|
3844
|
+
async function listKeys(db) {
|
|
3845
|
+
return db.select().from(apiKeys).orderBy(desc3(apiKeys.createdAt));
|
|
3846
|
+
}
|
|
3847
|
+
function matchByPrefixOrId(prefixOrId) {
|
|
3848
|
+
const asId = Number(prefixOrId);
|
|
3849
|
+
return Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq5(apiKeys.id, asId) : eq5(apiKeys.prefix, prefixOrId);
|
|
3850
|
+
}
|
|
3851
|
+
async function revokeKey(db, prefixOrId) {
|
|
3852
|
+
const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
|
|
3853
|
+
return rows.length;
|
|
3854
|
+
}
|
|
3855
|
+
async function rotateKey(db, prefixOrId) {
|
|
3856
|
+
const [previous] = await db.select().from(apiKeys).where(and5(matchByPrefixOrId(prefixOrId), isNull(apiKeys.revokedAt))).limit(1);
|
|
3857
|
+
if (!previous) return null;
|
|
3858
|
+
const { key, row } = await createKey(db, {
|
|
3859
|
+
label: previous.label ?? void 0,
|
|
3860
|
+
tier: previous.tier,
|
|
3861
|
+
ownerId: previous.ownerId ?? void 0
|
|
3862
|
+
});
|
|
3863
|
+
await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, previous.id));
|
|
3864
|
+
return { key, row, previous };
|
|
3865
|
+
}
|
|
3866
|
+
var DISPLAY_BODY;
|
|
3867
|
+
var init_apiKeys = __esm({
|
|
3868
|
+
"src/auth/apiKeys.ts"() {
|
|
2259
3869
|
"use strict";
|
|
2260
3870
|
init_esm_shims();
|
|
2261
|
-
|
|
3871
|
+
init_constants();
|
|
3872
|
+
init_schema();
|
|
3873
|
+
DISPLAY_BODY = 6;
|
|
2262
3874
|
}
|
|
2263
3875
|
});
|
|
2264
3876
|
|
|
2265
|
-
// src/
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
return
|
|
2273
|
-
}
|
|
2274
|
-
function detail(pairs) {
|
|
2275
|
-
const labelWidth = Math.max(...pairs.map(([k]) => k.length));
|
|
2276
|
-
return pairs.map(([k, v]) => `${dim((k + ":").padEnd(labelWidth + 1))} ${v}`).join("\n");
|
|
2277
|
-
}
|
|
2278
|
-
function formatNumber(n) {
|
|
2279
|
-
if (n == null) return "\u2014";
|
|
2280
|
-
if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
|
|
2281
|
-
if (n >= 1e3) return `${Math.round(n / 100) / 10}k`;
|
|
2282
|
-
return String(n);
|
|
2283
|
-
}
|
|
2284
|
-
function formatDate(iso) {
|
|
2285
|
-
return iso ? iso.slice(0, 10) : "\u2014";
|
|
2286
|
-
}
|
|
2287
|
-
function formatPercent(fraction) {
|
|
2288
|
-
if (fraction == null) return "\u2014";
|
|
2289
|
-
const pct = Math.round(fraction * 1e3) / 10;
|
|
2290
|
-
return `${pct > 0 ? "+" : ""}${pct}%`;
|
|
3877
|
+
// src/mcp/http.ts
|
|
3878
|
+
var http_exports = {};
|
|
3879
|
+
__export(http_exports, {
|
|
3880
|
+
startHttpServer: () => startHttpServer
|
|
3881
|
+
});
|
|
3882
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3883
|
+
function rpcError(code, message) {
|
|
3884
|
+
return { jsonrpc: "2.0", error: { code, message }, id: null };
|
|
2291
3885
|
}
|
|
2292
|
-
function
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
3886
|
+
async function startHttpServer(opts = {}) {
|
|
3887
|
+
const config = getConfig();
|
|
3888
|
+
const port = opts.port ?? config.PORT;
|
|
3889
|
+
const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
|
|
3890
|
+
const { db } = createDb({ max: 20 });
|
|
3891
|
+
const app = express();
|
|
3892
|
+
app.set("trust proxy", 1);
|
|
3893
|
+
app.use(helmet());
|
|
3894
|
+
app.use(express.json({ limit: "1mb" }));
|
|
3895
|
+
app.get("/healthz", (_req, res) => {
|
|
3896
|
+
res.status(200).json({ status: "ok" });
|
|
3897
|
+
});
|
|
3898
|
+
const ipLimiter = rateLimit({
|
|
3899
|
+
windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
|
|
3900
|
+
limit: config.LURQ_IP_RATE_LIMIT_MAX,
|
|
3901
|
+
standardHeaders: "draft-7",
|
|
3902
|
+
legacyHeaders: false,
|
|
3903
|
+
message: rpcError(-32029, "Rate limit exceeded.")
|
|
3904
|
+
});
|
|
3905
|
+
const auth = async (req, res, next) => {
|
|
3906
|
+
const header = req.headers.authorization;
|
|
3907
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
3908
|
+
if (!token) {
|
|
3909
|
+
res.status(401).json(rpcError(-32001, "Missing API key. Pass Authorization: Bearer <key>."));
|
|
3910
|
+
return;
|
|
3911
|
+
}
|
|
3912
|
+
try {
|
|
3913
|
+
const row = await lookupActiveKey(db, token);
|
|
3914
|
+
if (!row) {
|
|
3915
|
+
res.status(401).json(rpcError(-32001, "Invalid or revoked API key."));
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
req.lurqKey = row;
|
|
3919
|
+
next();
|
|
3920
|
+
} catch (err) {
|
|
3921
|
+
logger.error("auth lookup failed:", err instanceof Error ? err.message : String(err));
|
|
3922
|
+
res.status(500).json(rpcError(-32603, "Internal error."));
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
const keyLimiter = rateLimit({
|
|
3926
|
+
windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
|
|
3927
|
+
limit: config.LURQ_RATE_LIMIT_MAX,
|
|
3928
|
+
standardHeaders: "draft-7",
|
|
3929
|
+
legacyHeaders: false,
|
|
3930
|
+
// Key on the resolved API key's unique row id (always present — auth runs
|
|
3931
|
+
// first). The display `prefix` is only 6 chars of body, so distinct keys
|
|
3932
|
+
// can collide on it and share a quota; the id cannot. The IP fallback uses
|
|
3933
|
+
// express-rate-limit's ipKeyGenerator so IPv6 addresses are normalized
|
|
3934
|
+
// correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
|
|
3935
|
+
keyGenerator: (req) => {
|
|
3936
|
+
const id = req.lurqKey?.id;
|
|
3937
|
+
return id != null ? `key:${id}` : ipKeyGenerator(req.ip ?? "0.0.0.0");
|
|
3938
|
+
},
|
|
3939
|
+
message: rpcError(-32029, "Rate limit exceeded.")
|
|
3940
|
+
});
|
|
3941
|
+
app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
|
|
3942
|
+
const server = buildMcpServer(db);
|
|
3943
|
+
const transport = new StreamableHTTPServerTransport({
|
|
3944
|
+
sessionIdGenerator: void 0,
|
|
3945
|
+
enableJsonResponse: true
|
|
3946
|
+
});
|
|
3947
|
+
res.on("close", () => {
|
|
3948
|
+
void transport.close();
|
|
3949
|
+
void server.close();
|
|
3950
|
+
});
|
|
3951
|
+
try {
|
|
3952
|
+
await server.connect(transport);
|
|
3953
|
+
await transport.handleRequest(req, res, req.body);
|
|
3954
|
+
} catch (err) {
|
|
3955
|
+
logger.error("mcp request failed:", err instanceof Error ? err.message : String(err));
|
|
3956
|
+
if (!res.headersSent) res.status(500).json(rpcError(-32603, "Internal error"));
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
app.all("/mcp", (_req, res) => {
|
|
3960
|
+
res.status(405).json(rpcError(-32e3, "Method not allowed."));
|
|
3961
|
+
});
|
|
3962
|
+
app.listen(port, () => {
|
|
3963
|
+
logger.info(`lurq HTTP MCP server listening on :${port}/mcp`);
|
|
3964
|
+
});
|
|
2296
3965
|
}
|
|
2297
|
-
var
|
|
2298
|
-
|
|
2299
|
-
"src/cli/format.ts"() {
|
|
3966
|
+
var init_http2 = __esm({
|
|
3967
|
+
"src/mcp/http.ts"() {
|
|
2300
3968
|
"use strict";
|
|
2301
3969
|
init_esm_shims();
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
green = (s) => wrap("32", s);
|
|
2308
|
-
yellow = (s) => wrap("33", s);
|
|
2309
|
-
ANSI_RE = /\[[0-9;]*m/g;
|
|
2310
|
-
width = (s) => s.replace(ANSI_RE, "").length;
|
|
3970
|
+
init_config();
|
|
3971
|
+
init_logger();
|
|
3972
|
+
init_apiKeys();
|
|
3973
|
+
init_client();
|
|
3974
|
+
init_server();
|
|
2311
3975
|
}
|
|
2312
3976
|
});
|
|
2313
3977
|
|
|
2314
|
-
// src/
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
3978
|
+
// src/pipeline/rescore.ts
|
|
3979
|
+
import { isNotNull as isNotNull4 } from "drizzle-orm";
|
|
3980
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
3981
|
+
async function runRescore() {
|
|
3982
|
+
const weights = loadWeights();
|
|
3983
|
+
const handle = createDb({ max: 4 });
|
|
3984
|
+
try {
|
|
3985
|
+
const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull4(packages.scoreBreakdown));
|
|
3986
|
+
let updated = 0;
|
|
3987
|
+
for (const row of rows) {
|
|
3988
|
+
if (!row.breakdown) continue;
|
|
3989
|
+
const health = computeHealthScore(row.breakdown, weights.health);
|
|
3990
|
+
if (health !== row.healthScore) {
|
|
3991
|
+
await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq6(packages.id, row.id));
|
|
3992
|
+
updated++;
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
|
|
3996
|
+
if (updated > 0) await invalidateCache();
|
|
3997
|
+
return { seen: rows.length, updated };
|
|
3998
|
+
} finally {
|
|
3999
|
+
await handle.close();
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
var init_rescore = __esm({
|
|
4003
|
+
"src/pipeline/rescore.ts"() {
|
|
4004
|
+
"use strict";
|
|
4005
|
+
init_esm_shims();
|
|
4006
|
+
init_cache();
|
|
4007
|
+
init_logger();
|
|
4008
|
+
init_client();
|
|
4009
|
+
init_schema();
|
|
4010
|
+
init_scoring();
|
|
4011
|
+
init_weights();
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
4014
|
+
|
|
4015
|
+
// src/db/discovery.ts
|
|
4016
|
+
import { eq as eq7 } from "drizzle-orm";
|
|
4017
|
+
async function getKnownNames(db) {
|
|
4018
|
+
const [tracked, queued] = await Promise.all([
|
|
4019
|
+
db.select({ name: packages.name }).from(packages),
|
|
4020
|
+
db.select({ name: discoveryQueue.name }).from(discoveryQueue)
|
|
4021
|
+
]);
|
|
4022
|
+
return /* @__PURE__ */ new Set([...tracked.map((r) => r.name), ...queued.map((r) => r.name)]);
|
|
4023
|
+
}
|
|
4024
|
+
async function enqueueCandidates(db, candidates) {
|
|
4025
|
+
if (candidates.length === 0) return 0;
|
|
4026
|
+
const rows = candidates.map((c) => ({ name: c.name, discoveredVia: c.via }));
|
|
4027
|
+
const inserted = await db.insert(discoveryQueue).values(rows).onConflictDoNothing({ target: discoveryQueue.name }).returning({ id: discoveryQueue.id });
|
|
4028
|
+
return inserted.length;
|
|
4029
|
+
}
|
|
4030
|
+
async function getPendingCandidates(db, limit) {
|
|
4031
|
+
return db.select().from(discoveryQueue).where(eq7(discoveryQueue.status, "pending")).limit(limit);
|
|
4032
|
+
}
|
|
4033
|
+
async function setDiscoveryStatus(db, name, data) {
|
|
4034
|
+
await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq7(discoveryQueue.name, name));
|
|
4035
|
+
}
|
|
4036
|
+
var init_discovery = __esm({
|
|
4037
|
+
"src/db/discovery.ts"() {
|
|
4038
|
+
"use strict";
|
|
4039
|
+
init_esm_shims();
|
|
4040
|
+
init_schema();
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
|
|
4044
|
+
// src/pipeline/discovery.ts
|
|
4045
|
+
import { isNotNull as isNotNull5 } from "drizzle-orm";
|
|
4046
|
+
function selectCandidates(raw, known) {
|
|
4047
|
+
const seen = new Set(known);
|
|
4048
|
+
const out = [];
|
|
4049
|
+
for (const c of raw) {
|
|
4050
|
+
const name = c.name?.trim();
|
|
4051
|
+
if (!name || seen.has(name)) continue;
|
|
4052
|
+
seen.add(name);
|
|
4053
|
+
out.push({ name, via: c.via });
|
|
4054
|
+
}
|
|
4055
|
+
return out;
|
|
4056
|
+
}
|
|
4057
|
+
function passesGate(preScore) {
|
|
4058
|
+
return preScore !== null && preScore >= DISCOVERY.minPreScore;
|
|
4059
|
+
}
|
|
4060
|
+
async function preScorePackage(name, fetchImpl) {
|
|
4061
|
+
try {
|
|
4062
|
+
const registry = await fetchNpmRegistry(name, fetchImpl);
|
|
4063
|
+
const signals = {
|
|
4064
|
+
name,
|
|
4065
|
+
registry,
|
|
4066
|
+
downloads: null,
|
|
4067
|
+
github: null,
|
|
4068
|
+
depsDev: null,
|
|
4069
|
+
bundle: null,
|
|
4070
|
+
errors: []
|
|
4071
|
+
};
|
|
4072
|
+
return computeQuality(toScoringInput(signals, null));
|
|
4073
|
+
} catch {
|
|
4074
|
+
return null;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
async function graphChannel(db) {
|
|
4078
|
+
const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull5(packages.latestVersion));
|
|
4079
|
+
const out = [];
|
|
4080
|
+
for (const t of tracked) {
|
|
4081
|
+
if (!t.version) continue;
|
|
4082
|
+
const deps = (await fetchDependencyNames(t.name, t.version)).slice(
|
|
4083
|
+
0,
|
|
4084
|
+
DISCOVERY.graphNeighborsPerSeed
|
|
4085
|
+
);
|
|
4086
|
+
for (const name of deps) out.push({ name, via: "dependency-graph" });
|
|
4087
|
+
}
|
|
4088
|
+
return out;
|
|
4089
|
+
}
|
|
4090
|
+
async function searchChannel() {
|
|
4091
|
+
const out = [];
|
|
4092
|
+
for (const category of CATEGORIES) {
|
|
4093
|
+
if (category === "other") continue;
|
|
4094
|
+
const hits = await searchNpm(`keywords:${category}`, DISCOVERY.searchSizePerCategory);
|
|
4095
|
+
for (const hit of hits) {
|
|
4096
|
+
const via = isRecent(hit.date) ? "recent" : "category-search";
|
|
4097
|
+
out.push({ name: hit.name, via });
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
return out;
|
|
4101
|
+
}
|
|
4102
|
+
function isRecent(date) {
|
|
4103
|
+
if (!date) return false;
|
|
4104
|
+
const ms = Date.parse(date);
|
|
4105
|
+
if (Number.isNaN(ms)) return false;
|
|
4106
|
+
return Date.now() - ms <= 90 * 24 * 60 * 60 * 1e3;
|
|
4107
|
+
}
|
|
4108
|
+
async function runDiscovery(opts = {}) {
|
|
4109
|
+
const cap = opts.perRunCap ?? DISCOVERY.perRunCap;
|
|
4110
|
+
const handle = createDb({ max: 6 });
|
|
4111
|
+
try {
|
|
4112
|
+
logger.info("Discovery: gathering candidates from graph + search channels\u2026");
|
|
4113
|
+
const [graph, search] = await Promise.all([graphChannel(handle.db), searchChannel()]);
|
|
4114
|
+
const known = await getKnownNames(handle.db);
|
|
4115
|
+
const fresh = selectCandidates([...graph, ...search], known);
|
|
4116
|
+
const enqueued = await enqueueCandidates(handle.db, fresh);
|
|
4117
|
+
logger.info(
|
|
4118
|
+
`Discovery: ${graph.length} graph + ${search.length} search candidates \u2192 ${enqueued} new queued.`
|
|
4119
|
+
);
|
|
4120
|
+
const pending = await getPendingCandidates(handle.db, cap * 4);
|
|
4121
|
+
const scored = [];
|
|
4122
|
+
for (const cand of pending) {
|
|
4123
|
+
const preScore = await preScorePackage(cand.name);
|
|
4124
|
+
await setDiscoveryStatus(handle.db, cand.name, {
|
|
4125
|
+
status: passesGate(preScore) ? "pending" : "rejected",
|
|
4126
|
+
preScore: preScore ?? null
|
|
4127
|
+
});
|
|
4128
|
+
if (passesGate(preScore)) scored.push({ name: cand.name, preScore });
|
|
4129
|
+
}
|
|
4130
|
+
logger.info(`Discovery: gated ${pending.length}; ${scored.length} cleared the quality bar.`);
|
|
4131
|
+
scored.sort((a, b) => b.preScore - a.preScore);
|
|
4132
|
+
const toIngest = scored.slice(0, cap);
|
|
4133
|
+
const deferred = scored.slice(cap);
|
|
4134
|
+
if (deferred.length > 0) {
|
|
4135
|
+
logger.info(
|
|
4136
|
+
`Discovery: per-run cap ${cap} reached \u2014 ${deferred.length} eligible candidate(s) deferred to the next run: ${deferred.map((d) => d.name).join(", ")}`
|
|
4137
|
+
);
|
|
4138
|
+
}
|
|
4139
|
+
let ingested = 0;
|
|
4140
|
+
if (!opts.dryRun) {
|
|
4141
|
+
for (const cand of toIngest) {
|
|
4142
|
+
try {
|
|
4143
|
+
await syncOnePackage(handle.db, cand.name);
|
|
4144
|
+
await setDiscoveryStatus(handle.db, cand.name, { status: "ingested" });
|
|
4145
|
+
ingested++;
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
logger.warn(`Discovery: failed to ingest ${cand.name}: ${err.message}`);
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
logger.info(`Discovery: ingested ${ingested}/${toIngest.length} candidate(s).`);
|
|
4152
|
+
return {
|
|
4153
|
+
enqueued,
|
|
4154
|
+
gated: pending.length,
|
|
4155
|
+
passed: scored.length,
|
|
4156
|
+
ingested,
|
|
4157
|
+
droppedToNextRun: deferred.length
|
|
4158
|
+
};
|
|
4159
|
+
} finally {
|
|
4160
|
+
await handle.close();
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
var init_discovery2 = __esm({
|
|
4164
|
+
"src/pipeline/discovery.ts"() {
|
|
4165
|
+
"use strict";
|
|
4166
|
+
init_esm_shims();
|
|
4167
|
+
init_logger();
|
|
4168
|
+
init_types();
|
|
4169
|
+
init_client();
|
|
4170
|
+
init_discovery();
|
|
4171
|
+
init_schema();
|
|
4172
|
+
init_depsDev();
|
|
4173
|
+
init_npmRegistry();
|
|
4174
|
+
init_npmSearch();
|
|
4175
|
+
init_scoring();
|
|
4176
|
+
init_weights();
|
|
4177
|
+
init_single();
|
|
4178
|
+
}
|
|
4179
|
+
});
|
|
4180
|
+
|
|
4181
|
+
// src/pipeline/index.ts
|
|
4182
|
+
var pipeline_exports = {};
|
|
4183
|
+
__export(pipeline_exports, {
|
|
4184
|
+
runDiscovery: () => runDiscovery,
|
|
4185
|
+
runRescore: () => runRescore,
|
|
4186
|
+
runSync: () => runSync
|
|
4187
|
+
});
|
|
4188
|
+
var init_pipeline = __esm({
|
|
4189
|
+
"src/pipeline/index.ts"() {
|
|
4190
|
+
"use strict";
|
|
4191
|
+
init_esm_shims();
|
|
4192
|
+
init_sync();
|
|
4193
|
+
init_rescore();
|
|
4194
|
+
init_discovery2();
|
|
4195
|
+
}
|
|
4196
|
+
});
|
|
4197
|
+
|
|
4198
|
+
// src/cli/format.ts
|
|
4199
|
+
function table(headers, rows) {
|
|
4200
|
+
const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
|
|
4201
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
|
|
4202
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
|
|
4203
|
+
const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
|
|
4204
|
+
for (const r of rows) out.push(line(r));
|
|
4205
|
+
return out.join("\n");
|
|
4206
|
+
}
|
|
4207
|
+
function detail(pairs) {
|
|
4208
|
+
const labelWidth = Math.max(...pairs.map(([k]) => k.length));
|
|
4209
|
+
return pairs.map(([k, v]) => `${dim((k + ":").padEnd(labelWidth + 1))} ${v}`).join("\n");
|
|
4210
|
+
}
|
|
4211
|
+
function formatNumber(n) {
|
|
4212
|
+
if (n == null) return "\u2014";
|
|
4213
|
+
if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
|
|
4214
|
+
if (n >= 1e3) return `${Math.round(n / 100) / 10}k`;
|
|
4215
|
+
return String(n);
|
|
4216
|
+
}
|
|
4217
|
+
function formatDate(iso) {
|
|
4218
|
+
return iso ? iso.slice(0, 10) : "\u2014";
|
|
4219
|
+
}
|
|
4220
|
+
function formatPercent(fraction) {
|
|
4221
|
+
if (fraction == null) return "\u2014";
|
|
4222
|
+
const pct = Math.round(fraction * 1e3) / 10;
|
|
4223
|
+
return `${pct > 0 ? "+" : ""}${pct}%`;
|
|
4224
|
+
}
|
|
4225
|
+
function confidenceLabel(c) {
|
|
4226
|
+
if (c === "proven") return green(c);
|
|
4227
|
+
if (c === "emerging") return yellow(c);
|
|
4228
|
+
if (c === "promising") return green(c);
|
|
4229
|
+
return red(c);
|
|
4230
|
+
}
|
|
4231
|
+
var ESC, wrap, bold, dim, red, green, yellow, ANSI_RE, width;
|
|
4232
|
+
var init_format = __esm({
|
|
4233
|
+
"src/cli/format.ts"() {
|
|
4234
|
+
"use strict";
|
|
4235
|
+
init_esm_shims();
|
|
4236
|
+
ESC = "\x1B[";
|
|
4237
|
+
wrap = (code, s) => `${ESC}${code}m${s}${ESC}0m`;
|
|
4238
|
+
bold = (s) => wrap("1", s);
|
|
4239
|
+
dim = (s) => wrap("2", s);
|
|
4240
|
+
red = (s) => wrap("31", s);
|
|
4241
|
+
green = (s) => wrap("32", s);
|
|
4242
|
+
yellow = (s) => wrap("33", s);
|
|
4243
|
+
ANSI_RE = /\[[0-9;]*m/g;
|
|
4244
|
+
width = (s) => s.replace(ANSI_RE, "").length;
|
|
4245
|
+
}
|
|
4246
|
+
});
|
|
4247
|
+
|
|
4248
|
+
// src/cli/planView.ts
|
|
4249
|
+
var planView_exports = {};
|
|
4250
|
+
__export(planView_exports, {
|
|
4251
|
+
renderPlanHtml: () => renderPlanHtml
|
|
4252
|
+
});
|
|
4253
|
+
function esc(s) {
|
|
4254
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
4255
|
+
}
|
|
4256
|
+
function slotRows(plan) {
|
|
4257
|
+
return plan.slots.map((s) => {
|
|
4258
|
+
const rec = s.recommended;
|
|
4259
|
+
const name = rec ? rec.repoUrl ? `<a href="${esc(rec.repoUrl)}" target="_blank" rel="noreferrer">${esc(rec.name)}</a>` : esc(rec.name) : '<span class="muted">\u2014 no match \u2014</span>';
|
|
4260
|
+
const alts = s.alternatives.map((a) => esc(a.name)).join(", ") || '<span class="muted">\u2014</span>';
|
|
4261
|
+
return `<tr>
|
|
4262
|
+
<td>${esc(s.need)}</td>
|
|
4263
|
+
<td><span class="layer">${esc(s.layer)}</span></td>
|
|
4264
|
+
<td class="pkg">${name}</td>
|
|
4265
|
+
<td class="num">${rec ? rec.healthScore : "\u2014"}</td>
|
|
4266
|
+
<td>${rec ? esc(rec.confidence) : "\u2014"}</td>
|
|
4267
|
+
<td class="alts">${alts}</td>
|
|
4268
|
+
</tr>`;
|
|
4269
|
+
}).join("\n");
|
|
4270
|
+
}
|
|
4271
|
+
function renderPlanHtml(plan) {
|
|
4272
|
+
return `<!doctype html>
|
|
4273
|
+
<html lang="en">
|
|
4274
|
+
<head>
|
|
4275
|
+
<meta charset="utf-8" />
|
|
4276
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4277
|
+
<title>lurq plan \u2014 roadmap</title>
|
|
4278
|
+
<style>
|
|
4279
|
+
:root { color-scheme: dark; }
|
|
4280
|
+
* { box-sizing: border-box; }
|
|
4281
|
+
body { margin: 0; font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
4282
|
+
background: #0b0b0f; color: #e7e7ea; padding: 2.5rem clamp(1rem, 5vw, 4rem); }
|
|
4283
|
+
h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
|
|
4284
|
+
h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: .08em; color: #9a9aa6; margin: 2.5rem 0 .75rem; }
|
|
4285
|
+
.sub { color: #9a9aa6; margin: 0 0 1rem; max-width: 70ch; }
|
|
4286
|
+
.diagram { background: #141419; border: 1px solid #26262e; border-radius: 14px; padding: 1.5rem; overflow: auto; }
|
|
4287
|
+
table { width: 100%; border-collapse: collapse; margin-top: .5rem; }
|
|
4288
|
+
th, td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid #1e1e25; vertical-align: top; }
|
|
4289
|
+
th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #9a9aa6; }
|
|
4290
|
+
.pkg a { color: #d98cff; text-decoration: none; } .pkg a:hover { text-decoration: underline; }
|
|
4291
|
+
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
4292
|
+
.muted { color: #6a6a76; } .alts { color: #b8b8c2; font-size: .92rem; }
|
|
4293
|
+
.layer { font-size: .8rem; color: #8ad; background: #15202b; border-radius: 6px; padding: .1rem .45rem; }
|
|
4294
|
+
footer { margin-top: 2.5rem; color: #6a6a76; font-size: .85rem; }
|
|
4295
|
+
</style>
|
|
4296
|
+
</head>
|
|
4297
|
+
<body>
|
|
4298
|
+
<h1>lurq plan <span class="muted">\xB7 ${esc(plan.optimize)} \xB7 ${esc(plan.source)}${plan.framework ? ` \xB7 ${esc(plan.framework)} ecosystem` : ""}</span></h1>
|
|
4299
|
+
<p class="sub">${esc(plan.note)}</p>
|
|
4300
|
+
|
|
4301
|
+
<h2>Roadmap</h2>
|
|
4302
|
+
<div class="diagram"><pre class="mermaid">${esc(plan.mermaid)}</pre></div>
|
|
4303
|
+
|
|
4304
|
+
<h2>Components</h2>
|
|
4305
|
+
<table>
|
|
4306
|
+
<thead><tr><th>Component</th><th>Layer</th><th>Recommended</th><th>Health</th><th>Confidence</th><th>Alternatives</th></tr></thead>
|
|
4307
|
+
<tbody>
|
|
4308
|
+
${slotRows(plan)}
|
|
4309
|
+
</tbody>
|
|
4310
|
+
</table>
|
|
4311
|
+
|
|
4312
|
+
<footer>data as of ${esc(plan.dataAsOf)} \xB7 generated by lurq</footer>
|
|
4313
|
+
|
|
4314
|
+
<script type="module">
|
|
4315
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
4316
|
+
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
|
4317
|
+
</script>
|
|
4318
|
+
</body>
|
|
4319
|
+
</html>
|
|
4320
|
+
`;
|
|
4321
|
+
}
|
|
4322
|
+
var init_planView = __esm({
|
|
4323
|
+
"src/cli/planView.ts"() {
|
|
4324
|
+
"use strict";
|
|
4325
|
+
init_esm_shims();
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
|
|
4329
|
+
// src/db/watch.ts
|
|
4330
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
4331
|
+
async function getWatchCursor(db, id) {
|
|
4332
|
+
const rows = await db.select({ seq: watchState.seq }).from(watchState).where(eq8(watchState.id, id)).limit(1);
|
|
4333
|
+
return rows[0]?.seq ?? null;
|
|
4334
|
+
}
|
|
4335
|
+
async function setWatchCursor(db, id, seq) {
|
|
4336
|
+
await db.insert(watchState).values({ id, seq, updatedAt: /* @__PURE__ */ new Date() }).onConflictDoUpdate({ target: watchState.id, set: { seq, updatedAt: /* @__PURE__ */ new Date() } });
|
|
4337
|
+
}
|
|
4338
|
+
var init_watch = __esm({
|
|
4339
|
+
"src/db/watch.ts"() {
|
|
4340
|
+
"use strict";
|
|
4341
|
+
init_esm_shims();
|
|
4342
|
+
init_schema();
|
|
4343
|
+
}
|
|
4344
|
+
});
|
|
4345
|
+
|
|
4346
|
+
// src/pipeline/watch.ts
|
|
4347
|
+
var watch_exports = {};
|
|
4348
|
+
__export(watch_exports, {
|
|
4349
|
+
parseChangeLine: () => parseChangeLine,
|
|
4350
|
+
watchNpmChanges: () => watchNpmChanges
|
|
4351
|
+
});
|
|
4352
|
+
function parseChangeLine(line) {
|
|
4353
|
+
const trimmed = line.trim();
|
|
4354
|
+
if (!trimmed) return null;
|
|
4355
|
+
try {
|
|
4356
|
+
const obj = JSON.parse(trimmed);
|
|
4357
|
+
if (typeof obj?.id !== "string" || obj.seq == null) return null;
|
|
4358
|
+
return { seq: obj.seq, id: obj.id, deleted: Boolean(obj.deleted) };
|
|
4359
|
+
} catch {
|
|
4360
|
+
return null;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
async function watchNpmChanges(db, opts = {}) {
|
|
4364
|
+
const { signal } = opts;
|
|
4365
|
+
let backoff2 = 1e3;
|
|
4366
|
+
while (!signal?.aborted) {
|
|
4367
|
+
let tracked = new Set(await getAllPackageNames(db));
|
|
4368
|
+
let trackedAt = Date.now();
|
|
4369
|
+
const since = await getWatchCursor(db, FEED_ID) ?? opts.since ?? "now";
|
|
4370
|
+
const url = `${FEED_URL}?feed=continuous&since=${encodeURIComponent(since)}&heartbeat=${HEARTBEAT_MS}`;
|
|
4371
|
+
logger.info(`watch: connecting from seq=${since} (${tracked.size} tracked packages)`);
|
|
4372
|
+
try {
|
|
4373
|
+
const res = await fetch(url, { signal });
|
|
4374
|
+
if (!res.ok || !res.body) throw new Error(`feed responded ${res.status}`);
|
|
4375
|
+
backoff2 = 1e3;
|
|
4376
|
+
let sinceCheckpoint = 0;
|
|
4377
|
+
for await (const line of ndjsonLines(res.body, signal)) {
|
|
4378
|
+
const change = parseChangeLine(line);
|
|
4379
|
+
if (!change) continue;
|
|
4380
|
+
const seq = String(change.seq);
|
|
4381
|
+
sinceCheckpoint++;
|
|
4382
|
+
if (Date.now() - trackedAt > TRACKED_REFRESH_MS) {
|
|
4383
|
+
tracked = new Set(await getAllPackageNames(db));
|
|
4384
|
+
trackedAt = Date.now();
|
|
4385
|
+
}
|
|
4386
|
+
if (!change.deleted && tracked.has(change.id)) {
|
|
4387
|
+
logger.info(`watch: re-syncing ${change.id} (seq=${seq})`);
|
|
4388
|
+
await syncOnePackage(db, change.id).catch(
|
|
4389
|
+
(err) => logger.warn(`watch: re-sync failed for ${change.id}: ${String(err)}`)
|
|
4390
|
+
);
|
|
4391
|
+
await setWatchCursor(db, FEED_ID, seq);
|
|
4392
|
+
sinceCheckpoint = 0;
|
|
4393
|
+
} else if (sinceCheckpoint >= CHECKPOINT_EVERY) {
|
|
4394
|
+
await setWatchCursor(db, FEED_ID, seq);
|
|
4395
|
+
sinceCheckpoint = 0;
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
logger.info("watch: feed stream ended; reconnecting");
|
|
4399
|
+
} catch (err) {
|
|
4400
|
+
if (signal?.aborted) break;
|
|
4401
|
+
logger.warn(`watch: ${String(err)} \u2014 retrying in ${backoff2}ms`);
|
|
4402
|
+
await sleep(backoff2, signal);
|
|
4403
|
+
backoff2 = Math.min(backoff2 * 2, MAX_BACKOFF_MS);
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
async function* ndjsonLines(body, signal) {
|
|
4408
|
+
const reader = body.getReader();
|
|
4409
|
+
const decoder = new TextDecoder();
|
|
4410
|
+
let buffer = "";
|
|
4411
|
+
try {
|
|
4412
|
+
while (!signal?.aborted) {
|
|
4413
|
+
const { value, done } = await reader.read();
|
|
4414
|
+
if (done) break;
|
|
4415
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4416
|
+
let nl;
|
|
4417
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
4418
|
+
yield buffer.slice(0, nl);
|
|
4419
|
+
buffer = buffer.slice(nl + 1);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
} finally {
|
|
4423
|
+
reader.releaseLock();
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
function sleep(ms, signal) {
|
|
4427
|
+
return new Promise((resolve) => {
|
|
4428
|
+
const timer = setTimeout(resolve, ms);
|
|
4429
|
+
signal?.addEventListener(
|
|
4430
|
+
"abort",
|
|
4431
|
+
() => {
|
|
4432
|
+
clearTimeout(timer);
|
|
4433
|
+
resolve();
|
|
4434
|
+
},
|
|
4435
|
+
{ once: true }
|
|
4436
|
+
);
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
var FEED_ID, FEED_URL, HEARTBEAT_MS, TRACKED_REFRESH_MS, CHECKPOINT_EVERY, MAX_BACKOFF_MS;
|
|
4440
|
+
var init_watch2 = __esm({
|
|
4441
|
+
"src/pipeline/watch.ts"() {
|
|
4442
|
+
"use strict";
|
|
4443
|
+
init_esm_shims();
|
|
4444
|
+
init_logger();
|
|
4445
|
+
init_packages();
|
|
4446
|
+
init_watch();
|
|
4447
|
+
init_single();
|
|
4448
|
+
FEED_ID = "npm-changes";
|
|
4449
|
+
FEED_URL = "https://replicate.npmjs.com/_changes";
|
|
4450
|
+
HEARTBEAT_MS = 3e4;
|
|
4451
|
+
TRACKED_REFRESH_MS = 5 * 6e4;
|
|
4452
|
+
CHECKPOINT_EVERY = 200;
|
|
4453
|
+
MAX_BACKOFF_MS = 3e4;
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
// src/sandbox/types.ts
|
|
4458
|
+
var DEFAULT_TARGET;
|
|
4459
|
+
var init_types2 = __esm({
|
|
4460
|
+
"src/sandbox/types.ts"() {
|
|
4461
|
+
"use strict";
|
|
4462
|
+
init_esm_shims();
|
|
4463
|
+
DEFAULT_TARGET = {
|
|
4464
|
+
node: "20",
|
|
4465
|
+
moduleSystem: "cjs"
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
});
|
|
4469
|
+
|
|
4470
|
+
// src/sandbox/local.ts
|
|
4471
|
+
import { execFile } from "child_process";
|
|
4472
|
+
import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
4473
|
+
import { tmpdir } from "os";
|
|
4474
|
+
import { join as join3 } from "path";
|
|
4475
|
+
import { promisify } from "util";
|
|
4476
|
+
function npmInstallArgs(specs, opts) {
|
|
4477
|
+
const bad = specs.find((s) => s.startsWith("-"));
|
|
4478
|
+
if (bad) throw new Error(`Invalid package spec: ${bad}`);
|
|
4479
|
+
const args = ["install", ...specs, "--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
|
|
4480
|
+
if (!opts.allowScripts) args.push("--ignore-scripts");
|
|
4481
|
+
return args;
|
|
4482
|
+
}
|
|
4483
|
+
function smokeScript(pkg, moduleSystem) {
|
|
4484
|
+
return moduleSystem === "esm" ? ["--input-type=module", "-e", `await import(${JSON.stringify(pkg)})`] : ["-e", `require(${JSON.stringify(pkg)})`];
|
|
4485
|
+
}
|
|
4486
|
+
function condense(s) {
|
|
4487
|
+
return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX);
|
|
4488
|
+
}
|
|
4489
|
+
function stderrOf(err) {
|
|
4490
|
+
if (err && typeof err === "object") {
|
|
4491
|
+
const e = err;
|
|
4492
|
+
if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
|
|
4493
|
+
if (typeof e.message === "string") return e.message;
|
|
4494
|
+
}
|
|
4495
|
+
return String(err);
|
|
4496
|
+
}
|
|
4497
|
+
var execFileAsync, INSTALL_TIMEOUT_MS, SMOKE_TIMEOUT_MS, ERROR_MAX, toSpec, LocalSandbox;
|
|
4498
|
+
var init_local = __esm({
|
|
4499
|
+
"src/sandbox/local.ts"() {
|
|
4500
|
+
"use strict";
|
|
4501
|
+
init_esm_shims();
|
|
4502
|
+
init_types2();
|
|
4503
|
+
execFileAsync = promisify(execFile);
|
|
4504
|
+
INSTALL_TIMEOUT_MS = 12e4;
|
|
4505
|
+
SMOKE_TIMEOUT_MS = 3e4;
|
|
4506
|
+
ERROR_MAX = 500;
|
|
4507
|
+
toSpec = (p) => p.version ? `${p.name}@${p.version}` : p.name;
|
|
4508
|
+
LocalSandbox = class {
|
|
4509
|
+
name = "local";
|
|
4510
|
+
async verify(pkg, version, opts = {}) {
|
|
4511
|
+
const set = await this.verifySet([{ name: pkg, version }], opts);
|
|
4512
|
+
return {
|
|
4513
|
+
driver: this.name,
|
|
4514
|
+
moduleSystem: set.moduleSystem,
|
|
4515
|
+
installed: set.installed,
|
|
4516
|
+
imported: set.loaded[0]?.loaded ?? null,
|
|
4517
|
+
ranScripts: opts.allowScripts ?? false,
|
|
4518
|
+
durationMs: set.durationMs,
|
|
4519
|
+
error: set.error
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
async verifySet(packages2, opts = {}) {
|
|
4523
|
+
const target = opts.target ?? DEFAULT_TARGET;
|
|
4524
|
+
const allowScripts = opts.allowScripts ?? false;
|
|
4525
|
+
const specs = packages2.map(toSpec);
|
|
4526
|
+
const dir = await mkdtemp(join3(tmpdir(), "lurq-sandbox-"));
|
|
4527
|
+
const started = Date.now();
|
|
4528
|
+
const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
|
|
4529
|
+
let installed = false;
|
|
4530
|
+
let error = null;
|
|
4531
|
+
try {
|
|
4532
|
+
await writeFile2(
|
|
4533
|
+
join3(dir, "package.json"),
|
|
4534
|
+
JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
|
|
4535
|
+
);
|
|
4536
|
+
await execFileAsync("npm", npmInstallArgs(specs, { allowScripts }), {
|
|
4537
|
+
cwd: dir,
|
|
4538
|
+
timeout: opts.timeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
4539
|
+
signal: opts.signal
|
|
4540
|
+
});
|
|
4541
|
+
installed = true;
|
|
4542
|
+
for (let i = 0; i < packages2.length; i++) {
|
|
4543
|
+
try {
|
|
4544
|
+
await execFileAsync("node", smokeScript(packages2[i].name, target.moduleSystem), {
|
|
4545
|
+
cwd: dir,
|
|
4546
|
+
timeout: SMOKE_TIMEOUT_MS,
|
|
4547
|
+
signal: opts.signal
|
|
4548
|
+
});
|
|
4549
|
+
loaded[i].loaded = true;
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
loaded[i].loaded = false;
|
|
4552
|
+
if (!error) error = condense(stderrOf(err));
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
} catch (err) {
|
|
4556
|
+
error = condense(stderrOf(err));
|
|
4557
|
+
} finally {
|
|
4558
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4561
|
+
return {
|
|
4562
|
+
driver: this.name,
|
|
4563
|
+
moduleSystem: target.moduleSystem,
|
|
4564
|
+
installed,
|
|
4565
|
+
loaded,
|
|
4566
|
+
durationMs: Date.now() - started,
|
|
4567
|
+
error
|
|
4568
|
+
};
|
|
4569
|
+
}
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
4573
|
+
|
|
4574
|
+
// src/sandbox/index.ts
|
|
4575
|
+
function getSandbox() {
|
|
4576
|
+
return new LocalSandbox();
|
|
4577
|
+
}
|
|
4578
|
+
var init_sandbox = __esm({
|
|
4579
|
+
"src/sandbox/index.ts"() {
|
|
4580
|
+
"use strict";
|
|
4581
|
+
init_esm_shims();
|
|
4582
|
+
init_local();
|
|
4583
|
+
init_types2();
|
|
4584
|
+
init_local();
|
|
4585
|
+
}
|
|
4586
|
+
});
|
|
4587
|
+
|
|
4588
|
+
// src/pipeline/sandbox.ts
|
|
4589
|
+
var sandbox_exports = {};
|
|
4590
|
+
__export(sandbox_exports, {
|
|
4591
|
+
verifyPackageInSandbox: () => verifyPackageInSandbox
|
|
4592
|
+
});
|
|
4593
|
+
async function verifyPackageInSandbox(db, pkg, version, opts = {}) {
|
|
4594
|
+
const result = await getSandbox().verify(pkg, version, opts);
|
|
4595
|
+
await storeVerificationRun(db, {
|
|
4596
|
+
packageName: pkg,
|
|
4597
|
+
version: version ?? "latest",
|
|
4598
|
+
driver: result.driver,
|
|
4599
|
+
moduleSystem: result.moduleSystem,
|
|
4600
|
+
installed: result.installed,
|
|
4601
|
+
imported: result.imported,
|
|
4602
|
+
ranScripts: result.ranScripts,
|
|
4603
|
+
durationMs: result.durationMs,
|
|
4604
|
+
error: result.error,
|
|
4605
|
+
ranAt: /* @__PURE__ */ new Date()
|
|
4606
|
+
}).catch(() => {
|
|
4607
|
+
});
|
|
4608
|
+
return result;
|
|
4609
|
+
}
|
|
4610
|
+
var init_sandbox2 = __esm({
|
|
4611
|
+
"src/pipeline/sandbox.ts"() {
|
|
4612
|
+
"use strict";
|
|
4613
|
+
init_esm_shims();
|
|
4614
|
+
init_verification();
|
|
4615
|
+
init_sandbox();
|
|
4616
|
+
}
|
|
4617
|
+
});
|
|
4618
|
+
|
|
4619
|
+
// src/pipeline/compat.ts
|
|
4620
|
+
var compat_exports = {};
|
|
4621
|
+
__export(compat_exports, {
|
|
4622
|
+
deriveCompatEdges: () => deriveCompatEdges,
|
|
4623
|
+
verifyCompatibility: () => verifyCompatibility
|
|
4624
|
+
});
|
|
4625
|
+
function deriveCompatEdges(resolved, result) {
|
|
4626
|
+
const allLoaded = result.loaded.every((l) => l.loaded === true);
|
|
4627
|
+
const edges = [];
|
|
4628
|
+
if (result.installed && allLoaded) {
|
|
4629
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
4630
|
+
for (let j = i + 1; j < resolved.length; j++) {
|
|
4631
|
+
edges.push({
|
|
4632
|
+
a: resolved[i].name,
|
|
4633
|
+
aVersion: resolved[i].version,
|
|
4634
|
+
b: resolved[j].name,
|
|
4635
|
+
bVersion: resolved[j].version,
|
|
4636
|
+
status: "compatible"
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
} else if (resolved.length === 2) {
|
|
4641
|
+
edges.push({
|
|
4642
|
+
a: resolved[0].name,
|
|
4643
|
+
aVersion: resolved[0].version,
|
|
4644
|
+
b: resolved[1].name,
|
|
4645
|
+
bVersion: resolved[1].version,
|
|
4646
|
+
status: "conflict"
|
|
4647
|
+
});
|
|
4648
|
+
}
|
|
4649
|
+
return edges;
|
|
4650
|
+
}
|
|
4651
|
+
async function verifyCompatibility(db, packages2, opts = {}) {
|
|
4652
|
+
const resolved = await Promise.all(
|
|
4653
|
+
packages2.map(async (name) => ({
|
|
4654
|
+
name,
|
|
4655
|
+
version: (await getPackageByName(db, name))?.latestVersion ?? "latest"
|
|
4656
|
+
}))
|
|
4657
|
+
);
|
|
4658
|
+
const result = await getSandbox().verifySet(
|
|
4659
|
+
resolved.map((r) => ({ name: r.name, version: r.version === "latest" ? null : r.version })),
|
|
4660
|
+
{ allowScripts: opts.allowScripts }
|
|
4661
|
+
);
|
|
4662
|
+
const edges = deriveCompatEdges(resolved, result);
|
|
4663
|
+
for (const e of edges) {
|
|
4664
|
+
const pair = canonicalPair(
|
|
4665
|
+
{ name: e.a, version: e.aVersion },
|
|
4666
|
+
{ name: e.b, version: e.bVersion }
|
|
4667
|
+
);
|
|
4668
|
+
await upsertCompatEdge(db, {
|
|
4669
|
+
...pair,
|
|
4670
|
+
status: e.status,
|
|
4671
|
+
driver: result.driver,
|
|
4672
|
+
ranAt: /* @__PURE__ */ new Date()
|
|
4673
|
+
}).catch(() => {
|
|
4674
|
+
});
|
|
4675
|
+
}
|
|
4676
|
+
const failed = !result.installed || !result.loaded.every((l) => l.loaded === true);
|
|
4677
|
+
return { result, edges, unattributedConflict: failed && edges.length === 0 };
|
|
4678
|
+
}
|
|
4679
|
+
var init_compat2 = __esm({
|
|
4680
|
+
"src/pipeline/compat.ts"() {
|
|
4681
|
+
"use strict";
|
|
4682
|
+
init_esm_shims();
|
|
4683
|
+
init_compat();
|
|
4684
|
+
init_packages();
|
|
4685
|
+
init_sandbox();
|
|
4686
|
+
}
|
|
4687
|
+
});
|
|
4688
|
+
|
|
4689
|
+
// src/cli/commands.ts
|
|
4690
|
+
var commands_exports = {};
|
|
4691
|
+
__export(commands_exports, {
|
|
4692
|
+
runCompare: () => runCompare,
|
|
4693
|
+
runCompat: () => runCompat,
|
|
4694
|
+
runEditWeights: () => runEditWeights,
|
|
4695
|
+
runEvaluate: () => runEvaluate,
|
|
4696
|
+
runPlan: () => runPlan,
|
|
2319
4697
|
runRecommend: () => runRecommend,
|
|
2320
|
-
|
|
4698
|
+
runSandbox: () => runSandbox,
|
|
4699
|
+
runVerify: () => runVerify,
|
|
4700
|
+
runVersions: () => runVersions,
|
|
4701
|
+
runWatch: () => runWatch,
|
|
4702
|
+
runWeights: () => runWeights
|
|
2321
4703
|
});
|
|
2322
4704
|
async function withDb(fn) {
|
|
2323
4705
|
requireConfig(["DATABASE_URL"]);
|
|
@@ -2345,10 +4727,11 @@ async function runRecommend(need, opts) {
|
|
|
2345
4727
|
}
|
|
2346
4728
|
console.log(
|
|
2347
4729
|
table(
|
|
2348
|
-
["Package", "Health", "Confidence", "Weekly", "Latest", "Category"],
|
|
4730
|
+
["Package", "Health", "Quality", "Confidence", "Weekly", "Latest", "Category"],
|
|
2349
4731
|
res.candidates.map((c) => [
|
|
2350
4732
|
c.name,
|
|
2351
4733
|
String(c.healthScore),
|
|
4734
|
+
c.qualityScore != null ? String(c.qualityScore) : "\u2014",
|
|
2352
4735
|
confidenceLabel(c.confidence),
|
|
2353
4736
|
formatNumber(c.weeklyDownloads),
|
|
2354
4737
|
c.latestVersion ?? "\u2014",
|
|
@@ -2373,9 +4756,10 @@ async function runEvaluate(pkg, opts) {
|
|
|
2373
4756
|
console.log(
|
|
2374
4757
|
detail([
|
|
2375
4758
|
["health", `${res.healthScore} ${confidenceLabel(res.confidence)}`],
|
|
4759
|
+
["quality", res.qualityScore != null ? String(res.qualityScore) : "\u2014"],
|
|
2376
4760
|
[
|
|
2377
4761
|
"breakdown",
|
|
2378
|
-
`maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"}`
|
|
4762
|
+
`maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"} \xB7 qual ${b.quality ?? "\u2014"}`
|
|
2379
4763
|
],
|
|
2380
4764
|
["version", res.latestVersion ?? "\u2014"],
|
|
2381
4765
|
["weekly dl", `${formatNumber(res.weeklyDownloads)} (${formatPercent(res.downloadGrowth90d)} 90d)`],
|
|
@@ -2387,6 +4771,12 @@ async function runEvaluate(pkg, opts) {
|
|
|
2387
4771
|
["repo", res.repoUrl ?? "\u2014"]
|
|
2388
4772
|
])
|
|
2389
4773
|
);
|
|
4774
|
+
if (res.buildVerified) {
|
|
4775
|
+
const bv = res.buildVerified;
|
|
4776
|
+
const state = !bv.installed ? red("install failed") : bv.loaded === false ? yellow("installs, load failed") : green("installs and loads");
|
|
4777
|
+
console.log(`
|
|
4778
|
+
sandbox: ${state} ${dim(`${bv.version} \xB7 ${bv.driver}`)}`);
|
|
4779
|
+
}
|
|
2390
4780
|
if (res.summary) console.log("\n" + res.summary);
|
|
2391
4781
|
if (res.usageGuide) {
|
|
2392
4782
|
const g = res.usageGuide;
|
|
@@ -2424,8 +4814,148 @@ async function runCompare(pkgs, opts) {
|
|
|
2424
4814
|
])
|
|
2425
4815
|
)
|
|
2426
4816
|
);
|
|
2427
|
-
if (res.missing?.length) console.log(dim(`
|
|
2428
|
-
not found: ${res.missing.join(", ")}`));
|
|
4817
|
+
if (res.missing?.length) console.log(dim(`
|
|
4818
|
+
not found: ${res.missing.join(", ")}`));
|
|
4819
|
+
console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
|
|
4820
|
+
});
|
|
4821
|
+
}
|
|
4822
|
+
function runWeights(opts = {}) {
|
|
4823
|
+
const w = loadWeights();
|
|
4824
|
+
const active = activeWeightsPath();
|
|
4825
|
+
if (opts.json) {
|
|
4826
|
+
console.log(JSON.stringify({ ...w, source: active?.source ?? "defaults" }, null, 2));
|
|
4827
|
+
return;
|
|
4828
|
+
}
|
|
4829
|
+
const pct = (n) => n.toFixed(2);
|
|
4830
|
+
console.log(bold("Two axes, blended for default sort:"));
|
|
4831
|
+
console.log(` composite = (1\u2212\u03BB)\xB7health + \u03BB\xB7quality \u03BB = ${pct(w.composite.lambda)}
|
|
4832
|
+
`);
|
|
4833
|
+
console.log(bold("Health (proven-ness) \u2014 weighted sum of 4 components:"));
|
|
4834
|
+
console.log(
|
|
4835
|
+
detail([
|
|
4836
|
+
["maintenance", `${pct(w.health.maintenance)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.maintenance)}`],
|
|
4837
|
+
["adoption", `${pct(w.health.adoption)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.adoption)}`],
|
|
4838
|
+
["reliability", `${pct(w.health.reliability)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.reliability)}`],
|
|
4839
|
+
["efficiency", `${pct(w.health.efficiency)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.efficiency)}`]
|
|
4840
|
+
])
|
|
4841
|
+
);
|
|
4842
|
+
console.log("\n" + bold("Quality (intrinsic, adoption-independent):"));
|
|
4843
|
+
console.log(" " + dim(Object.keys(QUALITY_WEIGHTS).join(", ")));
|
|
4844
|
+
console.log("\n" + bold("Confidence thresholds:"));
|
|
4845
|
+
console.log(
|
|
4846
|
+
detail([
|
|
4847
|
+
["proven", `\u2265 ${formatNumber(CONFIDENCE.proven.minWeeklyDownloads)} weekly dl, \u2265 ${CONFIDENCE.proven.minAgeMonths}mo old`],
|
|
4848
|
+
["emerging", `\u2265 ${formatNumber(CONFIDENCE.emerging.minWeeklyDownloads)} weekly dl OR \u2265 ${CONFIDENCE.emerging.strongGrowth * 100}% 90d growth`],
|
|
4849
|
+
["promising", `\u2265 ${CONFIDENCE.promising.minQuality} quality score (adoption-independent)`]
|
|
4850
|
+
])
|
|
4851
|
+
);
|
|
4852
|
+
console.log(dim(`
|
|
4853
|
+
Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
|
|
4854
|
+
}
|
|
4855
|
+
async function runEditWeights(opts) {
|
|
4856
|
+
const { invalidateCache: invalidateCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
4857
|
+
if (opts.reset) {
|
|
4858
|
+
const removed = resetWeights();
|
|
4859
|
+
console.log(removed.length ? `Removed overrides:
|
|
4860
|
+
${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
|
|
4861
|
+
if (removed.length) await invalidateCache2();
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4864
|
+
if (opts.explain) {
|
|
4865
|
+
const key = opts.explain;
|
|
4866
|
+
const text2 = WEIGHT_EXPLANATIONS[key];
|
|
4867
|
+
if (!text2) {
|
|
4868
|
+
throw new Error(`No explanation for "${key}". Known: ${Object.keys(WEIGHT_EXPLANATIONS).join(", ")}.`);
|
|
4869
|
+
}
|
|
4870
|
+
console.log(`${bold(key)} \u2014 ${text2}`);
|
|
4871
|
+
return;
|
|
4872
|
+
}
|
|
4873
|
+
if (opts.set && opts.set.length > 0) {
|
|
4874
|
+
const next = applyOverrides(loadWeights(), opts.set);
|
|
4875
|
+
const { weights, normalized } = validateWeights(next);
|
|
4876
|
+
const path2 = saveWeights(weights, opts.project ? "project" : "user");
|
|
4877
|
+
await invalidateCache2();
|
|
4878
|
+
console.log(`Saved overrides to ${path2}`);
|
|
4879
|
+
if (normalized) {
|
|
4880
|
+
console.log(
|
|
4881
|
+
yellow("Health weights did not sum to 1.0 \u2014 renormalized to: ") + `maint ${weights.health.maintenance.toFixed(3)}, adopt ${weights.health.adoption.toFixed(3)}, rel ${weights.health.reliability.toFixed(3)}, eff ${weights.health.efficiency.toFixed(3)}`
|
|
4882
|
+
);
|
|
4883
|
+
}
|
|
4884
|
+
console.log(dim("\nRun `lurq rescore` to apply the new health weights to the stored index."));
|
|
4885
|
+
return;
|
|
4886
|
+
}
|
|
4887
|
+
console.log(dim(`No changes. Settable keys: ${settableKeys().join(", ")}.
|
|
4888
|
+
`));
|
|
4889
|
+
runWeights(opts);
|
|
4890
|
+
}
|
|
4891
|
+
async function openInBrowser(target) {
|
|
4892
|
+
const { spawn } = await import("child_process");
|
|
4893
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [target]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", target]] : ["xdg-open", [target]];
|
|
4894
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
4895
|
+
}
|
|
4896
|
+
async function runPlan(file, opts) {
|
|
4897
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
4898
|
+
let document;
|
|
4899
|
+
try {
|
|
4900
|
+
document = readFileSync4(file, "utf8");
|
|
4901
|
+
} catch {
|
|
4902
|
+
throw new Error(`Could not read "${file}".`);
|
|
4903
|
+
}
|
|
4904
|
+
if (opts.optimize && opts.optimize !== "speed" && opts.optimize !== "balanced") {
|
|
4905
|
+
throw new Error("--optimize must be 'speed' or 'balanced'.");
|
|
4906
|
+
}
|
|
4907
|
+
await withDb(async (db) => {
|
|
4908
|
+
const { handlePlan: handlePlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
4909
|
+
const res = await handlePlan2(db, {
|
|
4910
|
+
document,
|
|
4911
|
+
optimize: opts.optimize
|
|
4912
|
+
});
|
|
4913
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
4914
|
+
if (!("slots" in res)) {
|
|
4915
|
+
console.log(res.note);
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
if (opts.html || opts.open) {
|
|
4919
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
4920
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
4921
|
+
const { join: join5 } = await import("path");
|
|
4922
|
+
const { renderPlanHtml: renderPlanHtml2 } = await Promise.resolve().then(() => (init_planView(), planView_exports));
|
|
4923
|
+
const out = opts.html ?? join5(tmpdir2(), `lurq-plan-${Date.now()}.html`);
|
|
4924
|
+
writeFileSync3(out, renderPlanHtml2(res), "utf8");
|
|
4925
|
+
console.log(`Roadmap written to ${out}`);
|
|
4926
|
+
if (opts.open) await openInBrowser(out);
|
|
4927
|
+
}
|
|
4928
|
+
console.log(
|
|
4929
|
+
table(
|
|
4930
|
+
["Component", "Layer", "Recommended", "Health", "Confidence", "Alternatives"],
|
|
4931
|
+
res.slots.map((s) => [
|
|
4932
|
+
s.need.length > 32 ? s.need.slice(0, 31) + "\u2026" : s.need,
|
|
4933
|
+
s.layer,
|
|
4934
|
+
s.recommended ? `${s.recommended.name}@${s.recommended.latestVersion ?? "?"}` : dim("\u2014"),
|
|
4935
|
+
s.recommended ? String(s.recommended.healthScore) : "\u2014",
|
|
4936
|
+
s.recommended ? confidenceLabel(s.recommended.confidence) : "\u2014",
|
|
4937
|
+
s.alternatives.map((a) => a.name).join(", ") || "\u2014"
|
|
4938
|
+
])
|
|
4939
|
+
)
|
|
4940
|
+
);
|
|
4941
|
+
if (res.unmatched.length) console.log(yellow(`
|
|
4942
|
+
No match for: ${res.unmatched.join(", ")}`));
|
|
4943
|
+
if (res.compatibility) {
|
|
4944
|
+
const c = res.compatibility;
|
|
4945
|
+
const col = c.overall === "compatible" ? green : c.overall === "conflict" ? red : dim;
|
|
4946
|
+
console.log("\n" + bold("Compatibility: ") + col(c.overall));
|
|
4947
|
+
for (const s of res.slots) {
|
|
4948
|
+
if (s.swappedFrom && s.recommended) {
|
|
4949
|
+
console.log(green(` \u2713 swapped ${s.swappedFrom} \u2192 ${s.recommended.name} for compatibility`));
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
for (const cf of c.conflicts) console.log(red(` \u2717 ${cf.detail} (no compatible alternative)`));
|
|
4953
|
+
if (c.unverified.length) console.log(dim(` unverified: ${c.unverified.join(", ")}`));
|
|
4954
|
+
}
|
|
4955
|
+
console.log("\n" + bold("Roadmap (Mermaid):"));
|
|
4956
|
+
console.log(res.mermaid);
|
|
4957
|
+
console.log(dim(`
|
|
4958
|
+
${res.note}`));
|
|
2429
4959
|
console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
|
|
2430
4960
|
});
|
|
2431
4961
|
}
|
|
@@ -2433,7 +4963,8 @@ async function runVerify(pkg, opts) {
|
|
|
2433
4963
|
await withDb(async (db) => {
|
|
2434
4964
|
const res = await handleVerify(db, { package: pkg });
|
|
2435
4965
|
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
2436
|
-
const
|
|
4966
|
+
const riskColor = res.risk === "high" ? red : res.risk === "medium" ? yellow : green;
|
|
4967
|
+
const verdict = !res.exists ? red("\u2717 NOT FOUND on npm") : res.risk === "high" ? red("\u2717 high supply-chain risk") : res.risk === "medium" ? yellow("\u26A0 exists, but risky") : green("\u2713 looks safe");
|
|
2437
4968
|
console.log(`${bold(pkg)} ${verdict}`);
|
|
2438
4969
|
console.log(
|
|
2439
4970
|
detail([
|
|
@@ -2441,11 +4972,107 @@ async function runVerify(pkg, opts) {
|
|
|
2441
4972
|
["weekly dl", formatNumber(res.weeklyDownloads)],
|
|
2442
4973
|
["confidence", res.confidence ? confidenceLabel(res.confidence) : "\u2014"],
|
|
2443
4974
|
["advisories", String(res.advisoryCount)],
|
|
4975
|
+
["risk", riskColor(res.risk)],
|
|
2444
4976
|
["risk flags", res.riskFlags.length ? yellow(res.riskFlags.join(", ")) : "none"]
|
|
2445
4977
|
])
|
|
2446
4978
|
);
|
|
2447
4979
|
});
|
|
2448
4980
|
}
|
|
4981
|
+
async function runVersions(pkg, opts) {
|
|
4982
|
+
const limit = opts.limit ? Math.max(1, parseInt(opts.limit, 10) || 30) : 30;
|
|
4983
|
+
await withDb(async (db) => {
|
|
4984
|
+
const versions = await getPackageVersions(db, pkg, limit);
|
|
4985
|
+
if (opts.json) return console.log(JSON.stringify(versions, null, 2));
|
|
4986
|
+
if (versions.length === 0) {
|
|
4987
|
+
console.log(`No stored versions for ${bold(pkg)}. Run \`lurq sync --package ${pkg}\` first.`);
|
|
4988
|
+
return;
|
|
4989
|
+
}
|
|
4990
|
+
console.log(bold(pkg));
|
|
4991
|
+
console.log(
|
|
4992
|
+
table(
|
|
4993
|
+
["Version", "Published"],
|
|
4994
|
+
versions.map((v) => [
|
|
4995
|
+
v.version,
|
|
4996
|
+
v.publishedAt ? v.publishedAt.toISOString().slice(0, 10) : "\u2014"
|
|
4997
|
+
])
|
|
4998
|
+
)
|
|
4999
|
+
);
|
|
5000
|
+
});
|
|
5001
|
+
}
|
|
5002
|
+
async function runWatch() {
|
|
5003
|
+
const { watchNpmChanges: watchNpmChanges2 } = await Promise.resolve().then(() => (init_watch2(), watch_exports));
|
|
5004
|
+
await withDb(async (db) => {
|
|
5005
|
+
const controller = new AbortController();
|
|
5006
|
+
const stop = () => controller.abort();
|
|
5007
|
+
process.once("SIGINT", stop);
|
|
5008
|
+
process.once("SIGTERM", stop);
|
|
5009
|
+
console.log(dim("watching npm for releases of tracked packages \u2014 Ctrl-C to stop"));
|
|
5010
|
+
try {
|
|
5011
|
+
await watchNpmChanges2(db, { signal: controller.signal });
|
|
5012
|
+
} finally {
|
|
5013
|
+
process.off("SIGINT", stop);
|
|
5014
|
+
process.off("SIGTERM", stop);
|
|
5015
|
+
}
|
|
5016
|
+
});
|
|
5017
|
+
}
|
|
5018
|
+
async function runSandbox(pkg, version, opts) {
|
|
5019
|
+
if (opts.allowScripts) {
|
|
5020
|
+
console.error(
|
|
5021
|
+
yellow("warning: running install scripts and loading the package locally without isolation")
|
|
5022
|
+
);
|
|
5023
|
+
}
|
|
5024
|
+
const { verifyPackageInSandbox: verifyPackageInSandbox2 } = await Promise.resolve().then(() => (init_sandbox2(), sandbox_exports));
|
|
5025
|
+
await withDb(async (db) => {
|
|
5026
|
+
const result = await verifyPackageInSandbox2(db, pkg, version ?? null, {
|
|
5027
|
+
target: { node: "20", moduleSystem: opts.esm ? "esm" : "cjs" },
|
|
5028
|
+
allowScripts: opts.allowScripts
|
|
5029
|
+
});
|
|
5030
|
+
if (opts.json) return console.log(JSON.stringify(result, null, 2));
|
|
5031
|
+
const ok = result.installed && result.imported !== false;
|
|
5032
|
+
const label = version ? `${pkg}@${version}` : pkg;
|
|
5033
|
+
const verdict = ok ? green("\u2713 installs and loads") : red("\u2717 failed");
|
|
5034
|
+
console.log(`${bold(label)} ${verdict} ${dim(`(${result.durationMs}ms \xB7 ${result.driver})`)}`);
|
|
5035
|
+
console.log(
|
|
5036
|
+
detail([
|
|
5037
|
+
["installed", result.installed ? "yes" : "no"],
|
|
5038
|
+
["loaded", result.imported === null ? "\u2014" : result.imported ? "yes" : "no"],
|
|
5039
|
+
["module", result.moduleSystem],
|
|
5040
|
+
["scripts", result.ranScripts ? "ran" : "skipped"],
|
|
5041
|
+
["error", result.error ?? "none"]
|
|
5042
|
+
])
|
|
5043
|
+
);
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
async function runCompat(pkgs, opts) {
|
|
5047
|
+
await withDb(async (db) => {
|
|
5048
|
+
if (opts.run) {
|
|
5049
|
+
console.error(
|
|
5050
|
+
yellow("co-installing in the sandbox (loads package code locally without isolation)")
|
|
5051
|
+
);
|
|
5052
|
+
const { verifyCompatibility: verifyCompatibility2 } = await Promise.resolve().then(() => (init_compat2(), compat_exports));
|
|
5053
|
+
await verifyCompatibility2(db, pkgs);
|
|
5054
|
+
}
|
|
5055
|
+
const { handleCompat: handleCompat2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
|
|
5056
|
+
const res = await handleCompat2(db, { packages: pkgs });
|
|
5057
|
+
if (opts.json) return console.log(JSON.stringify(res, null, 2));
|
|
5058
|
+
const color = res.overall === "compatible" ? green : res.overall === "conflict" ? red : dim;
|
|
5059
|
+
console.log(`${bold(res.packages.join(" + "))} ${color(res.overall)}`);
|
|
5060
|
+
if (res.conflicts.length) {
|
|
5061
|
+
console.log(
|
|
5062
|
+
table(
|
|
5063
|
+
["Source", "Detail"],
|
|
5064
|
+
res.conflicts.map((c) => [c.source, c.detail])
|
|
5065
|
+
)
|
|
5066
|
+
);
|
|
5067
|
+
} else if (res.overall === "compatible") {
|
|
5068
|
+
console.log(dim("no peer-dependency or engine conflicts across the set"));
|
|
5069
|
+
}
|
|
5070
|
+
if (res.unverified.length) {
|
|
5071
|
+
console.log(dim(`
|
|
5072
|
+
unverified (no metadata): ${res.unverified.join(", ")}`));
|
|
5073
|
+
}
|
|
5074
|
+
});
|
|
5075
|
+
}
|
|
2449
5076
|
var init_commands = __esm({
|
|
2450
5077
|
"src/cli/commands.ts"() {
|
|
2451
5078
|
"use strict";
|
|
@@ -2453,58 +5080,35 @@ var init_commands = __esm({
|
|
|
2453
5080
|
init_config();
|
|
2454
5081
|
init_types();
|
|
2455
5082
|
init_client();
|
|
5083
|
+
init_packages();
|
|
2456
5084
|
init_handlers();
|
|
5085
|
+
init_weights();
|
|
5086
|
+
init_weights();
|
|
2457
5087
|
init_format();
|
|
2458
5088
|
}
|
|
2459
5089
|
});
|
|
2460
5090
|
|
|
2461
|
-
// src/core/paths.ts
|
|
2462
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2463
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
2464
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2465
|
-
function packageRoot() {
|
|
2466
|
-
if (cachedRoot) return cachedRoot;
|
|
2467
|
-
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
2468
|
-
for (; ; ) {
|
|
2469
|
-
if (existsSync2(join2(dir, "package.json"))) {
|
|
2470
|
-
cachedRoot = dir;
|
|
2471
|
-
return dir;
|
|
2472
|
-
}
|
|
2473
|
-
const parent = dirname2(dir);
|
|
2474
|
-
if (parent === dir) break;
|
|
2475
|
-
dir = parent;
|
|
2476
|
-
}
|
|
2477
|
-
cachedRoot = process.cwd();
|
|
2478
|
-
return cachedRoot;
|
|
2479
|
-
}
|
|
2480
|
-
function migrationsDir() {
|
|
2481
|
-
return join2(packageRoot(), "drizzle");
|
|
2482
|
-
}
|
|
2483
|
-
function seedJsonPath() {
|
|
2484
|
-
return join2(packageRoot(), "src", "data", "seed.json");
|
|
2485
|
-
}
|
|
2486
|
-
var cachedRoot;
|
|
2487
|
-
var init_paths = __esm({
|
|
2488
|
-
"src/core/paths.ts"() {
|
|
2489
|
-
"use strict";
|
|
2490
|
-
init_esm_shims();
|
|
2491
|
-
}
|
|
2492
|
-
});
|
|
2493
|
-
|
|
2494
5091
|
// src/cli/installSkill.ts
|
|
2495
5092
|
var installSkill_exports = {};
|
|
2496
5093
|
__export(installSkill_exports, {
|
|
2497
5094
|
SUPPORTED_AGENTS: () => SUPPORTED_AGENTS,
|
|
5095
|
+
agentSpecs: () => agentSpecs,
|
|
5096
|
+
buildRemoteServerEntry: () => buildRemoteServerEntry,
|
|
5097
|
+
buildRemoteTomlBlock: () => buildRemoteTomlBlock,
|
|
2498
5098
|
buildServerEntry: () => buildServerEntry,
|
|
2499
5099
|
buildTomlBlock: () => buildTomlBlock,
|
|
5100
|
+
installAgent: () => installAgent,
|
|
5101
|
+
installInstructionsFile: () => installInstructionsFile,
|
|
5102
|
+
printInstallReport: () => printInstallReport,
|
|
5103
|
+
resolveAgents: () => resolveAgents,
|
|
2500
5104
|
runInstallSkill: () => runInstallSkill
|
|
2501
5105
|
});
|
|
2502
|
-
import { existsSync as
|
|
5106
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2503
5107
|
import { copyFileSync } from "fs";
|
|
2504
|
-
import { homedir as
|
|
2505
|
-
import { dirname as
|
|
5108
|
+
import { homedir as homedir3 } from "os";
|
|
5109
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
2506
5110
|
function home(...p) {
|
|
2507
|
-
return
|
|
5111
|
+
return join4(homedir3(), ...p);
|
|
2508
5112
|
}
|
|
2509
5113
|
function agentSpecs() {
|
|
2510
5114
|
return [
|
|
@@ -2513,35 +5117,35 @@ function agentSpecs() {
|
|
|
2513
5117
|
label: "Claude Code",
|
|
2514
5118
|
format: "mcpServers",
|
|
2515
5119
|
path: home(".claude.json"),
|
|
2516
|
-
detected:
|
|
5120
|
+
detected: existsSync4(home(".claude.json")) || existsSync4(home(".claude"))
|
|
2517
5121
|
},
|
|
2518
5122
|
{
|
|
2519
5123
|
id: "cursor",
|
|
2520
5124
|
label: "Cursor",
|
|
2521
5125
|
format: "mcpServers",
|
|
2522
5126
|
path: home(".cursor", "mcp.json"),
|
|
2523
|
-
detected:
|
|
5127
|
+
detected: existsSync4(home(".cursor"))
|
|
2524
5128
|
},
|
|
2525
5129
|
{
|
|
2526
5130
|
id: "windsurf",
|
|
2527
5131
|
label: "Windsurf",
|
|
2528
5132
|
format: "mcpServers",
|
|
2529
5133
|
path: home(".codeium", "windsurf", "mcp_config.json"),
|
|
2530
|
-
detected:
|
|
5134
|
+
detected: existsSync4(home(".codeium"))
|
|
2531
5135
|
},
|
|
2532
5136
|
{
|
|
2533
5137
|
id: "copilot",
|
|
2534
5138
|
label: "VS Code / GitHub Copilot",
|
|
2535
5139
|
format: "servers",
|
|
2536
5140
|
path: home("Library", "Application Support", "Code", "User", "mcp.json"),
|
|
2537
|
-
detected:
|
|
5141
|
+
detected: existsSync4(home("Library", "Application Support", "Code", "User"))
|
|
2538
5142
|
},
|
|
2539
5143
|
{
|
|
2540
5144
|
id: "codex",
|
|
2541
5145
|
label: "OpenAI Codex CLI",
|
|
2542
5146
|
format: "toml",
|
|
2543
5147
|
path: home(".codex", "config.toml"),
|
|
2544
|
-
detected:
|
|
5148
|
+
detected: existsSync4(home(".codex"))
|
|
2545
5149
|
}
|
|
2546
5150
|
];
|
|
2547
5151
|
}
|
|
@@ -2554,14 +5158,14 @@ function collectEnv() {
|
|
|
2554
5158
|
return env;
|
|
2555
5159
|
}
|
|
2556
5160
|
function readJsonObject(path2) {
|
|
2557
|
-
if (!
|
|
2558
|
-
const text2 =
|
|
5161
|
+
if (!existsSync4(path2)) return {};
|
|
5162
|
+
const text2 = readFileSync2(path2, "utf8").trim();
|
|
2559
5163
|
if (!text2) return {};
|
|
2560
5164
|
return JSON.parse(text2);
|
|
2561
5165
|
}
|
|
2562
5166
|
function writeJson(path2, obj) {
|
|
2563
|
-
|
|
2564
|
-
|
|
5167
|
+
mkdirSync2(dirname4(path2), { recursive: true });
|
|
5168
|
+
writeFileSync2(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
2565
5169
|
}
|
|
2566
5170
|
function buildServerEntry(env, withType) {
|
|
2567
5171
|
const entry = { command: "npx", args: ["-y", PACKAGE_NAME, "serve"] };
|
|
@@ -2569,13 +5173,18 @@ function buildServerEntry(env, withType) {
|
|
|
2569
5173
|
if (Object.keys(env).length) entry.env = env;
|
|
2570
5174
|
return entry;
|
|
2571
5175
|
}
|
|
2572
|
-
function
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
5176
|
+
function buildRemoteServerEntry(agentId, opts) {
|
|
5177
|
+
const headers = { Authorization: `Bearer ${opts.apiKey}` };
|
|
5178
|
+
switch (agentId) {
|
|
5179
|
+
case "cursor":
|
|
5180
|
+
return { url: opts.url, headers };
|
|
5181
|
+
case "windsurf":
|
|
5182
|
+
return { serverUrl: opts.url, headers };
|
|
5183
|
+
case "claude-code":
|
|
5184
|
+
case "copilot":
|
|
5185
|
+
default:
|
|
5186
|
+
return { type: "http", url: opts.url, headers };
|
|
5187
|
+
}
|
|
2579
5188
|
}
|
|
2580
5189
|
function buildTomlBlock(env) {
|
|
2581
5190
|
const lines = ["[mcp_servers.lurq]", 'command = "npx"', `args = ["-y", "${PACKAGE_NAME}", "serve"]`];
|
|
@@ -2585,8 +5194,23 @@ function buildTomlBlock(env) {
|
|
|
2585
5194
|
}
|
|
2586
5195
|
return lines.join("\n") + "\n";
|
|
2587
5196
|
}
|
|
2588
|
-
function
|
|
2589
|
-
|
|
5197
|
+
function buildRemoteTomlBlock(opts) {
|
|
5198
|
+
return [
|
|
5199
|
+
"[mcp_servers.lurq]",
|
|
5200
|
+
`url = ${JSON.stringify(opts.url)}`,
|
|
5201
|
+
`http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.apiKey}`)} }`
|
|
5202
|
+
].join("\n") + "\n";
|
|
5203
|
+
}
|
|
5204
|
+
function installJsonEntry(spec, entry) {
|
|
5205
|
+
const key = spec.format === "servers" ? "servers" : "mcpServers";
|
|
5206
|
+
const config = readJsonObject(spec.path);
|
|
5207
|
+
if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
|
|
5208
|
+
config[key].lurq = entry;
|
|
5209
|
+
writeJson(spec.path, config);
|
|
5210
|
+
return { agent: spec.id, path: spec.path, status: "installed" };
|
|
5211
|
+
}
|
|
5212
|
+
function installTomlBlock(spec, block) {
|
|
5213
|
+
const existing = existsSync4(spec.path) ? readFileSync2(spec.path, "utf8") : "";
|
|
2590
5214
|
if (existing.includes("[mcp_servers.lurq]")) {
|
|
2591
5215
|
return {
|
|
2592
5216
|
agent: spec.id,
|
|
@@ -2595,59 +5219,48 @@ function installToml(spec, env) {
|
|
|
2595
5219
|
message: "lurq already present; edit manually to change it."
|
|
2596
5220
|
};
|
|
2597
5221
|
}
|
|
2598
|
-
|
|
5222
|
+
mkdirSync2(dirname4(spec.path), { recursive: true });
|
|
2599
5223
|
const sep = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
|
|
2600
|
-
|
|
5224
|
+
writeFileSync2(spec.path, existing + sep + block, "utf8");
|
|
2601
5225
|
return { agent: spec.id, path: spec.path, status: "installed" };
|
|
2602
5226
|
}
|
|
5227
|
+
function installAgent(spec, mode) {
|
|
5228
|
+
try {
|
|
5229
|
+
if (spec.format === "toml") {
|
|
5230
|
+
const block = mode.kind === "remote" ? buildRemoteTomlBlock(mode) : buildTomlBlock(mode.env);
|
|
5231
|
+
return installTomlBlock(spec, block);
|
|
5232
|
+
}
|
|
5233
|
+
const entry = mode.kind === "remote" ? buildRemoteServerEntry(spec.id, mode) : buildServerEntry(mode.env, spec.format === "servers");
|
|
5234
|
+
return installJsonEntry(spec, entry);
|
|
5235
|
+
} catch (err) {
|
|
5236
|
+
return {
|
|
5237
|
+
agent: spec.id,
|
|
5238
|
+
path: spec.path,
|
|
5239
|
+
status: "error",
|
|
5240
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5241
|
+
};
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
2603
5244
|
function installInstructionsFile() {
|
|
2604
|
-
const src =
|
|
2605
|
-
if (!
|
|
5245
|
+
const src = join4(packageRoot(), "templates", "skill-instructions.md");
|
|
5246
|
+
if (!existsSync4(src)) return null;
|
|
2606
5247
|
const destDir = home(".lurq");
|
|
2607
|
-
const dest =
|
|
2608
|
-
|
|
5248
|
+
const dest = join4(destDir, "skill-instructions.md");
|
|
5249
|
+
mkdirSync2(destDir, { recursive: true });
|
|
2609
5250
|
copyFileSync(src, dest);
|
|
2610
5251
|
return dest;
|
|
2611
5252
|
}
|
|
2612
|
-
|
|
2613
|
-
const target = opts.agent ?? "claude-code";
|
|
5253
|
+
function resolveAgents(target) {
|
|
2614
5254
|
const specs = agentSpecs();
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
if (
|
|
2618
|
-
|
|
2619
|
-
if (selected.length === 0) {
|
|
2620
|
-
console.log("No supported agents detected on this machine.");
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2623
|
-
} else {
|
|
2624
|
-
const spec = specs.find((s) => s.id === target);
|
|
2625
|
-
if (!spec) {
|
|
2626
|
-
throw new Error(
|
|
2627
|
-
`Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`
|
|
2628
|
-
);
|
|
2629
|
-
}
|
|
2630
|
-
selected = [spec];
|
|
2631
|
-
}
|
|
2632
|
-
if (!env.DATABASE_URL) {
|
|
2633
|
-
logger.warn(
|
|
2634
|
-
"DATABASE_URL is not set in the current environment \u2014 the installed server entry will have no DATABASE_URL. Set it (in .env) and re-run, or edit the config."
|
|
2635
|
-
);
|
|
2636
|
-
}
|
|
2637
|
-
const results = [];
|
|
2638
|
-
for (const spec of selected) {
|
|
2639
|
-
try {
|
|
2640
|
-
results.push(spec.format === "toml" ? installToml(spec, env) : installJson(spec, env));
|
|
2641
|
-
} catch (err) {
|
|
2642
|
-
results.push({
|
|
2643
|
-
agent: spec.id,
|
|
2644
|
-
path: spec.path,
|
|
2645
|
-
status: "error",
|
|
2646
|
-
message: err instanceof Error ? err.message : String(err)
|
|
2647
|
-
});
|
|
2648
|
-
}
|
|
5255
|
+
if (target === "all") return specs.filter((s) => s.detected);
|
|
5256
|
+
const spec = specs.find((s) => s.id === target);
|
|
5257
|
+
if (!spec) {
|
|
5258
|
+
throw new Error(`Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`);
|
|
2649
5259
|
}
|
|
2650
|
-
|
|
5260
|
+
return [spec];
|
|
5261
|
+
}
|
|
5262
|
+
function printInstallReport(results, instructionsPath, mode) {
|
|
5263
|
+
const specs = agentSpecs();
|
|
2651
5264
|
console.log("lurq MCP server registration:");
|
|
2652
5265
|
for (const r of results) {
|
|
2653
5266
|
const spec = specs.find((s) => s.id === r.agent);
|
|
@@ -2657,9 +5270,43 @@ async function runInstallSkill(opts) {
|
|
|
2657
5270
|
if (instructionsPath) console.log(`
|
|
2658
5271
|
Skill instructions written to ${instructionsPath}`);
|
|
2659
5272
|
console.log("\nNext steps:");
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
5273
|
+
if (mode.kind === "local") {
|
|
5274
|
+
console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
|
|
5275
|
+
console.log(" 2. Restart the agent so it picks up the new MCP server.");
|
|
5276
|
+
} else {
|
|
5277
|
+
console.log(" 1. Restart the agent so it picks up the new MCP server.");
|
|
5278
|
+
}
|
|
5279
|
+
console.log(" \u2022 Ask it to recommend a library \u2014 it should call lurq.");
|
|
5280
|
+
}
|
|
5281
|
+
async function runInstallSkill(opts) {
|
|
5282
|
+
const selected = resolveAgents(opts.agent ?? "claude-code");
|
|
5283
|
+
if (selected.length === 0) {
|
|
5284
|
+
console.log("No supported agents detected on this machine.");
|
|
5285
|
+
return;
|
|
5286
|
+
}
|
|
5287
|
+
const remote = !opts.local;
|
|
5288
|
+
let mode;
|
|
5289
|
+
if (remote) {
|
|
5290
|
+
const apiKey = opts.apiKey ?? process.env.LURQ_API_KEY;
|
|
5291
|
+
if (!apiKey) {
|
|
5292
|
+
throw new Error(
|
|
5293
|
+
"An API key is required for a hosted install. Pass --api-key <key> (or set LURQ_API_KEY), or use `lurq install` for the guided setup, or --local to self-host."
|
|
5294
|
+
);
|
|
5295
|
+
}
|
|
5296
|
+
const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
5297
|
+
mode = { kind: "remote", url, apiKey };
|
|
5298
|
+
} else {
|
|
5299
|
+
const env = collectEnv();
|
|
5300
|
+
if (!env.DATABASE_URL) {
|
|
5301
|
+
logger.warn(
|
|
5302
|
+
"DATABASE_URL is not set \u2014 the local server entry will have no DATABASE_URL. Set it (in .env) and re-run, or edit the config."
|
|
5303
|
+
);
|
|
5304
|
+
}
|
|
5305
|
+
mode = { kind: "local", env };
|
|
5306
|
+
}
|
|
5307
|
+
const results = selected.map((spec) => installAgent(spec, mode));
|
|
5308
|
+
const instructionsPath = installInstructionsFile();
|
|
5309
|
+
printInstallReport(results, instructionsPath, mode);
|
|
2663
5310
|
}
|
|
2664
5311
|
var ENV_KEYS, SUPPORTED_AGENTS;
|
|
2665
5312
|
var init_installSkill = __esm({
|
|
@@ -2683,12 +5330,279 @@ var init_installSkill = __esm({
|
|
|
2683
5330
|
}
|
|
2684
5331
|
});
|
|
2685
5332
|
|
|
5333
|
+
// src/cli/install.ts
|
|
5334
|
+
var install_exports = {};
|
|
5335
|
+
__export(install_exports, {
|
|
5336
|
+
runInstallWizard: () => runInstallWizard
|
|
5337
|
+
});
|
|
5338
|
+
async function validateKey(url, apiKey) {
|
|
5339
|
+
try {
|
|
5340
|
+
const res = await fetch(url, {
|
|
5341
|
+
method: "POST",
|
|
5342
|
+
headers: {
|
|
5343
|
+
"Content-Type": "application/json",
|
|
5344
|
+
Accept: "application/json, text/event-stream",
|
|
5345
|
+
Authorization: `Bearer ${apiKey}`
|
|
5346
|
+
},
|
|
5347
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
5348
|
+
});
|
|
5349
|
+
if (res.ok) return "valid";
|
|
5350
|
+
if (res.status === 401 || res.status === 403) return "invalid";
|
|
5351
|
+
return "unreachable";
|
|
5352
|
+
} catch {
|
|
5353
|
+
return "unreachable";
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
async function runInstallWizard(opts) {
|
|
5357
|
+
const interactive = !opts.yes;
|
|
5358
|
+
const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
5359
|
+
let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
|
|
5360
|
+
if (interactive) {
|
|
5361
|
+
const { input, checkbox, confirm, select } = await import("@inquirer/prompts");
|
|
5362
|
+
console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
|
|
5363
|
+
if (!apiKey) {
|
|
5364
|
+
console.log(` Need a key? Get one at ${GET_KEY_URL}
|
|
5365
|
+
`);
|
|
5366
|
+
apiKey = (await input({
|
|
5367
|
+
message: "Paste your lurq API key",
|
|
5368
|
+
validate: (v) => v.trim().startsWith("lurq_") ? true : "Keys look like lurq_live_\u2026 "
|
|
5369
|
+
})).trim();
|
|
5370
|
+
}
|
|
5371
|
+
process.stdout.write(" Validating key\u2026 ");
|
|
5372
|
+
const check = await validateKey(url, apiKey);
|
|
5373
|
+
console.log(
|
|
5374
|
+
check === "valid" ? "ok" : check === "invalid" ? "rejected" : "could not reach endpoint"
|
|
5375
|
+
);
|
|
5376
|
+
if (check !== "valid") {
|
|
5377
|
+
const proceed = await confirm({
|
|
5378
|
+
message: check === "invalid" ? `That key was rejected (401) by ${url}. Continue anyway?` : `Couldn't reach ${url} to validate the key. Continue anyway?`,
|
|
5379
|
+
default: false
|
|
5380
|
+
});
|
|
5381
|
+
if (!proceed) {
|
|
5382
|
+
console.log("Aborted. No config was changed.");
|
|
5383
|
+
return;
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
if (opts.agent) {
|
|
5387
|
+
await finish(resolveAgents(opts.agent), { url, apiKey });
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
const specs = agentSpecs();
|
|
5391
|
+
const detected = specs.filter((s) => s.detected);
|
|
5392
|
+
const primary = detected.find((s) => s.id === "claude-code") ?? detected[0];
|
|
5393
|
+
if (primary) {
|
|
5394
|
+
const others = detected.length - 1;
|
|
5395
|
+
const choice = await select({
|
|
5396
|
+
message: others > 0 ? `Looks like you have ${detected.map((s) => s.label).join(", ")}. Connect lurq to them?` : `Looks like you have ${primary.label}. Connect lurq to it?`,
|
|
5397
|
+
default: "yes",
|
|
5398
|
+
choices: [
|
|
5399
|
+
{
|
|
5400
|
+
name: others > 0 ? `Yes \u2014 set up all ${detected.length} detected agents` : `Yes \u2014 set up ${primary.label} for me`,
|
|
5401
|
+
value: "yes"
|
|
5402
|
+
},
|
|
5403
|
+
{ name: "No \u2014 let me choose which agent(s)", value: "other" },
|
|
5404
|
+
{ name: "Cancel \u2014 change nothing", value: "cancel" }
|
|
5405
|
+
]
|
|
5406
|
+
});
|
|
5407
|
+
if (choice === "cancel") {
|
|
5408
|
+
console.log("No problem \u2014 nothing was changed. Run `lurq install` again anytime.");
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
if (choice === "yes") {
|
|
5412
|
+
await finish(detected, { url, apiKey });
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
const ids = await checkbox({
|
|
5417
|
+
message: "Which assistant(s) should I configure?",
|
|
5418
|
+
choices: specs.map((s) => ({
|
|
5419
|
+
name: `${s.label}${s.detected ? " (detected)" : ""}`,
|
|
5420
|
+
value: s.id,
|
|
5421
|
+
checked: s.detected
|
|
5422
|
+
}))
|
|
5423
|
+
});
|
|
5424
|
+
await finish(specs.filter((s) => ids.includes(s.id)), { url, apiKey });
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5427
|
+
if (!apiKey) {
|
|
5428
|
+
throw new Error(
|
|
5429
|
+
"No API key. Pass --api-key <key> or set LURQ_API_KEY (or drop --yes to be prompted)."
|
|
5430
|
+
);
|
|
5431
|
+
}
|
|
5432
|
+
const selected = opts.agent ? resolveAgents(opts.agent) : agentSpecs().filter((s) => s.detected);
|
|
5433
|
+
await finish(selected, { url, apiKey });
|
|
5434
|
+
}
|
|
5435
|
+
async function finish(selected, remote) {
|
|
5436
|
+
if (selected.length === 0) {
|
|
5437
|
+
console.log(
|
|
5438
|
+
"\nNo agents selected or detected. Re-run with --agent <id>, or install an assistant first."
|
|
5439
|
+
);
|
|
5440
|
+
return;
|
|
5441
|
+
}
|
|
5442
|
+
const mode = { kind: "remote", ...remote };
|
|
5443
|
+
const results = selected.map((s) => installAgent(s, mode));
|
|
5444
|
+
const instructionsPath = installInstructionsFile();
|
|
5445
|
+
console.log("");
|
|
5446
|
+
printInstallReport(results, instructionsPath, mode);
|
|
5447
|
+
}
|
|
5448
|
+
var GET_KEY_URL;
|
|
5449
|
+
var init_install = __esm({
|
|
5450
|
+
"src/cli/install.ts"() {
|
|
5451
|
+
"use strict";
|
|
5452
|
+
init_esm_shims();
|
|
5453
|
+
init_constants();
|
|
5454
|
+
init_installSkill();
|
|
5455
|
+
GET_KEY_URL = "https://lurq.run";
|
|
5456
|
+
}
|
|
5457
|
+
});
|
|
5458
|
+
|
|
5459
|
+
// src/cli/keys.ts
|
|
5460
|
+
var keys_exports = {};
|
|
5461
|
+
__export(keys_exports, {
|
|
5462
|
+
runKeysCreate: () => runKeysCreate,
|
|
5463
|
+
runKeysList: () => runKeysList,
|
|
5464
|
+
runKeysRevoke: () => runKeysRevoke,
|
|
5465
|
+
runKeysRotate: () => runKeysRotate
|
|
5466
|
+
});
|
|
5467
|
+
function waitForEnter(prompt) {
|
|
5468
|
+
return new Promise((resolve) => {
|
|
5469
|
+
process.stdout.write(prompt);
|
|
5470
|
+
process.stdin.resume();
|
|
5471
|
+
process.stdin.once("data", () => {
|
|
5472
|
+
process.stdin.pause();
|
|
5473
|
+
resolve();
|
|
5474
|
+
});
|
|
5475
|
+
});
|
|
5476
|
+
}
|
|
5477
|
+
async function presentNewKey(key, row, opts) {
|
|
5478
|
+
if (opts.json) {
|
|
5479
|
+
console.log(
|
|
5480
|
+
JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label, ...opts.extraJson })
|
|
5481
|
+
);
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5484
|
+
const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
|
|
5485
|
+
const block = [bold(opts.header ?? "API key created."), "", ` ${green(key)}`, "", dim(meta)];
|
|
5486
|
+
console.log(block.join("\n"));
|
|
5487
|
+
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
5488
|
+
await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
|
|
5489
|
+
process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
|
|
5490
|
+
console.log(
|
|
5491
|
+
dim(`New key (prefix ${row.prefix}) erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
|
|
5492
|
+
);
|
|
5493
|
+
} else {
|
|
5494
|
+
console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
async function runKeysCreate(opts) {
|
|
5498
|
+
requireConfig(["DATABASE_URL"]);
|
|
5499
|
+
const { db, close } = createDb({ max: 1 });
|
|
5500
|
+
try {
|
|
5501
|
+
const { key, row } = await createKey(db, { label: opts.label, tier: opts.tier });
|
|
5502
|
+
await presentNewKey(key, row, opts);
|
|
5503
|
+
} finally {
|
|
5504
|
+
await close();
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
async function runKeysRotate(prefixOrId, opts) {
|
|
5508
|
+
requireConfig(["DATABASE_URL"]);
|
|
5509
|
+
const { db, close } = createDb({ max: 1 });
|
|
5510
|
+
try {
|
|
5511
|
+
const result = await rotateKey(db, prefixOrId);
|
|
5512
|
+
if (!result) {
|
|
5513
|
+
logger.warn(`No active key matched "${prefixOrId}".`);
|
|
5514
|
+
process.exitCode = 1;
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
await presentNewKey(result.key, result.row, {
|
|
5518
|
+
json: opts.json,
|
|
5519
|
+
header: `API key rotated \u2014 replaces ${result.previous.prefix} (now revoked).`,
|
|
5520
|
+
extraJson: { replaced: result.previous.prefix }
|
|
5521
|
+
});
|
|
5522
|
+
} finally {
|
|
5523
|
+
await close();
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
async function runKeysList(opts) {
|
|
5527
|
+
requireConfig(["DATABASE_URL"]);
|
|
5528
|
+
const { db, close } = createDb({ max: 1 });
|
|
5529
|
+
try {
|
|
5530
|
+
const rows = await listKeys(db);
|
|
5531
|
+
if (opts.json) {
|
|
5532
|
+
console.log(
|
|
5533
|
+
JSON.stringify(
|
|
5534
|
+
rows.map((r) => ({
|
|
5535
|
+
id: r.id,
|
|
5536
|
+
prefix: r.prefix,
|
|
5537
|
+
label: r.label,
|
|
5538
|
+
tier: r.tier,
|
|
5539
|
+
ownerId: r.ownerId,
|
|
5540
|
+
createdAt: r.createdAt,
|
|
5541
|
+
lastUsedAt: r.lastUsedAt,
|
|
5542
|
+
revokedAt: r.revokedAt
|
|
5543
|
+
})),
|
|
5544
|
+
null,
|
|
5545
|
+
2
|
|
5546
|
+
)
|
|
5547
|
+
);
|
|
5548
|
+
return;
|
|
5549
|
+
}
|
|
5550
|
+
if (rows.length === 0) {
|
|
5551
|
+
console.log("No API keys yet. Create one with `lurq keys create --label <name>`.");
|
|
5552
|
+
return;
|
|
5553
|
+
}
|
|
5554
|
+
console.log(
|
|
5555
|
+
table(
|
|
5556
|
+
["prefix", "label", "tier", "created", "last used", "status"],
|
|
5557
|
+
rows.map((r) => [
|
|
5558
|
+
r.prefix,
|
|
5559
|
+
r.label ?? "\u2014",
|
|
5560
|
+
r.tier,
|
|
5561
|
+
isoDay(r.createdAt),
|
|
5562
|
+
isoDay(r.lastUsedAt),
|
|
5563
|
+
r.revokedAt ? "revoked" : "active"
|
|
5564
|
+
])
|
|
5565
|
+
)
|
|
5566
|
+
);
|
|
5567
|
+
} finally {
|
|
5568
|
+
await close();
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
async function runKeysRevoke(prefixOrId) {
|
|
5572
|
+
requireConfig(["DATABASE_URL"]);
|
|
5573
|
+
const { db, close } = createDb({ max: 1 });
|
|
5574
|
+
try {
|
|
5575
|
+
const n = await revokeKey(db, prefixOrId);
|
|
5576
|
+
if (n === 0) {
|
|
5577
|
+
logger.warn(`No active key matched "${prefixOrId}".`);
|
|
5578
|
+
process.exitCode = 1;
|
|
5579
|
+
return;
|
|
5580
|
+
}
|
|
5581
|
+
console.log(`Revoked ${n} key(s) matching "${prefixOrId}".`);
|
|
5582
|
+
} finally {
|
|
5583
|
+
await close();
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
var isoDay;
|
|
5587
|
+
var init_keys = __esm({
|
|
5588
|
+
"src/cli/keys.ts"() {
|
|
5589
|
+
"use strict";
|
|
5590
|
+
init_esm_shims();
|
|
5591
|
+
init_config();
|
|
5592
|
+
init_logger();
|
|
5593
|
+
init_apiKeys();
|
|
5594
|
+
init_client();
|
|
5595
|
+
init_format();
|
|
5596
|
+
isoDay = (d) => d ? d.toISOString().slice(0, 10) : "\u2014";
|
|
5597
|
+
}
|
|
5598
|
+
});
|
|
5599
|
+
|
|
2686
5600
|
// src/db/seed.ts
|
|
2687
|
-
import { readFileSync as
|
|
5601
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2688
5602
|
import { sql as sql5 } from "drizzle-orm";
|
|
2689
5603
|
import { z as z3 } from "zod";
|
|
2690
5604
|
function loadSeedFile(path2 = seedJsonPath()) {
|
|
2691
|
-
const raw = JSON.parse(
|
|
5605
|
+
const raw = JSON.parse(readFileSync3(path2, "utf8"));
|
|
2692
5606
|
const parsed = SeedFileSchema.parse(raw);
|
|
2693
5607
|
const byName = /* @__PURE__ */ new Map();
|
|
2694
5608
|
for (const entry of parsed) {
|
|
@@ -2732,7 +5646,13 @@ __export(migrate_exports, {
|
|
|
2732
5646
|
});
|
|
2733
5647
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
2734
5648
|
async function ensureVectorExtension(handle) {
|
|
2735
|
-
|
|
5649
|
+
try {
|
|
5650
|
+
await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
|
|
5651
|
+
} catch (err) {
|
|
5652
|
+
logger.warn(
|
|
5653
|
+
`CREATE EXTENSION vector did not run cleanly (continuing to migrations): ${err instanceof Error ? err.message : String(err)}`
|
|
5654
|
+
);
|
|
5655
|
+
}
|
|
2736
5656
|
}
|
|
2737
5657
|
async function runMigrate() {
|
|
2738
5658
|
const handle = createDb({ max: 1 });
|
|
@@ -2791,13 +5711,17 @@ function buildProgram() {
|
|
|
2791
5711
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
2792
5712
|
await startMcpServer2();
|
|
2793
5713
|
});
|
|
5714
|
+
program.command("serve-http").description("start the hosted MCP server over HTTP with API-key auth").option("--port <n>", "port to listen on (default: $PORT or 8080)", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
5715
|
+
const { startHttpServer: startHttpServer2 } = await Promise.resolve().then(() => (init_http2(), http_exports));
|
|
5716
|
+
await startHttpServer2({ port: opts.port });
|
|
5717
|
+
});
|
|
2794
5718
|
program.command("sync").description("run ingestion: refresh scores for the seed list (or one package)").option("--full", "force a full re-sync, ignoring cache TTLs").option("--package <name>", "sync a single package by name").option("--json", "output the run summary as JSON").action(async (opts) => {
|
|
2795
5719
|
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
2796
5720
|
const summary = await runSync2({ full: opts.full, packageName: opts.package });
|
|
2797
5721
|
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
2798
5722
|
if (summary.status === "failed") process.exitCode = 1;
|
|
2799
5723
|
});
|
|
2800
|
-
program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
|
|
5724
|
+
program.command("recommend").argument("<need>", "natural-language description of what you need").description("recommend the best current packages for a described need").option("--category <category>", "restrict to a taxonomy category").option("--min-confidence <level>", "proven | emerging | promising | unproven").option("--json", "output JSON instead of a table").action(async (need, opts) => {
|
|
2801
5725
|
const { runRecommend: runRecommend2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2802
5726
|
await runRecommend2(need, opts);
|
|
2803
5727
|
});
|
|
@@ -2813,14 +5737,83 @@ function buildProgram() {
|
|
|
2813
5737
|
const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2814
5738
|
await runVerify2(pkg, opts);
|
|
2815
5739
|
});
|
|
2816
|
-
program.command("
|
|
5740
|
+
program.command("versions").argument("<package>", "npm package name").description("show the stored version timeline for a package").option("--json", "output JSON instead of a table").option("-n, --limit <n>", "how many versions to show (default 30)").action(async (pkg, opts) => {
|
|
5741
|
+
const { runVersions: runVersions2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5742
|
+
await runVersions2(pkg, opts);
|
|
5743
|
+
});
|
|
5744
|
+
program.command("plan").argument("<file>", "path to a markdown file describing your program").description("turn a program description into an evidence-scored package plan + roadmap").option("--optimize <mode>", "ranking bias: 'speed' (lightest bundle) or 'balanced'").option("--html <path>", "write the roadmap as a self-contained HTML visualization").option("--open", "render the roadmap to HTML and open it in your browser").option("--json", "output the full plan as JSON").action(async (file, opts) => {
|
|
5745
|
+
const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5746
|
+
await runPlan2(file, opts);
|
|
5747
|
+
});
|
|
5748
|
+
program.command("weights").description("show and explain the scoring weight model (health, quality, composite \u03BB)").option("--json", "output the weight model as JSON").action(async (opts) => {
|
|
5749
|
+
const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5750
|
+
runWeights2(opts);
|
|
5751
|
+
});
|
|
5752
|
+
program.command("edit-weights").description("override, reset, or explain the scoring weights (layered over defaults)").option("--set <pair>", "override key=value, e.g. composite.lambda=0.5 (repeatable)", (v, acc) => acc.concat(v), []).option("--reset", "remove all overrides and restore defaults").option("--explain <component>", "explain a component (e.g. adoption, quality, lambda)").option("--project", "write to project-local .lurq/weights.json instead of the user config").action(async (opts) => {
|
|
5753
|
+
const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5754
|
+
await runEditWeights2(opts);
|
|
5755
|
+
});
|
|
5756
|
+
program.command("discover").description("operator-side: proactively crawl for new packages and queue/gate them (\xA72B)").option("--cap <n>", "max candidates to fully ingest this run", (v) => parseInt(v, 10)).option("--dry-run", "discover, queue, and gate, but do not ingest survivors").option("--json", "output the discovery summary as JSON").action(async (opts) => {
|
|
5757
|
+
const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
5758
|
+
requireConfig2(["DATABASE_URL"]);
|
|
5759
|
+
const { runDiscovery: runDiscovery2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
5760
|
+
const summary = await runDiscovery2({ perRunCap: opts.cap, dryRun: opts.dryRun });
|
|
5761
|
+
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
5762
|
+
});
|
|
5763
|
+
program.command("rescore").description("re-derive health scores from cached breakdowns using current weights (no re-ingest)").option("--json", "output the rescore summary as JSON").action(async (opts) => {
|
|
5764
|
+
const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
5765
|
+
requireConfig2(["DATABASE_URL"]);
|
|
5766
|
+
const { runRescore: runRescore2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
5767
|
+
const summary = await runRescore2();
|
|
5768
|
+
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
5769
|
+
});
|
|
5770
|
+
program.command("watch").description(
|
|
5771
|
+
"operator-side: follow the npm changes feed, re-syncing tracked packages on new releases"
|
|
5772
|
+
).action(async () => {
|
|
5773
|
+
const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5774
|
+
await runWatch2();
|
|
5775
|
+
});
|
|
5776
|
+
program.command("sandbox").argument("<package>", "npm package name").argument("[version]", "specific version (default: latest)").description(
|
|
5777
|
+
"operator-side: install + smoke-load a package in a sandbox to verify it actually works"
|
|
5778
|
+
).option("--esm", "load via ESM import instead of CJS require").option("--allow-scripts", "run install scripts (UNSAFE without VM isolation)").option("--json", "output JSON").action(
|
|
5779
|
+
async (pkg, version, opts) => {
|
|
5780
|
+
const { runSandbox: runSandbox2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5781
|
+
await runSandbox2(pkg, version, opts);
|
|
5782
|
+
}
|
|
5783
|
+
);
|
|
5784
|
+
program.command("compat").argument("<packages...>", "npm package names to check together").description("check pairwise compatibility of packages from the sandbox matrix").option("--run", "operator-side: co-install them in the sandbox first (UNSAFE without VM isolation)").option("--json", "output JSON").action(async (pkgs, opts) => {
|
|
5785
|
+
const { runCompat: runCompat2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
5786
|
+
await runCompat2(pkgs, opts);
|
|
5787
|
+
});
|
|
5788
|
+
program.command("install").description("guided setup: connect lurq to your AI assistant(s)").option("--api-key <key>", "hosted API key (skips the prompt)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--agent <agent>", "claude-code | cursor | copilot | windsurf | codex | all").option("--yes", "non-interactive: use flags/env and detected agents without prompting").action(async (opts) => {
|
|
5789
|
+
const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
5790
|
+
await runInstallWizard2(opts);
|
|
5791
|
+
});
|
|
5792
|
+
program.command("install-skill").description("register lurq as an MCP server in supported AI assistants (scriptable)").option(
|
|
2817
5793
|
"--agent <agent>",
|
|
2818
5794
|
"claude-code | cursor | copilot | windsurf | codex | all",
|
|
2819
5795
|
"claude-code"
|
|
2820
|
-
).action(async (opts) => {
|
|
5796
|
+
).option("--api-key <key>", "hosted API key (remote install; default mode)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--local", "self-host: write a local stdio entry using your own DATABASE_URL").action(async (opts) => {
|
|
2821
5797
|
const { runInstallSkill: runInstallSkill2 } = await Promise.resolve().then(() => (init_installSkill(), installSkill_exports));
|
|
2822
5798
|
await runInstallSkill2(opts);
|
|
2823
5799
|
});
|
|
5800
|
+
const keys = program.command("keys").description("manage API keys for the hosted service (operator; needs DATABASE_URL)");
|
|
5801
|
+
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) => {
|
|
5802
|
+
const { runKeysCreate: runKeysCreate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
5803
|
+
await runKeysCreate2(opts);
|
|
5804
|
+
});
|
|
5805
|
+
keys.command("list").description("list issued API keys (hashes are never shown)").option("--json", "output as JSON").action(async (opts) => {
|
|
5806
|
+
const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
5807
|
+
await runKeysList2(opts);
|
|
5808
|
+
});
|
|
5809
|
+
keys.command("rotate").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id to replace").description("issue a replacement key (same label/tier) and revoke the old one").option("--json", "print the new key as JSON and skip the interactive erase (for scripts)").action(async (prefixOrId, opts) => {
|
|
5810
|
+
const { runKeysRotate: runKeysRotate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
5811
|
+
await runKeysRotate2(prefixOrId, opts);
|
|
5812
|
+
});
|
|
5813
|
+
keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
|
|
5814
|
+
const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
5815
|
+
await runKeysRevoke2(prefixOrId);
|
|
5816
|
+
});
|
|
2824
5817
|
const db = program.command("db").description("database management");
|
|
2825
5818
|
db.command("migrate").description("apply database migrations and load the seed list").action(async () => {
|
|
2826
5819
|
const { runMigrate: runMigrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
@@ -2840,9 +5833,11 @@ function buildProgram() {
|
|
|
2840
5833
|
return program;
|
|
2841
5834
|
}
|
|
2842
5835
|
export {
|
|
5836
|
+
API_KEY_PREFIX,
|
|
2843
5837
|
CACHE_TTL,
|
|
2844
5838
|
CATEGORIES,
|
|
2845
5839
|
ConfigError,
|
|
5840
|
+
DEFAULT_ENDPOINT,
|
|
2846
5841
|
EMBEDDING_DIM,
|
|
2847
5842
|
FRONTEND_CATEGORIES,
|
|
2848
5843
|
PACKAGE_NAME,
|