lurqrun 0.0.1 → 0.0.2
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 +31 -7
- package/dist/bin/lurq.js +1490 -270
- package/dist/bin/lurq.js.map +1 -1
- package/dist/index.d.ts +52 -5
- package/dist/index.js +1492 -270
- 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/meta/0001_snapshot.json +482 -0
- package/drizzle/meta/0002_snapshot.json +575 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +6 -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,13 +212,17 @@ var init_logger = __esm({
|
|
|
188
212
|
// src/db/schema.ts
|
|
189
213
|
var schema_exports = {};
|
|
190
214
|
__export(schema_exports, {
|
|
215
|
+
apiKeys: () => apiKeys,
|
|
216
|
+
discoveryQueue: () => discoveryQueue,
|
|
191
217
|
packages: () => packages,
|
|
192
218
|
seedPackages: () => seedPackages,
|
|
193
219
|
syncRuns: () => syncRuns
|
|
194
220
|
});
|
|
221
|
+
import { sql } from "drizzle-orm";
|
|
195
222
|
import {
|
|
196
223
|
bigint,
|
|
197
224
|
boolean,
|
|
225
|
+
customType,
|
|
198
226
|
index,
|
|
199
227
|
integer,
|
|
200
228
|
jsonb,
|
|
@@ -205,12 +233,17 @@ import {
|
|
|
205
233
|
timestamp,
|
|
206
234
|
vector
|
|
207
235
|
} from "drizzle-orm/pg-core";
|
|
208
|
-
var ts, packages, syncRuns, seedPackages;
|
|
236
|
+
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys;
|
|
209
237
|
var init_schema = __esm({
|
|
210
238
|
"src/db/schema.ts"() {
|
|
211
239
|
"use strict";
|
|
212
240
|
init_esm_shims();
|
|
213
241
|
init_constants();
|
|
242
|
+
tsvector = customType({
|
|
243
|
+
dataType() {
|
|
244
|
+
return "tsvector";
|
|
245
|
+
}
|
|
246
|
+
});
|
|
214
247
|
ts = (name) => timestamp(name, { withTimezone: true, mode: "date" });
|
|
215
248
|
packages = pgTable(
|
|
216
249
|
"packages",
|
|
@@ -220,6 +253,8 @@ var init_schema = __esm({
|
|
|
220
253
|
ecosystem: text("ecosystem").notNull().default("npm"),
|
|
221
254
|
// Classification + descriptive text
|
|
222
255
|
category: text("category").$type(),
|
|
256
|
+
/** Whether `category` was hand-curated or inferred at ingest (§2A). */
|
|
257
|
+
categorySource: text("category_source").$type(),
|
|
223
258
|
description: text("description"),
|
|
224
259
|
summary: text("summary"),
|
|
225
260
|
repoUrl: text("repo_url"),
|
|
@@ -245,10 +280,19 @@ var init_schema = __esm({
|
|
|
245
280
|
advisories: jsonb("advisories").$type(),
|
|
246
281
|
// Computed outputs
|
|
247
282
|
healthScore: integer("health_score"),
|
|
283
|
+
/** Intrinsic-quality axis (§1), adoption-independent. Blends with health at
|
|
284
|
+
* ranking time (composite); never folded into health_score itself. */
|
|
285
|
+
qualityScore: integer("quality_score"),
|
|
248
286
|
confidence: text("confidence").$type(),
|
|
249
287
|
scoreBreakdown: jsonb("score_breakdown").$type(),
|
|
250
288
|
usageGuide: jsonb("usage_guide").$type(),
|
|
251
289
|
embedding: vector("embedding", { dimensions: EMBEDDING_DIM }),
|
|
290
|
+
// Lexical search vector (§3): name weighted highest (A), then category (B),
|
|
291
|
+
// then summary/description (C). Generated + STORED so it stays in sync with
|
|
292
|
+
// the row automatically; indexed with GIN for fast `@@` matching.
|
|
293
|
+
searchVector: tsvector("search_vector").generatedAlwaysAs(
|
|
294
|
+
sql`setweight(to_tsvector('english', coalesce(name, '')), 'A') || setweight(to_tsvector('english', coalesce(category, '')), 'B') || setweight(to_tsvector('english', coalesce(summary, description, '')), 'C')`
|
|
295
|
+
),
|
|
252
296
|
// Freshness + bookkeeping
|
|
253
297
|
dataAsOf: timestamp("data_as_of", { withTimezone: true, mode: "date" }),
|
|
254
298
|
createdAt: ts("created_at").notNull().defaultNow(),
|
|
@@ -258,7 +302,9 @@ var init_schema = __esm({
|
|
|
258
302
|
index("packages_category_idx").on(table2.category),
|
|
259
303
|
index("packages_health_score_idx").on(table2.healthScore),
|
|
260
304
|
// pgvector HNSW index for cosine similarity search (§11).
|
|
261
|
-
index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops"))
|
|
305
|
+
index("packages_embedding_idx").using("hnsw", table2.embedding.op("vector_cosine_ops")),
|
|
306
|
+
// GIN index for lexical full-text search (§3).
|
|
307
|
+
index("packages_search_vector_idx").using("gin", table2.searchVector)
|
|
262
308
|
]
|
|
263
309
|
);
|
|
264
310
|
syncRuns = pgTable("sync_runs", {
|
|
@@ -275,6 +321,38 @@ var init_schema = __esm({
|
|
|
275
321
|
category: text("category").$type(),
|
|
276
322
|
addedAt: ts("added_at").notNull().defaultNow()
|
|
277
323
|
});
|
|
324
|
+
discoveryQueue = pgTable(
|
|
325
|
+
"discovery_queue",
|
|
326
|
+
{
|
|
327
|
+
id: serial("id").primaryKey(),
|
|
328
|
+
name: text("name").notNull().unique(),
|
|
329
|
+
discoveredVia: text("discovered_via").$type().notNull(),
|
|
330
|
+
/** Lightweight quality-only pre-score (§2B). Null until the gate runs. */
|
|
331
|
+
preScore: integer("pre_score"),
|
|
332
|
+
status: text("status").$type().notNull().default("pending"),
|
|
333
|
+
discoveredAt: ts("discovered_at").notNull().defaultNow()
|
|
334
|
+
},
|
|
335
|
+
(table2) => [index("discovery_queue_status_idx").on(table2.status)]
|
|
336
|
+
);
|
|
337
|
+
apiKeys = pgTable(
|
|
338
|
+
"api_keys",
|
|
339
|
+
{
|
|
340
|
+
id: serial("id").primaryKey(),
|
|
341
|
+
/** sha256 hex of the full key — the only form persisted. */
|
|
342
|
+
keyHash: text("key_hash").notNull().unique(),
|
|
343
|
+
/** Identifiable display prefix, e.g. `lurq_live_ab12cd` (safe to show/log). */
|
|
344
|
+
prefix: text("prefix").notNull(),
|
|
345
|
+
/** Free-text label (owner / org / purpose). */
|
|
346
|
+
label: text("label"),
|
|
347
|
+
/** Reserved for self-serve issuance: maps to a Clerk user id. */
|
|
348
|
+
ownerId: text("owner_id"),
|
|
349
|
+
tier: text("tier").notNull().default("free"),
|
|
350
|
+
createdAt: ts("created_at").notNull().defaultNow(),
|
|
351
|
+
lastUsedAt: ts("last_used_at"),
|
|
352
|
+
revokedAt: ts("revoked_at")
|
|
353
|
+
},
|
|
354
|
+
(table2) => [index("api_keys_owner_idx").on(table2.ownerId)]
|
|
355
|
+
);
|
|
278
356
|
}
|
|
279
357
|
});
|
|
280
358
|
|
|
@@ -283,10 +361,10 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|
|
283
361
|
import postgres from "postgres";
|
|
284
362
|
function createDb(opts = {}) {
|
|
285
363
|
const { DATABASE_URL } = requireConfig(["DATABASE_URL"]);
|
|
286
|
-
const
|
|
364
|
+
const sql8 = postgres(DATABASE_URL, { max: opts.max ?? 10, onnotice: () => {
|
|
287
365
|
} });
|
|
288
|
-
const db = drizzle(
|
|
289
|
-
return { db, sql:
|
|
366
|
+
const db = drizzle(sql8, { schema: schema_exports });
|
|
367
|
+
return { db, sql: sql8, close: () => sql8.end() };
|
|
290
368
|
}
|
|
291
369
|
var init_client = __esm({
|
|
292
370
|
"src/db/client.ts"() {
|
|
@@ -530,8 +608,9 @@ function parseNpmRegistry(json2) {
|
|
|
530
608
|
const { repoUrl, repo } = normalizeRepoUrl(repository, homepage);
|
|
531
609
|
const deprecated = Boolean(latestManifest?.deprecated ?? json2?.deprecated);
|
|
532
610
|
const maintainers = Array.isArray(json2?.maintainers) ? json2.maintainers.length : null;
|
|
611
|
+
const name = json2?.name ?? "";
|
|
533
612
|
return {
|
|
534
|
-
name
|
|
613
|
+
name,
|
|
535
614
|
description: json2?.description ?? latestManifest?.description ?? null,
|
|
536
615
|
latestVersion: latest,
|
|
537
616
|
license: pickLicense(json2?.license ?? latestManifest?.license),
|
|
@@ -542,9 +621,40 @@ function parseNpmRegistry(json2) {
|
|
|
542
621
|
lastReleaseAt: toDate(latest ? time?.[latest] : null) ?? toDate(time?.modified),
|
|
543
622
|
deprecated,
|
|
544
623
|
maintainersCount: maintainers,
|
|
545
|
-
readme: typeof json2?.readme === "string" ? json2.readme : null
|
|
624
|
+
readme: typeof json2?.readme === "string" ? json2.readme : null,
|
|
625
|
+
keywords: parseKeywords(json2?.keywords ?? latestManifest?.keywords),
|
|
626
|
+
hasTypes: detectTypes(name, latestManifest),
|
|
627
|
+
hasTestScript: detectTestScript(latestManifest),
|
|
628
|
+
directDependenciesCount: countDeps(latestManifest?.dependencies),
|
|
629
|
+
hasProvenance: detectProvenance(latestManifest)
|
|
546
630
|
};
|
|
547
631
|
}
|
|
632
|
+
function parseKeywords(value) {
|
|
633
|
+
if (!Array.isArray(value)) return [];
|
|
634
|
+
return value.filter((k) => typeof k === "string");
|
|
635
|
+
}
|
|
636
|
+
function detectTypes(name, manifest) {
|
|
637
|
+
if (typeof manifest?.types === "string" || typeof manifest?.typings === "string") return true;
|
|
638
|
+
if (name.startsWith("@types/")) return true;
|
|
639
|
+
const exportsField = manifest?.exports;
|
|
640
|
+
if (exportsField && typeof exportsField === "object") {
|
|
641
|
+
const serialized = JSON.stringify(exportsField);
|
|
642
|
+
if (serialized.includes('"types"')) return true;
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
function detectTestScript(manifest) {
|
|
647
|
+
const test = manifest?.scripts?.test;
|
|
648
|
+
if (typeof test !== "string" || test.trim() === "") return false;
|
|
649
|
+
return !/no test specified/i.test(test);
|
|
650
|
+
}
|
|
651
|
+
function countDeps(deps) {
|
|
652
|
+
if (!deps || typeof deps !== "object") return 0;
|
|
653
|
+
return Object.keys(deps).length;
|
|
654
|
+
}
|
|
655
|
+
function detectProvenance(manifest) {
|
|
656
|
+
return Boolean(manifest?.dist?.attestations);
|
|
657
|
+
}
|
|
548
658
|
async function fetchNpmRegistry(name, fetchImpl) {
|
|
549
659
|
const url = `https://${HOST}/${encodeNpmName(name)}`;
|
|
550
660
|
const { data } = await httpGetJson(url, {
|
|
@@ -582,6 +692,32 @@ var init_npmRegistry = __esm({
|
|
|
582
692
|
}
|
|
583
693
|
});
|
|
584
694
|
|
|
695
|
+
// src/ingestion/sources/npmSearch.ts
|
|
696
|
+
async function searchNpm(query, size = 20, fetchImpl) {
|
|
697
|
+
const url = `https://${HOST2}/-/v1/search?text=${encodeURIComponent(query)}&size=${size}`;
|
|
698
|
+
try {
|
|
699
|
+
const { data } = await httpGetJson(url, {
|
|
700
|
+
host: HOST2,
|
|
701
|
+
ttlMs: CACHE_TTL.npmRegistry,
|
|
702
|
+
fetchImpl
|
|
703
|
+
});
|
|
704
|
+
const objects = Array.isArray(data?.objects) ? data.objects : [];
|
|
705
|
+
return objects.map((o) => ({ name: o?.package?.name, date: o?.package?.date ?? null })).filter((h) => typeof h.name === "string");
|
|
706
|
+
} catch {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
var HOST2;
|
|
711
|
+
var init_npmSearch = __esm({
|
|
712
|
+
"src/ingestion/sources/npmSearch.ts"() {
|
|
713
|
+
"use strict";
|
|
714
|
+
init_esm_shims();
|
|
715
|
+
init_constants();
|
|
716
|
+
init_http();
|
|
717
|
+
HOST2 = "registry.npmjs.org";
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
585
721
|
// src/ingestion/sources/npmDownloads.ts
|
|
586
722
|
function parseWeeklyDownloads(json2) {
|
|
587
723
|
const value = json2?.downloads;
|
|
@@ -627,8 +763,8 @@ function chunk(items, size) {
|
|
|
627
763
|
async function fetchWeeklyDownloads(name, fetchImpl) {
|
|
628
764
|
try {
|
|
629
765
|
const { data } = await httpGetJson(
|
|
630
|
-
`https://${
|
|
631
|
-
{ host:
|
|
766
|
+
`https://${HOST3}/downloads/point/last-week/${name}`,
|
|
767
|
+
{ host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
|
|
632
768
|
);
|
|
633
769
|
return parseWeeklyDownloads(data);
|
|
634
770
|
} catch (err) {
|
|
@@ -639,8 +775,8 @@ async function fetchWeeklyDownloads(name, fetchImpl) {
|
|
|
639
775
|
async function fetchDownloadGrowth(name, fetchImpl) {
|
|
640
776
|
try {
|
|
641
777
|
const { data } = await httpGetJson(
|
|
642
|
-
`https://${
|
|
643
|
-
{ host:
|
|
778
|
+
`https://${HOST3}/downloads/range/${last90DayRange()}/${name}`,
|
|
779
|
+
{ host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 3, fetchImpl }
|
|
644
780
|
);
|
|
645
781
|
return parseDownloadGrowth(data);
|
|
646
782
|
} catch {
|
|
@@ -654,8 +790,8 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
|
|
|
654
790
|
for (const batch of chunk(unscoped, BULK_CHUNK)) {
|
|
655
791
|
try {
|
|
656
792
|
const { data } = await httpGetJson(
|
|
657
|
-
`https://${
|
|
658
|
-
{ host:
|
|
793
|
+
`https://${HOST3}/downloads/point/last-week/${batch.join(",")}`,
|
|
794
|
+
{ host: HOST3, ttlMs: CACHE_TTL.npmDownloads, retries: 5, fetchImpl }
|
|
659
795
|
);
|
|
660
796
|
for (const [k, v] of parseBulkWeekly(data, batch)) result.set(k, v);
|
|
661
797
|
} catch {
|
|
@@ -669,14 +805,14 @@ async function fetchBulkWeeklyDownloads(names, fetchImpl) {
|
|
|
669
805
|
}
|
|
670
806
|
return result;
|
|
671
807
|
}
|
|
672
|
-
var
|
|
808
|
+
var HOST3, BULK_CHUNK;
|
|
673
809
|
var init_npmDownloads = __esm({
|
|
674
810
|
"src/ingestion/sources/npmDownloads.ts"() {
|
|
675
811
|
"use strict";
|
|
676
812
|
init_esm_shims();
|
|
677
813
|
init_constants();
|
|
678
814
|
init_http();
|
|
679
|
-
|
|
815
|
+
HOST3 = "api.npmjs.org";
|
|
680
816
|
BULK_CHUNK = 128;
|
|
681
817
|
}
|
|
682
818
|
});
|
|
@@ -700,7 +836,7 @@ function parseGithub(json2, now = /* @__PURE__ */ new Date()) {
|
|
|
700
836
|
async function fetchGithubRepo(owner, repo, token, fetchImpl) {
|
|
701
837
|
const body = JSON.stringify({ query: QUERY, variables: { owner, name: repo } });
|
|
702
838
|
const { data } = await httpRequest(ENDPOINT, {
|
|
703
|
-
host:
|
|
839
|
+
host: HOST4,
|
|
704
840
|
ttlMs: CACHE_TTL.github,
|
|
705
841
|
method: "POST",
|
|
706
842
|
headers: {
|
|
@@ -715,15 +851,15 @@ async function fetchGithubRepo(owner, repo, token, fetchImpl) {
|
|
|
715
851
|
});
|
|
716
852
|
return parseGithub(data);
|
|
717
853
|
}
|
|
718
|
-
var
|
|
854
|
+
var HOST4, ENDPOINT, QUERY, ONE_YEAR_MS;
|
|
719
855
|
var init_github = __esm({
|
|
720
856
|
"src/ingestion/sources/github.ts"() {
|
|
721
857
|
"use strict";
|
|
722
858
|
init_esm_shims();
|
|
723
859
|
init_constants();
|
|
724
860
|
init_http();
|
|
725
|
-
|
|
726
|
-
ENDPOINT = `https://${
|
|
861
|
+
HOST4 = "api.github.com";
|
|
862
|
+
ENDPOINT = `https://${HOST4}/graphql`;
|
|
727
863
|
QUERY = `query($owner:String!, $name:String!) {
|
|
728
864
|
repository(owner:$owner, name:$name) {
|
|
729
865
|
stargazerCount
|
|
@@ -769,10 +905,10 @@ function parseAdvisoryDetail(json2) {
|
|
|
769
905
|
}
|
|
770
906
|
async function fetchScorecard(owner, repo, fetchImpl) {
|
|
771
907
|
const projectKey = encodeURIComponent(`github.com/${owner}/${repo}`);
|
|
772
|
-
const url = `https://${
|
|
908
|
+
const url = `https://${HOST5}/v3/projects/${projectKey}`;
|
|
773
909
|
try {
|
|
774
910
|
const { data } = await httpGetJson(url, {
|
|
775
|
-
host:
|
|
911
|
+
host: HOST5,
|
|
776
912
|
ttlMs: CACHE_TTL.depsDev,
|
|
777
913
|
fetchImpl
|
|
778
914
|
});
|
|
@@ -782,11 +918,11 @@ async function fetchScorecard(owner, repo, fetchImpl) {
|
|
|
782
918
|
}
|
|
783
919
|
}
|
|
784
920
|
async function fetchAdvisories(name, version, fetchImpl) {
|
|
785
|
-
const versionUrl = `https://${
|
|
921
|
+
const versionUrl = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}`;
|
|
786
922
|
let keys = [];
|
|
787
923
|
try {
|
|
788
924
|
const { data } = await httpGetJson(versionUrl, {
|
|
789
|
-
host:
|
|
925
|
+
host: HOST5,
|
|
790
926
|
ttlMs: CACHE_TTL.depsDev,
|
|
791
927
|
fetchImpl
|
|
792
928
|
});
|
|
@@ -796,8 +932,8 @@ async function fetchAdvisories(name, version, fetchImpl) {
|
|
|
796
932
|
}
|
|
797
933
|
const details = await Promise.allSettled(
|
|
798
934
|
keys.map(
|
|
799
|
-
(key) => httpGetJson(`https://${
|
|
800
|
-
host:
|
|
935
|
+
(key) => httpGetJson(`https://${HOST5}/v3/advisories/${encodeURIComponent(key)}`, {
|
|
936
|
+
host: HOST5,
|
|
801
937
|
ttlMs: CACHE_TTL.depsDev,
|
|
802
938
|
fetchImpl
|
|
803
939
|
})
|
|
@@ -807,6 +943,21 @@ async function fetchAdvisories(name, version, fetchImpl) {
|
|
|
807
943
|
(d) => d.status === "fulfilled"
|
|
808
944
|
).map((d) => parseAdvisoryDetail(d.value.data));
|
|
809
945
|
}
|
|
946
|
+
async function fetchDependencyNames(name, version, fetchImpl) {
|
|
947
|
+
const url = `https://${HOST5}/v3/systems/npm/packages/${encodeName(name)}/versions/${encodeURIComponent(version)}:dependencies`;
|
|
948
|
+
try {
|
|
949
|
+
const { data } = await httpGetJson(url, {
|
|
950
|
+
host: HOST5,
|
|
951
|
+
ttlMs: CACHE_TTL.depsDev,
|
|
952
|
+
fetchImpl
|
|
953
|
+
});
|
|
954
|
+
const nodes = Array.isArray(data?.nodes) ? data.nodes : [];
|
|
955
|
+
const names = nodes.map((n) => n?.versionKey?.name).filter((n) => typeof n === "string" && n !== name);
|
|
956
|
+
return [...new Set(names)];
|
|
957
|
+
} catch {
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
810
961
|
async function fetchDepsDev(name, version, repo, fetchImpl) {
|
|
811
962
|
const [scorecard, advisories] = await Promise.all([
|
|
812
963
|
repo ? fetchScorecard(repo.owner, repo.repo, fetchImpl) : Promise.resolve(null),
|
|
@@ -814,14 +965,14 @@ async function fetchDepsDev(name, version, repo, fetchImpl) {
|
|
|
814
965
|
]);
|
|
815
966
|
return { scorecard, advisories };
|
|
816
967
|
}
|
|
817
|
-
var
|
|
968
|
+
var HOST5, MAX_ADVISORIES;
|
|
818
969
|
var init_depsDev = __esm({
|
|
819
970
|
"src/ingestion/sources/depsDev.ts"() {
|
|
820
971
|
"use strict";
|
|
821
972
|
init_esm_shims();
|
|
822
973
|
init_constants();
|
|
823
974
|
init_http();
|
|
824
|
-
|
|
975
|
+
HOST5 = "api.deps.dev";
|
|
825
976
|
MAX_ADVISORIES = 10;
|
|
826
977
|
}
|
|
827
978
|
});
|
|
@@ -834,10 +985,10 @@ function parseBundleSize(json2) {
|
|
|
834
985
|
}
|
|
835
986
|
async function fetchBundlephobia(name, category, fetchImpl) {
|
|
836
987
|
if (!isFrontendCategory(category)) return { bundleMinGzipKb: null };
|
|
837
|
-
const url = `https://${
|
|
988
|
+
const url = `https://${HOST6}/api/size?package=${encodeURIComponent(name)}`;
|
|
838
989
|
try {
|
|
839
990
|
const { data } = await httpGetJson(url, {
|
|
840
|
-
host:
|
|
991
|
+
host: HOST6,
|
|
841
992
|
ttlMs: CACHE_TTL.bundlephobia,
|
|
842
993
|
timeoutMs: 12e3,
|
|
843
994
|
retries: 1,
|
|
@@ -848,7 +999,7 @@ async function fetchBundlephobia(name, category, fetchImpl) {
|
|
|
848
999
|
return { bundleMinGzipKb: null };
|
|
849
1000
|
}
|
|
850
1001
|
}
|
|
851
|
-
var
|
|
1002
|
+
var HOST6;
|
|
852
1003
|
var init_bundlephobia = __esm({
|
|
853
1004
|
"src/ingestion/sources/bundlephobia.ts"() {
|
|
854
1005
|
"use strict";
|
|
@@ -856,7 +1007,7 @@ var init_bundlephobia = __esm({
|
|
|
856
1007
|
init_constants();
|
|
857
1008
|
init_http();
|
|
858
1009
|
init_types();
|
|
859
|
-
|
|
1010
|
+
HOST6 = "bundlephobia.com";
|
|
860
1011
|
}
|
|
861
1012
|
});
|
|
862
1013
|
|
|
@@ -866,6 +1017,7 @@ var init_sources = __esm({
|
|
|
866
1017
|
"use strict";
|
|
867
1018
|
init_esm_shims();
|
|
868
1019
|
init_npmRegistry();
|
|
1020
|
+
init_npmSearch();
|
|
869
1021
|
init_npmDownloads();
|
|
870
1022
|
init_github();
|
|
871
1023
|
init_depsDev();
|
|
@@ -876,10 +1028,10 @@ var init_sources = __esm({
|
|
|
876
1028
|
// src/ingestion/sources/githubReadme.ts
|
|
877
1029
|
async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
878
1030
|
for (const file of CANDIDATES) {
|
|
879
|
-
const url = `https://${
|
|
1031
|
+
const url = `https://${HOST7}/${owner}/${repo}/HEAD/${file}`;
|
|
880
1032
|
try {
|
|
881
1033
|
const { data } = await httpRequest(url, {
|
|
882
|
-
host:
|
|
1034
|
+
host: HOST7,
|
|
883
1035
|
ttlMs: CACHE_TTL.github,
|
|
884
1036
|
accept: "text",
|
|
885
1037
|
retries: 0,
|
|
@@ -891,14 +1043,14 @@ async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
|
891
1043
|
}
|
|
892
1044
|
return null;
|
|
893
1045
|
}
|
|
894
|
-
var
|
|
1046
|
+
var HOST7, CANDIDATES;
|
|
895
1047
|
var init_githubReadme = __esm({
|
|
896
1048
|
"src/ingestion/sources/githubReadme.ts"() {
|
|
897
1049
|
"use strict";
|
|
898
1050
|
init_esm_shims();
|
|
899
1051
|
init_constants();
|
|
900
1052
|
init_http();
|
|
901
|
-
|
|
1053
|
+
HOST7 = "raw.githubusercontent.com";
|
|
902
1054
|
CANDIDATES = ["README.md", "readme.md", "README.markdown", "README"];
|
|
903
1055
|
}
|
|
904
1056
|
});
|
|
@@ -915,6 +1067,7 @@ function truncateSentences(text2, max = 3) {
|
|
|
915
1067
|
}
|
|
916
1068
|
function buildUserPrompt(input) {
|
|
917
1069
|
const readme = (input.readme ?? "").slice(0, 4e3);
|
|
1070
|
+
const needsCategory = input.category == null;
|
|
918
1071
|
return [
|
|
919
1072
|
`Package: ${input.name}`,
|
|
920
1073
|
`Category: ${input.category ?? "unknown"}`,
|
|
@@ -928,7 +1081,8 @@ ${readme || "(none)"}`,
|
|
|
928
1081
|
'- "whenToUse": one sentence on the ideal use case.',
|
|
929
1082
|
'- "whenNotToUse": one sentence on when to pick something else (or "").',
|
|
930
1083
|
'- "whereItFits": one sentence on its architectural role.',
|
|
931
|
-
'- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".'
|
|
1084
|
+
'- "howToWireIn": one short sentence on the minimal setup (install + entry point), or "".',
|
|
1085
|
+
...needsCategory ? [`- "category": the single best fit from this list: ${CATEGORIES.join(", ")}.`] : []
|
|
932
1086
|
].join("\n");
|
|
933
1087
|
}
|
|
934
1088
|
function str(value) {
|
|
@@ -962,6 +1116,7 @@ var init_summarize = __esm({
|
|
|
962
1116
|
"src/ingestion/summarize.ts"() {
|
|
963
1117
|
"use strict";
|
|
964
1118
|
init_esm_shims();
|
|
1119
|
+
init_types();
|
|
965
1120
|
init_config();
|
|
966
1121
|
init_http();
|
|
967
1122
|
init_logger();
|
|
@@ -1003,7 +1158,7 @@ var init_summarize = __esm({
|
|
|
1003
1158
|
whereItFits: `Sits at ${role}.`,
|
|
1004
1159
|
context7Hint: context7Hint(input.name)
|
|
1005
1160
|
};
|
|
1006
|
-
return { summary, usageGuide: guide };
|
|
1161
|
+
return { summary, usageGuide: guide, inferredCategory: null };
|
|
1007
1162
|
}
|
|
1008
1163
|
};
|
|
1009
1164
|
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 +1206,13 @@ var init_summarize = __esm({
|
|
|
1051
1206
|
howToWireIn: str(parsed.howToWireIn) || void 0,
|
|
1052
1207
|
context7Hint: context7Hint(input.name)
|
|
1053
1208
|
};
|
|
1054
|
-
|
|
1209
|
+
const rawCategory = str(parsed.category);
|
|
1210
|
+
const inferredCategory = rawCategory && isCategory(rawCategory) ? rawCategory : null;
|
|
1211
|
+
return {
|
|
1212
|
+
summary: str(parsed.summary) ? truncateSentences(parsed.summary, 3) : null,
|
|
1213
|
+
usageGuide: guide,
|
|
1214
|
+
inferredCategory
|
|
1215
|
+
};
|
|
1055
1216
|
} catch (err) {
|
|
1056
1217
|
logger.warn(`summary LLM failed for ${input.name}, using fallback: ${err.message}`);
|
|
1057
1218
|
return new FallbackSummaryProvider().generate(input);
|
|
@@ -1103,18 +1264,227 @@ var init_collect = __esm({
|
|
|
1103
1264
|
}
|
|
1104
1265
|
});
|
|
1105
1266
|
|
|
1267
|
+
// src/search/categoryInference.ts
|
|
1268
|
+
function inferCategory(need) {
|
|
1269
|
+
const text2 = need.toLowerCase();
|
|
1270
|
+
for (const rule of RULES) {
|
|
1271
|
+
if (rule.patterns.test(text2)) return rule.category;
|
|
1272
|
+
}
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
function inferCategoryFromSignals(signals) {
|
|
1276
|
+
const r = signals.registry;
|
|
1277
|
+
const keywords = (r?.keywords ?? []).join(" ");
|
|
1278
|
+
const text2 = [
|
|
1279
|
+
signals.name,
|
|
1280
|
+
keywords,
|
|
1281
|
+
keywords,
|
|
1282
|
+
// weight keywords higher via repetition
|
|
1283
|
+
r?.description ?? "",
|
|
1284
|
+
(r?.readme ?? "").slice(0, 600)
|
|
1285
|
+
].filter(Boolean).join(" ");
|
|
1286
|
+
return inferCategory(text2);
|
|
1287
|
+
}
|
|
1288
|
+
var RULES;
|
|
1289
|
+
var init_categoryInference = __esm({
|
|
1290
|
+
"src/search/categoryInference.ts"() {
|
|
1291
|
+
"use strict";
|
|
1292
|
+
init_esm_shims();
|
|
1293
|
+
RULES = [
|
|
1294
|
+
{ category: "meta-framework", patterns: /\b(meta-?framework|next\.?js|nuxt|remix|astro|gatsby|sveltekit|full-?stack framework)\b/ },
|
|
1295
|
+
{ category: "state-management", patterns: /\b(state management|global state|store|redux|zustand|jotai|mobx|atoms?)\b/ },
|
|
1296
|
+
{ category: "routing", patterns: /\b(rout(e|er|ing)|navigation|url matching)\b/ },
|
|
1297
|
+
{ category: "orm", patterns: /\b(orm|object-?relational|query builder|prisma|drizzle|sequelize|typeorm)\b/ },
|
|
1298
|
+
{ category: "database-client", patterns: /\b(database (driver|client)|postgres|mysql|mongo(db)?|redis|sqlite|db driver)\b/ },
|
|
1299
|
+
{ category: "ui-component-library", patterns: /\b(component library|ui kit|ui components?|design system|buttons?|modal|dialog)\b/ },
|
|
1300
|
+
{ category: "styling", patterns: /\b(styl(e|ing)|css|tailwind|sass|scss|class ?names?|theme)\b/ },
|
|
1301
|
+
{ category: "forms", patterns: /\b(forms?|form (state|library|handling)|input handling)\b/ },
|
|
1302
|
+
{ category: "validation", patterns: /\b(validat(e|ion|or)|schema|parse input|type ?safe parsing)\b/ },
|
|
1303
|
+
{ category: "data-fetching", patterns: /\b(data fetching|server state|react query|swr|graphql client|caching queries)\b/ },
|
|
1304
|
+
{ category: "http-client", patterns: /\b(http client|fetch wrapper|rest client|make (a )?request|ajax|api calls?)\b/ },
|
|
1305
|
+
{ category: "auth", patterns: /\b(auth(entication|orization)?|login|session|jwt|oauth|password hashing)\b/ },
|
|
1306
|
+
{ category: "testing", patterns: /\b(test(ing|s)?|unit test|e2e|assertion|mocking|test runner)\b/ },
|
|
1307
|
+
{ category: "bundler", patterns: /\b(bundler|bundle (modules?|code)|webpack|rollup|esbuild|parcel)\b/ },
|
|
1308
|
+
{ category: "build-tool", patterns: /\b(build tool|dev server|monorepo|task runner|compile|transpile)\b/ },
|
|
1309
|
+
// date-time precedes linting: "date formatting" must not be captured by linting's
|
|
1310
|
+
// bare "formatting" token before the date rule is reached (first-match-wins).
|
|
1311
|
+
{ category: "date-time", patterns: /\b(dates?|date ?time|time(zone)?s?|calendar|parse dates?|format dates?)\b/ },
|
|
1312
|
+
{ category: "linting", patterns: /\b(lint(er|ing)?|formatter|formatting|code (quality|style|format)|prettier|eslint)\b/ },
|
|
1313
|
+
{ category: "animation", patterns: /\b(animat(e|ion)|motion|transition|spring|gsap|3d)\b/ },
|
|
1314
|
+
{ category: "charts", patterns: /\b(charts?|graphs?|data ?vis(ualization)?|plots?|dashboards?)\b/ },
|
|
1315
|
+
{ category: "i18n", patterns: /\b(i18n|internationali[sz]ation|localization|translat(e|ion)|locale)\b/ },
|
|
1316
|
+
{ category: "framework", patterns: /\b(framework|react|vue|svelte|angular|web server|backend framework)\b/ },
|
|
1317
|
+
{ category: "utility", patterns: /\b(debounce|throttle|deep ?clone|slugify|uuid|util(ity|ities)?|helper|lodash|retry)\b/ }
|
|
1318
|
+
];
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
// src/core/paths.ts
|
|
1323
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1324
|
+
import { homedir as homedir2 } from "os";
|
|
1325
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1326
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1327
|
+
function packageRoot() {
|
|
1328
|
+
if (cachedRoot) return cachedRoot;
|
|
1329
|
+
let dir = dirname2(fileURLToPath2(import.meta.url));
|
|
1330
|
+
for (; ; ) {
|
|
1331
|
+
if (existsSync2(join2(dir, "package.json"))) {
|
|
1332
|
+
cachedRoot = dir;
|
|
1333
|
+
return dir;
|
|
1334
|
+
}
|
|
1335
|
+
const parent = dirname2(dir);
|
|
1336
|
+
if (parent === dir) break;
|
|
1337
|
+
dir = parent;
|
|
1338
|
+
}
|
|
1339
|
+
cachedRoot = process.cwd();
|
|
1340
|
+
return cachedRoot;
|
|
1341
|
+
}
|
|
1342
|
+
function migrationsDir() {
|
|
1343
|
+
return join2(packageRoot(), "drizzle");
|
|
1344
|
+
}
|
|
1345
|
+
function seedJsonPath() {
|
|
1346
|
+
return join2(packageRoot(), "src", "data", "seed.json");
|
|
1347
|
+
}
|
|
1348
|
+
function userWeightsPath() {
|
|
1349
|
+
const base = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
|
|
1350
|
+
return join2(base, "lurq", "weights.json");
|
|
1351
|
+
}
|
|
1352
|
+
function projectWeightsPath() {
|
|
1353
|
+
return join2(process.cwd(), ".lurq", "weights.json");
|
|
1354
|
+
}
|
|
1355
|
+
var cachedRoot;
|
|
1356
|
+
var init_paths = __esm({
|
|
1357
|
+
"src/core/paths.ts"() {
|
|
1358
|
+
"use strict";
|
|
1359
|
+
init_esm_shims();
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1106
1363
|
// src/scoring/weights.ts
|
|
1107
|
-
|
|
1364
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
1365
|
+
import { dirname as dirname3 } from "path";
|
|
1366
|
+
function activeWeightsPath() {
|
|
1367
|
+
if (existsSync3(projectWeightsPath())) return { path: projectWeightsPath(), source: "project-config" };
|
|
1368
|
+
if (existsSync3(userWeightsPath())) return { path: userWeightsPath(), source: "user-config" };
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
function loadWeights() {
|
|
1372
|
+
if (cachedWeights) return cachedWeights;
|
|
1373
|
+
let merged = structuredClone(DEFAULT_WEIGHTS);
|
|
1374
|
+
const active = activeWeightsPath();
|
|
1375
|
+
if (active) {
|
|
1376
|
+
try {
|
|
1377
|
+
const fromFile = JSON.parse(readFileSync(active.path, "utf8"));
|
|
1378
|
+
merged = mergeWeights(merged, fromFile);
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const envLambda = process.env.LURQ_COMPOSITE_LAMBDA;
|
|
1383
|
+
if (envLambda !== void 0 && envLambda !== "") {
|
|
1384
|
+
const n = Number(envLambda);
|
|
1385
|
+
if (Number.isFinite(n)) merged.composite.lambda = n;
|
|
1386
|
+
}
|
|
1387
|
+
cachedWeights = validateWeights(merged).weights;
|
|
1388
|
+
return cachedWeights;
|
|
1389
|
+
}
|
|
1390
|
+
function resetWeightsCache() {
|
|
1391
|
+
cachedWeights = void 0;
|
|
1392
|
+
}
|
|
1393
|
+
function mergeWeights(base, over) {
|
|
1394
|
+
return {
|
|
1395
|
+
health: { ...base.health, ...over.health ?? {} },
|
|
1396
|
+
composite: { ...base.composite, ...over.composite ?? {} }
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function validateWeights(w) {
|
|
1400
|
+
const lambda = Math.max(0, Math.min(1, w.composite.lambda));
|
|
1401
|
+
const h = w.health;
|
|
1402
|
+
const sum = h.maintenance + h.adoption + h.reliability + h.efficiency;
|
|
1403
|
+
let normalized = false;
|
|
1404
|
+
let health = h;
|
|
1405
|
+
if (sum <= 0) {
|
|
1406
|
+
health = { ...DEFAULT_WEIGHTS.health };
|
|
1407
|
+
normalized = true;
|
|
1408
|
+
} else if (Math.abs(sum - 1) > 1e-6) {
|
|
1409
|
+
health = {
|
|
1410
|
+
maintenance: h.maintenance / sum,
|
|
1411
|
+
adoption: h.adoption / sum,
|
|
1412
|
+
reliability: h.reliability / sum,
|
|
1413
|
+
efficiency: h.efficiency / sum
|
|
1414
|
+
};
|
|
1415
|
+
normalized = true;
|
|
1416
|
+
}
|
|
1417
|
+
return { weights: { health, composite: { lambda } }, normalized };
|
|
1418
|
+
}
|
|
1419
|
+
function settableKeys() {
|
|
1420
|
+
return Object.keys(SETTABLE);
|
|
1421
|
+
}
|
|
1422
|
+
function applyOverrides(base, sets) {
|
|
1423
|
+
const next = structuredClone(base);
|
|
1424
|
+
for (const entry of sets) {
|
|
1425
|
+
const eq7 = entry.indexOf("=");
|
|
1426
|
+
if (eq7 < 0) throw new Error(`Invalid --set "${entry}" (expected key=value).`);
|
|
1427
|
+
const key = entry.slice(0, eq7).trim();
|
|
1428
|
+
const value = Number(entry.slice(eq7 + 1).trim());
|
|
1429
|
+
const apply = SETTABLE[key];
|
|
1430
|
+
if (!apply) {
|
|
1431
|
+
throw new Error(`Unknown weight key "${key}". Settable: ${settableKeys().join(", ")}.`);
|
|
1432
|
+
}
|
|
1433
|
+
if (!Number.isFinite(value)) throw new Error(`Value for "${key}" must be a number.`);
|
|
1434
|
+
apply(next, value);
|
|
1435
|
+
}
|
|
1436
|
+
return next;
|
|
1437
|
+
}
|
|
1438
|
+
function saveWeights(w, scope = "user") {
|
|
1439
|
+
const path2 = scope === "project" ? projectWeightsPath() : userWeightsPath();
|
|
1440
|
+
mkdirSync(dirname3(path2), { recursive: true });
|
|
1441
|
+
writeFileSync(path2, JSON.stringify(w, null, 2) + "\n", "utf8");
|
|
1442
|
+
resetWeightsCache();
|
|
1443
|
+
return path2;
|
|
1444
|
+
}
|
|
1445
|
+
function resetWeights() {
|
|
1446
|
+
const removed = [];
|
|
1447
|
+
for (const path2 of [projectWeightsPath(), userWeightsPath()]) {
|
|
1448
|
+
if (existsSync3(path2)) {
|
|
1449
|
+
rmSync(path2);
|
|
1450
|
+
removed.push(path2);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
resetWeightsCache();
|
|
1454
|
+
return removed;
|
|
1455
|
+
}
|
|
1456
|
+
var HEALTH_WEIGHTS, COMPOSITE, QUALITY_WEIGHTS, QUALITY, MAINTENANCE_WEIGHTS, MAINTENANCE, ADOPTION, RELIABILITY, EFFICIENCY, DISCOVERY, DEFAULT_WEIGHTS, cachedWeights, SETTABLE, WEIGHT_EXPLANATIONS, CONFIDENCE;
|
|
1108
1457
|
var init_weights = __esm({
|
|
1109
1458
|
"src/scoring/weights.ts"() {
|
|
1110
1459
|
"use strict";
|
|
1111
1460
|
init_esm_shims();
|
|
1461
|
+
init_paths();
|
|
1112
1462
|
HEALTH_WEIGHTS = {
|
|
1113
1463
|
maintenance: 0.35,
|
|
1114
1464
|
adoption: 0.3,
|
|
1115
1465
|
reliability: 0.25,
|
|
1116
1466
|
efficiency: 0.1
|
|
1117
1467
|
};
|
|
1468
|
+
COMPOSITE = {
|
|
1469
|
+
lambda: 0.35
|
|
1470
|
+
};
|
|
1471
|
+
QUALITY_WEIGHTS = {
|
|
1472
|
+
types: 0.25,
|
|
1473
|
+
tests: 0.2,
|
|
1474
|
+
docs: 0.2,
|
|
1475
|
+
changelog: 0.1,
|
|
1476
|
+
deps: 0.1,
|
|
1477
|
+
license: 0.1,
|
|
1478
|
+
provenance: 0.05
|
|
1479
|
+
};
|
|
1480
|
+
QUALITY = {
|
|
1481
|
+
/** README at/above this many characters scores full marks on doc length. */
|
|
1482
|
+
docsFullLengthChars: 1200,
|
|
1483
|
+
/** Direct-dependency count at/above this scores 0 on the deps component (log-scaled). */
|
|
1484
|
+
depsLogMax: 30,
|
|
1485
|
+
/** A license string present but not OSI-recognized scores this (vs 100 / 0). */
|
|
1486
|
+
licenseUnrecognizedScore: 60
|
|
1487
|
+
};
|
|
1118
1488
|
MAINTENANCE_WEIGHTS = {
|
|
1119
1489
|
recency: 0.5,
|
|
1120
1490
|
cadence: 0.25,
|
|
@@ -1162,6 +1532,35 @@ var init_weights = __esm({
|
|
|
1162
1532
|
/** Bundle size at the category median maps to this score; smaller → higher. */
|
|
1163
1533
|
medianScore: 50
|
|
1164
1534
|
};
|
|
1535
|
+
DISCOVERY = {
|
|
1536
|
+
/** A queued candidate must clear this quality pre-score to graduate to ingest. */
|
|
1537
|
+
minPreScore: 45,
|
|
1538
|
+
/** Max candidates fully ingested per crawler run (cost bound). */
|
|
1539
|
+
perRunCap: 25,
|
|
1540
|
+
/** npm-search hits to pull per category keyword. */
|
|
1541
|
+
searchSizePerCategory: 10,
|
|
1542
|
+
/** Max dependency-graph neighbors to enqueue per tracked seed. */
|
|
1543
|
+
graphNeighborsPerSeed: 20
|
|
1544
|
+
};
|
|
1545
|
+
DEFAULT_WEIGHTS = {
|
|
1546
|
+
health: { ...HEALTH_WEIGHTS },
|
|
1547
|
+
composite: { ...COMPOSITE }
|
|
1548
|
+
};
|
|
1549
|
+
SETTABLE = {
|
|
1550
|
+
"health.maintenance": (w, v) => w.health.maintenance = v,
|
|
1551
|
+
"health.adoption": (w, v) => w.health.adoption = v,
|
|
1552
|
+
"health.reliability": (w, v) => w.health.reliability = v,
|
|
1553
|
+
"health.efficiency": (w, v) => w.health.efficiency = v,
|
|
1554
|
+
"composite.lambda": (w, v) => w.composite.lambda = v
|
|
1555
|
+
};
|
|
1556
|
+
WEIGHT_EXPLANATIONS = {
|
|
1557
|
+
maintenance: "release recency, cadence, and issue close-ratio (weights.ts MAINTENANCE_WEIGHTS).",
|
|
1558
|
+
adoption: "weekly downloads (log-scaled), stars, and 90-day growth (weights.ts ADOPTION).",
|
|
1559
|
+
reliability: "OpenSSF Scorecard scaled 0\u2013100, minus advisory penalties (weights.ts RELIABILITY).",
|
|
1560
|
+
efficiency: "bundle size vs the category median; frontend categories only (weights.ts EFFICIENCY).",
|
|
1561
|
+
quality: "intrinsic, adoption-independent: types, tests, docs, changelog, deps, license, provenance (weights.ts QUALITY_WEIGHTS). A standalone axis \u2014 it does NOT feed health.",
|
|
1562
|
+
lambda: "how much the default sort composite favors quality over health: (1\u2212\u03BB)\xB7health + \u03BB\xB7quality."
|
|
1563
|
+
};
|
|
1165
1564
|
CONFIDENCE = {
|
|
1166
1565
|
proven: {
|
|
1167
1566
|
minWeeklyDownloads: 1e5,
|
|
@@ -1172,6 +1571,11 @@ var init_weights = __esm({
|
|
|
1172
1571
|
minWeeklyDownloads: 5e3,
|
|
1173
1572
|
strongGrowth: 0.5,
|
|
1174
1573
|
maxLastReleaseMonths: 9
|
|
1574
|
+
},
|
|
1575
|
+
/** `promising` (§1): new but intrinsically high-quality, regardless of adoption. */
|
|
1576
|
+
promising: {
|
|
1577
|
+
minQuality: 70,
|
|
1578
|
+
maxLastReleaseMonths: 12
|
|
1175
1579
|
}
|
|
1176
1580
|
};
|
|
1177
1581
|
}
|
|
@@ -1180,6 +1584,7 @@ var init_weights = __esm({
|
|
|
1180
1584
|
// src/scoring/score.ts
|
|
1181
1585
|
function toScoringInput(signals, category) {
|
|
1182
1586
|
const { registry, downloads, github, depsDev, bundle } = signals;
|
|
1587
|
+
const readme = registry?.readme ?? null;
|
|
1183
1588
|
return {
|
|
1184
1589
|
weeklyDownloads: downloads?.weeklyDownloads ?? null,
|
|
1185
1590
|
downloadGrowth90d: downloads?.downloadGrowth90d ?? null,
|
|
@@ -1196,7 +1601,18 @@ function toScoringInput(signals, category) {
|
|
|
1196
1601
|
deprecated: registry?.deprecated ?? false,
|
|
1197
1602
|
archived: github?.archived ?? false,
|
|
1198
1603
|
bundleMinGzipKb: bundle?.bundleMinGzipKb ?? null,
|
|
1199
|
-
category
|
|
1604
|
+
category,
|
|
1605
|
+
// Quality signals — null (not false) when the manifest itself is missing, so
|
|
1606
|
+
// weightedAverage drops the component rather than scoring it 0.
|
|
1607
|
+
hasTypes: registry ? registry.hasTypes : null,
|
|
1608
|
+
hasTestScript: registry ? registry.hasTestScript : null,
|
|
1609
|
+
readmeLength: readme !== null ? readme.length : null,
|
|
1610
|
+
hasExamples: readme !== null ? /```/.test(readme) : null,
|
|
1611
|
+
hasHomepage: registry ? registry.homepage !== null : null,
|
|
1612
|
+
hasReleaseNotes: github?.releasesLast12mo != null ? github.releasesLast12mo > 0 : null,
|
|
1613
|
+
directDependenciesCount: registry?.directDependenciesCount ?? null,
|
|
1614
|
+
license: registry?.license ?? null,
|
|
1615
|
+
hasProvenance: registry ? registry.hasProvenance : null
|
|
1200
1616
|
};
|
|
1201
1617
|
}
|
|
1202
1618
|
function computeMaintenance(input, now) {
|
|
@@ -1254,23 +1670,68 @@ function computeEfficiency(bundleMinGzipKb, category, categoryMedian) {
|
|
|
1254
1670
|
const ratio = bundleMinGzipKb / categoryMedian;
|
|
1255
1671
|
return clamp(100 - ratio * EFFICIENCY.medianScore);
|
|
1256
1672
|
}
|
|
1257
|
-
function
|
|
1673
|
+
function isRecognizedLicense(license) {
|
|
1674
|
+
if (!license) return false;
|
|
1675
|
+
const id = license.trim().toLowerCase();
|
|
1676
|
+
return RECOGNIZED_LICENSES.some((l) => id === l || id.startsWith(l));
|
|
1677
|
+
}
|
|
1678
|
+
function computeQuality(input) {
|
|
1679
|
+
const components = [];
|
|
1680
|
+
if (input.hasTypes !== null) {
|
|
1681
|
+
components.push({ value: input.hasTypes ? 100 : 0, weight: QUALITY_WEIGHTS.types });
|
|
1682
|
+
}
|
|
1683
|
+
if (input.hasTestScript !== null) {
|
|
1684
|
+
components.push({ value: input.hasTestScript ? 100 : 0, weight: QUALITY_WEIGHTS.tests });
|
|
1685
|
+
}
|
|
1686
|
+
if (input.readmeLength !== null || input.hasHomepage !== null) {
|
|
1687
|
+
const lenScore = Math.min((input.readmeLength ?? 0) / QUALITY.docsFullLengthChars, 1) * 60;
|
|
1688
|
+
const examples = input.hasExamples ? 25 : 0;
|
|
1689
|
+
const homepage = input.hasHomepage ? 15 : 0;
|
|
1690
|
+
components.push({ value: clamp(lenScore + examples + homepage), weight: QUALITY_WEIGHTS.docs });
|
|
1691
|
+
}
|
|
1692
|
+
if (input.hasReleaseNotes !== null) {
|
|
1693
|
+
components.push({ value: input.hasReleaseNotes ? 100 : 0, weight: QUALITY_WEIGHTS.changelog });
|
|
1694
|
+
}
|
|
1695
|
+
if (input.directDependenciesCount !== null) {
|
|
1696
|
+
const deps = input.directDependenciesCount;
|
|
1697
|
+
const depsScore = clamp(
|
|
1698
|
+
100 - Math.log10(deps + 1) / Math.log10(QUALITY.depsLogMax + 1) * 100
|
|
1699
|
+
);
|
|
1700
|
+
components.push({ value: depsScore, weight: QUALITY_WEIGHTS.deps });
|
|
1701
|
+
}
|
|
1702
|
+
if (input.license !== null) {
|
|
1703
|
+
components.push({
|
|
1704
|
+
value: isRecognizedLicense(input.license) ? 100 : QUALITY.licenseUnrecognizedScore,
|
|
1705
|
+
weight: QUALITY_WEIGHTS.license
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
if (input.hasProvenance) {
|
|
1709
|
+
components.push({ value: 100, weight: QUALITY_WEIGHTS.provenance });
|
|
1710
|
+
}
|
|
1711
|
+
if (components.length === 0) return null;
|
|
1712
|
+
return weightedAverage(components);
|
|
1713
|
+
}
|
|
1714
|
+
function computeHealthScore(breakdown, weights = HEALTH_WEIGHTS) {
|
|
1258
1715
|
const { maintenance, adoption, reliability, efficiency } = breakdown;
|
|
1716
|
+
const w = weights;
|
|
1259
1717
|
if (efficiency === null) {
|
|
1260
|
-
const w = HEALTH_WEIGHTS;
|
|
1261
1718
|
const denom = w.maintenance + w.adoption + w.reliability;
|
|
1262
1719
|
return Math.round(
|
|
1263
1720
|
(w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability) / denom
|
|
1264
1721
|
);
|
|
1265
1722
|
}
|
|
1266
1723
|
return Math.round(
|
|
1267
|
-
|
|
1724
|
+
w.maintenance * maintenance + w.adoption * adoption + w.reliability * reliability + w.efficiency * efficiency
|
|
1268
1725
|
);
|
|
1269
1726
|
}
|
|
1727
|
+
function computeComposite(healthScore, qualityScore, lambda = COMPOSITE.lambda) {
|
|
1728
|
+
if (qualityScore === null) return healthScore;
|
|
1729
|
+
return Math.round((1 - lambda) * healthScore + lambda * qualityScore);
|
|
1730
|
+
}
|
|
1270
1731
|
function hasCriticalOrHighAdvisory(advisories) {
|
|
1271
1732
|
return advisories.some((a) => a.severity === "critical" || a.severity === "high");
|
|
1272
1733
|
}
|
|
1273
|
-
function computeConfidence(input, now) {
|
|
1734
|
+
function computeConfidence(input, now, qualityScore = null) {
|
|
1274
1735
|
const dl = input.weeklyDownloads ?? 0;
|
|
1275
1736
|
const ageMonths = monthsSince(input.firstPublishedAt, now);
|
|
1276
1737
|
const lastReleaseMonths = monthsSince(input.lastReleaseAt, now);
|
|
@@ -1285,6 +1746,10 @@ function computeConfidence(input, now) {
|
|
|
1285
1746
|
if (emergingAdoptionOk && emergingReleaseOk && !input.deprecated && !input.archived) {
|
|
1286
1747
|
return "emerging";
|
|
1287
1748
|
}
|
|
1749
|
+
const promisingReleaseOk = lastReleaseMonths !== null && lastReleaseMonths <= CONFIDENCE.promising.maxLastReleaseMonths;
|
|
1750
|
+
if (qualityScore !== null && qualityScore >= CONFIDENCE.promising.minQuality && promisingReleaseOk && !hasCriticalOrHighAdvisory(input.advisories) && !input.deprecated && !input.archived) {
|
|
1751
|
+
return "promising";
|
|
1752
|
+
}
|
|
1288
1753
|
return "unproven";
|
|
1289
1754
|
}
|
|
1290
1755
|
function weightedAverage(components) {
|
|
@@ -1299,7 +1764,7 @@ function median(values) {
|
|
|
1299
1764
|
const mid = Math.floor(sorted.length / 2);
|
|
1300
1765
|
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
1301
1766
|
}
|
|
1302
|
-
var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince;
|
|
1767
|
+
var DAY_MS, MONTH_MS, clamp, daysSince, monthsSince, RECOGNIZED_LICENSES;
|
|
1303
1768
|
var init_score = __esm({
|
|
1304
1769
|
"src/scoring/score.ts"() {
|
|
1305
1770
|
"use strict";
|
|
@@ -1311,6 +1776,20 @@ var init_score = __esm({
|
|
|
1311
1776
|
clamp = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
|
|
1312
1777
|
daysSince = (date, now) => date ? (now.getTime() - date.getTime()) / DAY_MS : null;
|
|
1313
1778
|
monthsSince = (date, now) => date ? (now.getTime() - date.getTime()) / MONTH_MS : null;
|
|
1779
|
+
RECOGNIZED_LICENSES = [
|
|
1780
|
+
"mit",
|
|
1781
|
+
"isc",
|
|
1782
|
+
"apache-2.0",
|
|
1783
|
+
"bsd-2-clause",
|
|
1784
|
+
"bsd-3-clause",
|
|
1785
|
+
"mpl-2.0",
|
|
1786
|
+
"lgpl",
|
|
1787
|
+
"gpl",
|
|
1788
|
+
"agpl",
|
|
1789
|
+
"unlicense",
|
|
1790
|
+
"0bsd",
|
|
1791
|
+
"cc0-1.0"
|
|
1792
|
+
];
|
|
1314
1793
|
}
|
|
1315
1794
|
});
|
|
1316
1795
|
|
|
@@ -1422,7 +1901,7 @@ var init_embeddings = __esm({
|
|
|
1422
1901
|
});
|
|
1423
1902
|
|
|
1424
1903
|
// src/db/packages.ts
|
|
1425
|
-
import { eq, sql } from "drizzle-orm";
|
|
1904
|
+
import { eq, sql as sql2 } from "drizzle-orm";
|
|
1426
1905
|
async function getSeedTargets(db) {
|
|
1427
1906
|
const rows = await db.select({ name: seedPackages.name, category: seedPackages.category }).from(seedPackages);
|
|
1428
1907
|
return rows.map((r) => ({ name: r.name, category: r.category ?? null }));
|
|
@@ -1508,18 +1987,28 @@ async function runSync(opts = {}) {
|
|
|
1508
1987
|
prefetchedWeekly: weeklyMap.has(target.name) ? weeklyMap.get(target.name) : void 0
|
|
1509
1988
|
});
|
|
1510
1989
|
for (const e of signals.errors) allErrors.push({ package: target.name, ...e });
|
|
1511
|
-
const input = toScoringInput(signals, target.category);
|
|
1512
1990
|
const summaryInput = await buildSummaryInput(signals, target.category);
|
|
1513
|
-
const { summary, usageGuide } = await provider.generate(summaryInput);
|
|
1991
|
+
const { summary, usageGuide, inferredCategory } = await provider.generate(summaryInput);
|
|
1992
|
+
let category = target.category;
|
|
1993
|
+
let categorySource = target.category ? "curated" : null;
|
|
1994
|
+
if (!category) {
|
|
1995
|
+
category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
|
|
1996
|
+
categorySource = category ? "inferred" : null;
|
|
1997
|
+
}
|
|
1998
|
+
const input = toScoringInput(signals, category);
|
|
1999
|
+
const quality = computeQuality(input);
|
|
1514
2000
|
if (++done % 25 === 0) logger.info(` \u2026${done}/${targets.length}`);
|
|
1515
2001
|
return {
|
|
1516
2002
|
target,
|
|
2003
|
+
category,
|
|
2004
|
+
categorySource,
|
|
1517
2005
|
signals,
|
|
1518
2006
|
input,
|
|
1519
2007
|
maintenance: computeMaintenance(input, now),
|
|
1520
2008
|
adoption: computeAdoption(input),
|
|
1521
2009
|
reliability: computeReliability(input),
|
|
1522
|
-
|
|
2010
|
+
quality,
|
|
2011
|
+
confidence: computeConfidence(input, now, quality),
|
|
1523
2012
|
summary,
|
|
1524
2013
|
usageGuide
|
|
1525
2014
|
};
|
|
@@ -1542,7 +2031,7 @@ async function runSync(opts = {}) {
|
|
|
1542
2031
|
ok.map(
|
|
1543
2032
|
(c) => buildEmbeddingText({
|
|
1544
2033
|
name: c.target.name,
|
|
1545
|
-
category: c.
|
|
2034
|
+
category: c.category,
|
|
1546
2035
|
summary: c.summary,
|
|
1547
2036
|
description: c.signals.registry?.description ?? null
|
|
1548
2037
|
})
|
|
@@ -1553,21 +2042,23 @@ async function runSync(opts = {}) {
|
|
|
1553
2042
|
const c = ok[i];
|
|
1554
2043
|
const efficiency = computeEfficiency(
|
|
1555
2044
|
c.input.bundleMinGzipKb,
|
|
1556
|
-
c.
|
|
1557
|
-
c.
|
|
2045
|
+
c.category,
|
|
2046
|
+
c.category ? medians.get(c.category) ?? null : null
|
|
1558
2047
|
);
|
|
1559
2048
|
const breakdown = {
|
|
1560
2049
|
maintenance: c.maintenance,
|
|
1561
2050
|
adoption: c.adoption,
|
|
1562
2051
|
reliability: c.reliability,
|
|
1563
|
-
efficiency
|
|
2052
|
+
efficiency,
|
|
2053
|
+
quality: c.quality
|
|
1564
2054
|
};
|
|
1565
2055
|
const healthScore = computeHealthScore(breakdown);
|
|
1566
2056
|
await upsertPackage(
|
|
1567
2057
|
handle.db,
|
|
1568
2058
|
assemblePackageRow({
|
|
1569
2059
|
name: c.target.name,
|
|
1570
|
-
category: c.
|
|
2060
|
+
category: c.category,
|
|
2061
|
+
categorySource: c.categorySource,
|
|
1571
2062
|
signals: c.signals,
|
|
1572
2063
|
input: c.input,
|
|
1573
2064
|
summary: c.summary,
|
|
@@ -1575,6 +2066,7 @@ async function runSync(opts = {}) {
|
|
|
1575
2066
|
confidence: c.confidence,
|
|
1576
2067
|
breakdown,
|
|
1577
2068
|
healthScore,
|
|
2069
|
+
qualityScore: c.quality,
|
|
1578
2070
|
embedding: embeddings[i] ?? null,
|
|
1579
2071
|
now
|
|
1580
2072
|
})
|
|
@@ -1614,7 +2106,7 @@ async function resolveTargets(db, opts) {
|
|
|
1614
2106
|
function computeCategoryMedians(computed) {
|
|
1615
2107
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1616
2108
|
for (const c of computed) {
|
|
1617
|
-
const cat = c.
|
|
2109
|
+
const cat = c.category;
|
|
1618
2110
|
const kb = c.input.bundleMinGzipKb;
|
|
1619
2111
|
if (cat && isFrontendCategory(cat) && kb !== null) {
|
|
1620
2112
|
const list = byCategory.get(cat) ?? [];
|
|
@@ -1635,6 +2127,7 @@ function assemblePackageRow(p) {
|
|
|
1635
2127
|
name: p.name,
|
|
1636
2128
|
ecosystem: "npm",
|
|
1637
2129
|
category: p.category,
|
|
2130
|
+
categorySource: p.categorySource,
|
|
1638
2131
|
description: r?.description ?? null,
|
|
1639
2132
|
summary: p.summary,
|
|
1640
2133
|
repoUrl: r?.repoUrl ?? null,
|
|
@@ -1655,6 +2148,7 @@ function assemblePackageRow(p) {
|
|
|
1655
2148
|
bundleMinGzipKb: p.input.bundleMinGzipKb,
|
|
1656
2149
|
advisories: p.input.advisories,
|
|
1657
2150
|
healthScore: p.healthScore,
|
|
2151
|
+
qualityScore: p.qualityScore,
|
|
1658
2152
|
confidence: p.confidence,
|
|
1659
2153
|
scoreBreakdown: p.breakdown,
|
|
1660
2154
|
usageGuide: p.usageGuide,
|
|
@@ -1673,6 +2167,7 @@ var init_sync = __esm({
|
|
|
1673
2167
|
init_collect();
|
|
1674
2168
|
init_npmDownloads();
|
|
1675
2169
|
init_summarize();
|
|
2170
|
+
init_categoryInference();
|
|
1676
2171
|
init_embeddings();
|
|
1677
2172
|
init_scoring();
|
|
1678
2173
|
init_client();
|
|
@@ -1682,14 +2177,14 @@ var init_sync = __esm({
|
|
|
1682
2177
|
});
|
|
1683
2178
|
|
|
1684
2179
|
// src/pipeline/single.ts
|
|
1685
|
-
import { and, eq as eq2, isNotNull, sql as
|
|
2180
|
+
import { and, eq as eq2, isNotNull, sql as sql3 } from "drizzle-orm";
|
|
1686
2181
|
async function getSeedCategory(db, name) {
|
|
1687
2182
|
const [row] = await db.select({ category: seedPackages.category }).from(seedPackages).where(eq2(seedPackages.name, name)).limit(1);
|
|
1688
2183
|
return row?.category ?? null;
|
|
1689
2184
|
}
|
|
1690
2185
|
async function getCategoryMedianBundle(db, category) {
|
|
1691
2186
|
const [row] = await db.select({
|
|
1692
|
-
m:
|
|
2187
|
+
m: sql3`percentile_cont(0.5) within group (order by ${packages.bundleMinGzipKb})`
|
|
1693
2188
|
}).from(packages).where(and(eq2(packages.category, category), isNotNull(packages.bundleMinGzipKb)));
|
|
1694
2189
|
return row?.m ?? null;
|
|
1695
2190
|
}
|
|
@@ -1697,24 +2192,39 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
1697
2192
|
const config = getConfig();
|
|
1698
2193
|
const now = /* @__PURE__ */ new Date();
|
|
1699
2194
|
const existing = await getPackageByName(db, name);
|
|
1700
|
-
const
|
|
2195
|
+
const curatedCategory = opts.category ?? await getSeedCategory(db, name) ?? null;
|
|
2196
|
+
const initialCategory = curatedCategory ?? existing?.category ?? null;
|
|
1701
2197
|
const prefetchedWeekly = await fetchWeeklyDownloads(name).catch(() => null);
|
|
1702
|
-
const signals = await collectSignals(name,
|
|
2198
|
+
const signals = await collectSignals(name, initialCategory, {
|
|
1703
2199
|
githubToken: config.GITHUB_TOKEN,
|
|
1704
2200
|
prefetchedWeekly
|
|
1705
2201
|
});
|
|
2202
|
+
const summaryInput = await buildSummaryInput(signals, initialCategory);
|
|
2203
|
+
const { summary, usageGuide, inferredCategory } = await createSummaryProvider().generate(summaryInput);
|
|
2204
|
+
let category;
|
|
2205
|
+
let categorySource;
|
|
2206
|
+
if (curatedCategory) {
|
|
2207
|
+
category = curatedCategory;
|
|
2208
|
+
categorySource = "curated";
|
|
2209
|
+
} else if (existing?.category) {
|
|
2210
|
+
category = existing.category;
|
|
2211
|
+
categorySource = existing.categorySource ?? "inferred";
|
|
2212
|
+
} else {
|
|
2213
|
+
category = inferCategoryFromSignals(signals) ?? inferredCategory ?? null;
|
|
2214
|
+
categorySource = category ? "inferred" : null;
|
|
2215
|
+
}
|
|
1706
2216
|
const input = toScoringInput(signals, category);
|
|
1707
|
-
const summaryInput = await buildSummaryInput(signals, category);
|
|
1708
|
-
const { summary, usageGuide } = await createSummaryProvider().generate(summaryInput);
|
|
1709
2217
|
const median2 = category ? await getCategoryMedianBundle(db, category) : null;
|
|
2218
|
+
const quality = computeQuality(input);
|
|
1710
2219
|
const breakdown = {
|
|
1711
2220
|
maintenance: computeMaintenance(input, now),
|
|
1712
2221
|
adoption: computeAdoption(input),
|
|
1713
2222
|
reliability: computeReliability(input),
|
|
1714
|
-
efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2)
|
|
2223
|
+
efficiency: computeEfficiency(input.bundleMinGzipKb, category, median2),
|
|
2224
|
+
quality
|
|
1715
2225
|
};
|
|
1716
2226
|
const healthScore = computeHealthScore(breakdown);
|
|
1717
|
-
const confidence = computeConfidence(input, now);
|
|
2227
|
+
const confidence = computeConfidence(input, now, quality);
|
|
1718
2228
|
const [embedding] = await createEmbeddingProvider().embed([
|
|
1719
2229
|
buildEmbeddingText({ name, category, summary, description: signals.registry?.description ?? null })
|
|
1720
2230
|
]);
|
|
@@ -1723,6 +2233,7 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
1723
2233
|
assemblePackageRow({
|
|
1724
2234
|
name,
|
|
1725
2235
|
category,
|
|
2236
|
+
categorySource,
|
|
1726
2237
|
signals,
|
|
1727
2238
|
input,
|
|
1728
2239
|
summary,
|
|
@@ -1730,6 +2241,7 @@ async function syncOnePackage(db, name, opts = {}) {
|
|
|
1730
2241
|
confidence,
|
|
1731
2242
|
breakdown,
|
|
1732
2243
|
healthScore,
|
|
2244
|
+
qualityScore: quality,
|
|
1733
2245
|
embedding: embedding ?? null,
|
|
1734
2246
|
now
|
|
1735
2247
|
})
|
|
@@ -1753,6 +2265,7 @@ var init_single = __esm({
|
|
|
1753
2265
|
init_collect();
|
|
1754
2266
|
init_sources();
|
|
1755
2267
|
init_summarize();
|
|
2268
|
+
init_categoryInference();
|
|
1756
2269
|
init_scoring();
|
|
1757
2270
|
init_embeddings();
|
|
1758
2271
|
init_packages();
|
|
@@ -1761,103 +2274,83 @@ var init_single = __esm({
|
|
|
1761
2274
|
}
|
|
1762
2275
|
});
|
|
1763
2276
|
|
|
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
2277
|
// src/search/recommend.ts
|
|
1807
|
-
import { and as and2, cosineDistance,
|
|
2278
|
+
import { and as and2, cosineDistance, eq as eq3, isNotNull as isNotNull2, lte, sql as sql4 } from "drizzle-orm";
|
|
1808
2279
|
async function recommend(db, opts, provider = createEmbeddingProvider()) {
|
|
1809
2280
|
const limit = Math.min(Math.max(opts.limit ?? 3, 1), 5);
|
|
1810
2281
|
const [queryVec] = await provider.embed([opts.need]);
|
|
1811
2282
|
if (!queryVec) return [];
|
|
1812
2283
|
const category = opts.category ?? inferCategory(opts.need);
|
|
1813
2284
|
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
|
-
|
|
2285
|
+
let fused = await hybridSearch(db, queryVec, opts.need, opts.constraints, category, pool);
|
|
2286
|
+
if (category && fused.length < limit) {
|
|
2287
|
+
const broad = await hybridSearch(db, queryVec, opts.need, opts.constraints, null, pool);
|
|
2288
|
+
const seen = new Set(fused.map((f) => f.row.name));
|
|
2289
|
+
fused = fused.concat(broad.filter((f) => !seen.has(f.row.name)));
|
|
2290
|
+
}
|
|
2291
|
+
if (fused.length === 0) return [];
|
|
2292
|
+
const lambda = loadWeights().composite.lambda;
|
|
2293
|
+
const maxRrf = Math.max(...fused.map((f) => f.rrf));
|
|
2294
|
+
const ranked = fused.map((f) => {
|
|
2295
|
+
const relevance = maxRrf > 0 ? f.rrf / maxRrf : 0;
|
|
2296
|
+
const composite = computeComposite(f.row.healthScore ?? 0, f.row.qualityScore, lambda) / 100;
|
|
2297
|
+
return { row: f.row, score: RELEVANCE_WEIGHT * relevance + COMPOSITE_WEIGHT * composite };
|
|
1825
2298
|
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1826
2299
|
return ranked.map(({ row }) => toCandidate(row));
|
|
1827
2300
|
}
|
|
1828
|
-
async function
|
|
1829
|
-
const
|
|
1830
|
-
|
|
2301
|
+
async function hybridSearch(db, queryVec, need, constraints, category, pool) {
|
|
2302
|
+
const [vectorRows, lexicalRows] = await Promise.all([
|
|
2303
|
+
runVectorQuery(db, queryVec, constraints, category, pool),
|
|
2304
|
+
runLexicalQuery(db, need, constraints, category, pool)
|
|
2305
|
+
]);
|
|
2306
|
+
return rrfFuse([vectorRows, lexicalRows]);
|
|
2307
|
+
}
|
|
2308
|
+
function rrfFuse(lists, k = RRF_K) {
|
|
2309
|
+
const fused = /* @__PURE__ */ new Map();
|
|
2310
|
+
for (const list of lists) {
|
|
2311
|
+
list.forEach((row, idx) => {
|
|
2312
|
+
const contribution = 1 / (k + idx + 1);
|
|
2313
|
+
const prev = fused.get(row.name);
|
|
2314
|
+
if (prev) prev.rrf += contribution;
|
|
2315
|
+
else fused.set(row.name, { row, rrf: contribution });
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
return [...fused.values()].sort((a, b) => b.rrf - a.rrf);
|
|
2319
|
+
}
|
|
2320
|
+
function buildConditions(constraints, category) {
|
|
2321
|
+
const conditions = [];
|
|
1831
2322
|
if (category) conditions.push(eq3(packages.category, category));
|
|
1832
2323
|
if (constraints?.license) conditions.push(eq3(packages.license, constraints.license));
|
|
1833
2324
|
if (constraints?.maxBundleKb !== void 0) {
|
|
1834
2325
|
conditions.push(lte(packages.bundleMinGzipKb, constraints.maxBundleKb));
|
|
1835
2326
|
}
|
|
1836
2327
|
if (constraints?.minConfidence) {
|
|
1837
|
-
const allowed = ["proven", "emerging", "unproven"].filter(
|
|
2328
|
+
const allowed = ["proven", "emerging", "promising", "unproven"].filter(
|
|
1838
2329
|
(c) => CONFIDENCE_RANK[c] >= CONFIDENCE_RANK[constraints.minConfidence]
|
|
1839
2330
|
);
|
|
1840
2331
|
conditions.push(
|
|
1841
|
-
|
|
2332
|
+
sql4`${packages.confidence} in ${sql4.raw(`(${allowed.map((c) => `'${c}'`).join(",")})`)}`
|
|
1842
2333
|
);
|
|
1843
2334
|
}
|
|
1844
|
-
return
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2335
|
+
return conditions;
|
|
2336
|
+
}
|
|
2337
|
+
async function runVectorQuery(db, queryVec, constraints, category, pool) {
|
|
2338
|
+
const distance = cosineDistance(packages.embedding, queryVec);
|
|
2339
|
+
const conditions = [isNotNull2(packages.embedding), ...buildConditions(constraints, category)];
|
|
2340
|
+
return db.select(ROW_COLUMNS).from(packages).where(and2(...conditions)).orderBy(distance).limit(pool);
|
|
2341
|
+
}
|
|
2342
|
+
async function runLexicalQuery(db, need, constraints, category, pool) {
|
|
2343
|
+
const tsquery = sql4`websearch_to_tsquery('english', ${need})`;
|
|
2344
|
+
const rank = sql4`ts_rank(${packages.searchVector}, ${tsquery})`;
|
|
2345
|
+
const conditions = [sql4`${packages.searchVector} @@ ${tsquery}`, ...buildConditions(constraints, category)];
|
|
2346
|
+
return db.select(ROW_COLUMNS).from(packages).where(and2(...conditions)).orderBy(sql4`${rank} desc`).limit(pool);
|
|
1855
2347
|
}
|
|
1856
2348
|
function toCandidate(row) {
|
|
1857
2349
|
return {
|
|
1858
2350
|
name: row.name,
|
|
1859
2351
|
category: row.category,
|
|
1860
2352
|
healthScore: row.healthScore ?? 0,
|
|
2353
|
+
qualityScore: row.qualityScore,
|
|
1861
2354
|
confidence: row.confidence ?? "unproven",
|
|
1862
2355
|
why: buildWhy(row),
|
|
1863
2356
|
latestVersion: row.latestVersion,
|
|
@@ -1878,22 +2371,41 @@ function formatDownloads(n) {
|
|
|
1878
2371
|
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
1879
2372
|
return String(n);
|
|
1880
2373
|
}
|
|
1881
|
-
var CONFIDENCE_RANK,
|
|
2374
|
+
var CONFIDENCE_RANK, RELEVANCE_WEIGHT, COMPOSITE_WEIGHT, RRF_K, ROW_COLUMNS;
|
|
1882
2375
|
var init_recommend = __esm({
|
|
1883
2376
|
"src/search/recommend.ts"() {
|
|
1884
2377
|
"use strict";
|
|
1885
2378
|
init_esm_shims();
|
|
1886
2379
|
init_schema();
|
|
2380
|
+
init_scoring();
|
|
2381
|
+
init_weights();
|
|
1887
2382
|
init_categoryInference();
|
|
1888
2383
|
init_embeddings();
|
|
1889
|
-
CONFIDENCE_RANK = {
|
|
1890
|
-
|
|
1891
|
-
|
|
2384
|
+
CONFIDENCE_RANK = {
|
|
2385
|
+
unproven: 0,
|
|
2386
|
+
promising: 1,
|
|
2387
|
+
emerging: 2,
|
|
2388
|
+
proven: 3
|
|
2389
|
+
};
|
|
2390
|
+
RELEVANCE_WEIGHT = 0.6;
|
|
2391
|
+
COMPOSITE_WEIGHT = 0.4;
|
|
2392
|
+
RRF_K = 60;
|
|
2393
|
+
ROW_COLUMNS = {
|
|
2394
|
+
name: packages.name,
|
|
2395
|
+
category: packages.category,
|
|
2396
|
+
healthScore: packages.healthScore,
|
|
2397
|
+
qualityScore: packages.qualityScore,
|
|
2398
|
+
confidence: packages.confidence,
|
|
2399
|
+
latestVersion: packages.latestVersion,
|
|
2400
|
+
weeklyDownloads: packages.weeklyDownloads,
|
|
2401
|
+
lastReleaseAt: packages.lastReleaseAt,
|
|
2402
|
+
repoUrl: packages.repoUrl
|
|
2403
|
+
};
|
|
1892
2404
|
}
|
|
1893
2405
|
});
|
|
1894
2406
|
|
|
1895
2407
|
// src/mcp/handlers.ts
|
|
1896
|
-
import { sql as
|
|
2408
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
1897
2409
|
function isStale(dataAsOf) {
|
|
1898
2410
|
if (!dataAsOf) return true;
|
|
1899
2411
|
return Date.now() - dataAsOf.getTime() > STALENESS_DAYS * DAY_MS2;
|
|
@@ -1910,7 +2422,8 @@ function rowToEvaluate(row) {
|
|
|
1910
2422
|
maintenance: 0,
|
|
1911
2423
|
adoption: 0,
|
|
1912
2424
|
reliability: 0,
|
|
1913
|
-
efficiency: null
|
|
2425
|
+
efficiency: null,
|
|
2426
|
+
quality: null
|
|
1914
2427
|
};
|
|
1915
2428
|
return {
|
|
1916
2429
|
dataAsOf: (row.dataAsOf ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1918,6 +2431,7 @@ function rowToEvaluate(row) {
|
|
|
1918
2431
|
name: row.name,
|
|
1919
2432
|
category: row.category,
|
|
1920
2433
|
healthScore: row.healthScore ?? 0,
|
|
2434
|
+
qualityScore: row.qualityScore ?? null,
|
|
1921
2435
|
confidence: row.confidence ?? "unproven",
|
|
1922
2436
|
scoreBreakdown: breakdown,
|
|
1923
2437
|
latestVersion: row.latestVersion,
|
|
@@ -1936,7 +2450,7 @@ function rowToEvaluate(row) {
|
|
|
1936
2450
|
};
|
|
1937
2451
|
}
|
|
1938
2452
|
async function latestDataAsOf(db) {
|
|
1939
|
-
const [row] = await db.select({ m:
|
|
2453
|
+
const [row] = await db.select({ m: sql5`max(${packages.dataAsOf})` }).from(packages);
|
|
1940
2454
|
return new Date(row?.m ?? Date.now()).toISOString();
|
|
1941
2455
|
}
|
|
1942
2456
|
async function handleRecommend(db, input) {
|
|
@@ -2239,7 +2753,7 @@ var init_server = __esm({
|
|
|
2239
2753
|
init_handlers();
|
|
2240
2754
|
init_diagram();
|
|
2241
2755
|
categoryEnum = z2.enum(CATEGORIES);
|
|
2242
|
-
confidenceEnum = z2.enum(["proven", "emerging", "unproven"]);
|
|
2756
|
+
confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
|
|
2243
2757
|
constraintsSchema = z2.object({
|
|
2244
2758
|
runtime: z2.enum(["browser", "node", "both"]).optional(),
|
|
2245
2759
|
license: z2.string().optional(),
|
|
@@ -2249,27 +2763,378 @@ var init_server = __esm({
|
|
|
2249
2763
|
}
|
|
2250
2764
|
});
|
|
2251
2765
|
|
|
2252
|
-
// src/
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2766
|
+
// src/auth/apiKeys.ts
|
|
2767
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
2768
|
+
import { and as and3, desc, eq as eq4, isNull } from "drizzle-orm";
|
|
2769
|
+
function hashKey(key) {
|
|
2770
|
+
return createHash3("sha256").update(key).digest("hex");
|
|
2771
|
+
}
|
|
2772
|
+
function generateApiKey() {
|
|
2773
|
+
const body = randomBytes(24).toString("base64url");
|
|
2774
|
+
const key = API_KEY_PREFIX + body;
|
|
2775
|
+
return { key, hash: hashKey(key), prefix: API_KEY_PREFIX + body.slice(0, DISPLAY_BODY) };
|
|
2776
|
+
}
|
|
2777
|
+
async function createKey(db, input = {}) {
|
|
2778
|
+
const { key, hash, prefix } = generateApiKey();
|
|
2779
|
+
const [row] = await db.insert(apiKeys).values({
|
|
2780
|
+
keyHash: hash,
|
|
2781
|
+
prefix,
|
|
2782
|
+
label: input.label,
|
|
2783
|
+
tier: input.tier ?? "free",
|
|
2784
|
+
ownerId: input.ownerId
|
|
2785
|
+
}).returning();
|
|
2786
|
+
return { key, row };
|
|
2787
|
+
}
|
|
2788
|
+
async function lookupActiveKey(db, key) {
|
|
2789
|
+
const hash = hashKey(key);
|
|
2790
|
+
const [row] = await db.select().from(apiKeys).where(and3(eq4(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
|
|
2791
|
+
if (!row) return null;
|
|
2792
|
+
db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq4(apiKeys.id, row.id)).then(void 0, () => {
|
|
2793
|
+
});
|
|
2794
|
+
return row;
|
|
2795
|
+
}
|
|
2796
|
+
async function listKeys(db) {
|
|
2797
|
+
return db.select().from(apiKeys).orderBy(desc(apiKeys.createdAt));
|
|
2798
|
+
}
|
|
2799
|
+
async function revokeKey(db, prefixOrId) {
|
|
2800
|
+
const asId = Number(prefixOrId);
|
|
2801
|
+
const match = Number.isInteger(asId) && String(asId) === prefixOrId.trim() ? eq4(apiKeys.id, asId) : eq4(apiKeys.prefix, prefixOrId);
|
|
2802
|
+
const rows = await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and3(match, isNull(apiKeys.revokedAt))).returning({ id: apiKeys.id });
|
|
2803
|
+
return rows.length;
|
|
2804
|
+
}
|
|
2805
|
+
var DISPLAY_BODY;
|
|
2806
|
+
var init_apiKeys = __esm({
|
|
2807
|
+
"src/auth/apiKeys.ts"() {
|
|
2259
2808
|
"use strict";
|
|
2260
2809
|
init_esm_shims();
|
|
2261
|
-
|
|
2810
|
+
init_constants();
|
|
2811
|
+
init_schema();
|
|
2812
|
+
DISPLAY_BODY = 6;
|
|
2262
2813
|
}
|
|
2263
2814
|
});
|
|
2264
2815
|
|
|
2265
|
-
// src/
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
return
|
|
2816
|
+
// src/mcp/http.ts
|
|
2817
|
+
var http_exports = {};
|
|
2818
|
+
__export(http_exports, {
|
|
2819
|
+
startHttpServer: () => startHttpServer
|
|
2820
|
+
});
|
|
2821
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2822
|
+
function rpcError(code, message) {
|
|
2823
|
+
return { jsonrpc: "2.0", error: { code, message }, id: null };
|
|
2824
|
+
}
|
|
2825
|
+
async function startHttpServer(opts = {}) {
|
|
2826
|
+
const config = getConfig();
|
|
2827
|
+
const port = opts.port ?? config.PORT;
|
|
2828
|
+
const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
|
|
2829
|
+
const { db } = createDb({ max: 20 });
|
|
2830
|
+
const app = express();
|
|
2831
|
+
app.set("trust proxy", 1);
|
|
2832
|
+
app.use(helmet());
|
|
2833
|
+
app.use(express.json({ limit: "1mb" }));
|
|
2834
|
+
app.get("/healthz", (_req, res) => {
|
|
2835
|
+
res.status(200).json({ status: "ok" });
|
|
2836
|
+
});
|
|
2837
|
+
const ipLimiter = rateLimit({
|
|
2838
|
+
windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
|
|
2839
|
+
limit: config.LURQ_IP_RATE_LIMIT_MAX,
|
|
2840
|
+
standardHeaders: "draft-7",
|
|
2841
|
+
legacyHeaders: false,
|
|
2842
|
+
message: rpcError(-32029, "Rate limit exceeded.")
|
|
2843
|
+
});
|
|
2844
|
+
const auth = async (req, res, next) => {
|
|
2845
|
+
const header = req.headers.authorization;
|
|
2846
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
2847
|
+
if (!token) {
|
|
2848
|
+
res.status(401).json(rpcError(-32001, "Missing API key. Pass Authorization: Bearer <key>."));
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
try {
|
|
2852
|
+
const row = await lookupActiveKey(db, token);
|
|
2853
|
+
if (!row) {
|
|
2854
|
+
res.status(401).json(rpcError(-32001, "Invalid or revoked API key."));
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
req.lurqKey = row;
|
|
2858
|
+
next();
|
|
2859
|
+
} catch (err) {
|
|
2860
|
+
logger.error("auth lookup failed:", err instanceof Error ? err.message : String(err));
|
|
2861
|
+
res.status(500).json(rpcError(-32603, "Internal error."));
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
const keyLimiter = rateLimit({
|
|
2865
|
+
windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
|
|
2866
|
+
limit: config.LURQ_RATE_LIMIT_MAX,
|
|
2867
|
+
standardHeaders: "draft-7",
|
|
2868
|
+
legacyHeaders: false,
|
|
2869
|
+
// Key on the resolved API key (always present — auth runs first). The IP
|
|
2870
|
+
// fallback uses express-rate-limit's ipKeyGenerator so IPv6 addresses are
|
|
2871
|
+
// normalized correctly (v8 throws ERR_ERL_KEY_GEN_IPV6 on a raw req.ip).
|
|
2872
|
+
keyGenerator: (req) => req.lurqKey?.prefix ?? ipKeyGenerator(req.ip ?? "0.0.0.0"),
|
|
2873
|
+
message: rpcError(-32029, "Rate limit exceeded.")
|
|
2874
|
+
});
|
|
2875
|
+
app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
|
|
2876
|
+
const server = buildMcpServer(db);
|
|
2877
|
+
const transport = new StreamableHTTPServerTransport({
|
|
2878
|
+
sessionIdGenerator: void 0,
|
|
2879
|
+
enableJsonResponse: true
|
|
2880
|
+
});
|
|
2881
|
+
res.on("close", () => {
|
|
2882
|
+
void transport.close();
|
|
2883
|
+
void server.close();
|
|
2884
|
+
});
|
|
2885
|
+
try {
|
|
2886
|
+
await server.connect(transport);
|
|
2887
|
+
await transport.handleRequest(req, res, req.body);
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
logger.error("mcp request failed:", err instanceof Error ? err.message : String(err));
|
|
2890
|
+
if (!res.headersSent) res.status(500).json(rpcError(-32603, "Internal error"));
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
app.all("/mcp", (_req, res) => {
|
|
2894
|
+
res.status(405).json(rpcError(-32e3, "Method not allowed."));
|
|
2895
|
+
});
|
|
2896
|
+
app.listen(port, () => {
|
|
2897
|
+
logger.info(`lurq HTTP MCP server listening on :${port}/mcp`);
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
var init_http2 = __esm({
|
|
2901
|
+
"src/mcp/http.ts"() {
|
|
2902
|
+
"use strict";
|
|
2903
|
+
init_esm_shims();
|
|
2904
|
+
init_config();
|
|
2905
|
+
init_logger();
|
|
2906
|
+
init_apiKeys();
|
|
2907
|
+
init_client();
|
|
2908
|
+
init_server();
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
|
|
2912
|
+
// src/pipeline/rescore.ts
|
|
2913
|
+
import { isNotNull as isNotNull3 } from "drizzle-orm";
|
|
2914
|
+
import { eq as eq5 } from "drizzle-orm";
|
|
2915
|
+
async function runRescore() {
|
|
2916
|
+
const weights = loadWeights();
|
|
2917
|
+
const handle = createDb({ max: 4 });
|
|
2918
|
+
try {
|
|
2919
|
+
const rows = await handle.db.select({ id: packages.id, breakdown: packages.scoreBreakdown, healthScore: packages.healthScore }).from(packages).where(isNotNull3(packages.scoreBreakdown));
|
|
2920
|
+
let updated = 0;
|
|
2921
|
+
for (const row of rows) {
|
|
2922
|
+
if (!row.breakdown) continue;
|
|
2923
|
+
const health = computeHealthScore(row.breakdown, weights.health);
|
|
2924
|
+
if (health !== row.healthScore) {
|
|
2925
|
+
await handle.db.update(packages).set({ healthScore: health, updatedAt: /* @__PURE__ */ new Date() }).where(eq5(packages.id, row.id));
|
|
2926
|
+
updated++;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
logger.info(`Rescored ${rows.length} package(s); ${updated} health score(s) changed.`);
|
|
2930
|
+
return { seen: rows.length, updated };
|
|
2931
|
+
} finally {
|
|
2932
|
+
await handle.close();
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
var init_rescore = __esm({
|
|
2936
|
+
"src/pipeline/rescore.ts"() {
|
|
2937
|
+
"use strict";
|
|
2938
|
+
init_esm_shims();
|
|
2939
|
+
init_logger();
|
|
2940
|
+
init_client();
|
|
2941
|
+
init_schema();
|
|
2942
|
+
init_scoring();
|
|
2943
|
+
init_weights();
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
|
|
2947
|
+
// src/db/discovery.ts
|
|
2948
|
+
import { eq as eq6, inArray as inArray2, sql as sql6 } from "drizzle-orm";
|
|
2949
|
+
async function getKnownNames(db) {
|
|
2950
|
+
const [tracked, queued] = await Promise.all([
|
|
2951
|
+
db.select({ name: packages.name }).from(packages),
|
|
2952
|
+
db.select({ name: discoveryQueue.name }).from(discoveryQueue)
|
|
2953
|
+
]);
|
|
2954
|
+
return /* @__PURE__ */ new Set([...tracked.map((r) => r.name), ...queued.map((r) => r.name)]);
|
|
2955
|
+
}
|
|
2956
|
+
async function enqueueCandidates(db, candidates) {
|
|
2957
|
+
if (candidates.length === 0) return 0;
|
|
2958
|
+
const rows = candidates.map((c) => ({ name: c.name, discoveredVia: c.via }));
|
|
2959
|
+
const inserted = await db.insert(discoveryQueue).values(rows).onConflictDoNothing({ target: discoveryQueue.name }).returning({ id: discoveryQueue.id });
|
|
2960
|
+
return inserted.length;
|
|
2961
|
+
}
|
|
2962
|
+
async function getPendingCandidates(db, limit) {
|
|
2963
|
+
return db.select().from(discoveryQueue).where(eq6(discoveryQueue.status, "pending")).limit(limit);
|
|
2964
|
+
}
|
|
2965
|
+
async function setDiscoveryStatus(db, name, data) {
|
|
2966
|
+
await db.update(discoveryQueue).set({ status: data.status, ...data.preScore !== void 0 ? { preScore: data.preScore } : {} }).where(eq6(discoveryQueue.name, name));
|
|
2967
|
+
}
|
|
2968
|
+
var init_discovery = __esm({
|
|
2969
|
+
"src/db/discovery.ts"() {
|
|
2970
|
+
"use strict";
|
|
2971
|
+
init_esm_shims();
|
|
2972
|
+
init_schema();
|
|
2973
|
+
}
|
|
2974
|
+
});
|
|
2975
|
+
|
|
2976
|
+
// src/pipeline/discovery.ts
|
|
2977
|
+
import { isNotNull as isNotNull4 } from "drizzle-orm";
|
|
2978
|
+
function selectCandidates(raw, known) {
|
|
2979
|
+
const seen = new Set(known);
|
|
2980
|
+
const out = [];
|
|
2981
|
+
for (const c of raw) {
|
|
2982
|
+
const name = c.name?.trim();
|
|
2983
|
+
if (!name || seen.has(name)) continue;
|
|
2984
|
+
seen.add(name);
|
|
2985
|
+
out.push({ name, via: c.via });
|
|
2986
|
+
}
|
|
2987
|
+
return out;
|
|
2988
|
+
}
|
|
2989
|
+
function passesGate(preScore) {
|
|
2990
|
+
return preScore !== null && preScore >= DISCOVERY.minPreScore;
|
|
2991
|
+
}
|
|
2992
|
+
async function preScorePackage(name, fetchImpl) {
|
|
2993
|
+
try {
|
|
2994
|
+
const registry = await fetchNpmRegistry(name, fetchImpl);
|
|
2995
|
+
const signals = {
|
|
2996
|
+
name,
|
|
2997
|
+
registry,
|
|
2998
|
+
downloads: null,
|
|
2999
|
+
github: null,
|
|
3000
|
+
depsDev: null,
|
|
3001
|
+
bundle: null,
|
|
3002
|
+
errors: []
|
|
3003
|
+
};
|
|
3004
|
+
return computeQuality(toScoringInput(signals, null));
|
|
3005
|
+
} catch {
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
async function graphChannel(db) {
|
|
3010
|
+
const tracked = await db.select({ name: packages.name, version: packages.latestVersion }).from(packages).where(isNotNull4(packages.latestVersion));
|
|
3011
|
+
const out = [];
|
|
3012
|
+
for (const t of tracked) {
|
|
3013
|
+
if (!t.version) continue;
|
|
3014
|
+
const deps = (await fetchDependencyNames(t.name, t.version)).slice(
|
|
3015
|
+
0,
|
|
3016
|
+
DISCOVERY.graphNeighborsPerSeed
|
|
3017
|
+
);
|
|
3018
|
+
for (const name of deps) out.push({ name, via: "dependency-graph" });
|
|
3019
|
+
}
|
|
3020
|
+
return out;
|
|
3021
|
+
}
|
|
3022
|
+
async function searchChannel() {
|
|
3023
|
+
const out = [];
|
|
3024
|
+
for (const category of CATEGORIES) {
|
|
3025
|
+
if (category === "other") continue;
|
|
3026
|
+
const hits = await searchNpm(`keywords:${category}`, DISCOVERY.searchSizePerCategory);
|
|
3027
|
+
for (const hit of hits) {
|
|
3028
|
+
const via = isRecent(hit.date) ? "recent" : "category-search";
|
|
3029
|
+
out.push({ name: hit.name, via });
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return out;
|
|
3033
|
+
}
|
|
3034
|
+
function isRecent(date) {
|
|
3035
|
+
if (!date) return false;
|
|
3036
|
+
const ms = Date.parse(date);
|
|
3037
|
+
if (Number.isNaN(ms)) return false;
|
|
3038
|
+
return Date.now() - ms <= 90 * 24 * 60 * 60 * 1e3;
|
|
3039
|
+
}
|
|
3040
|
+
async function runDiscovery(opts = {}) {
|
|
3041
|
+
const cap = opts.perRunCap ?? DISCOVERY.perRunCap;
|
|
3042
|
+
const handle = createDb({ max: 6 });
|
|
3043
|
+
try {
|
|
3044
|
+
logger.info("Discovery: gathering candidates from graph + search channels\u2026");
|
|
3045
|
+
const [graph, search] = await Promise.all([graphChannel(handle.db), searchChannel()]);
|
|
3046
|
+
const known = await getKnownNames(handle.db);
|
|
3047
|
+
const fresh = selectCandidates([...graph, ...search], known);
|
|
3048
|
+
const enqueued = await enqueueCandidates(handle.db, fresh);
|
|
3049
|
+
logger.info(
|
|
3050
|
+
`Discovery: ${graph.length} graph + ${search.length} search candidates \u2192 ${enqueued} new queued.`
|
|
3051
|
+
);
|
|
3052
|
+
const pending = await getPendingCandidates(handle.db, cap * 4);
|
|
3053
|
+
const scored = [];
|
|
3054
|
+
for (const cand of pending) {
|
|
3055
|
+
const preScore = await preScorePackage(cand.name);
|
|
3056
|
+
await setDiscoveryStatus(handle.db, cand.name, {
|
|
3057
|
+
status: passesGate(preScore) ? "pending" : "rejected",
|
|
3058
|
+
preScore: preScore ?? null
|
|
3059
|
+
});
|
|
3060
|
+
if (passesGate(preScore)) scored.push({ name: cand.name, preScore });
|
|
3061
|
+
}
|
|
3062
|
+
logger.info(`Discovery: gated ${pending.length}; ${scored.length} cleared the quality bar.`);
|
|
3063
|
+
scored.sort((a, b) => b.preScore - a.preScore);
|
|
3064
|
+
const toIngest = scored.slice(0, cap);
|
|
3065
|
+
const deferred = scored.slice(cap);
|
|
3066
|
+
if (deferred.length > 0) {
|
|
3067
|
+
logger.info(
|
|
3068
|
+
`Discovery: per-run cap ${cap} reached \u2014 ${deferred.length} eligible candidate(s) deferred to the next run: ${deferred.map((d) => d.name).join(", ")}`
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
let ingested = 0;
|
|
3072
|
+
if (!opts.dryRun) {
|
|
3073
|
+
for (const cand of toIngest) {
|
|
3074
|
+
try {
|
|
3075
|
+
await syncOnePackage(handle.db, cand.name);
|
|
3076
|
+
await setDiscoveryStatus(handle.db, cand.name, { status: "ingested" });
|
|
3077
|
+
ingested++;
|
|
3078
|
+
} catch (err) {
|
|
3079
|
+
logger.warn(`Discovery: failed to ingest ${cand.name}: ${err.message}`);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
logger.info(`Discovery: ingested ${ingested}/${toIngest.length} candidate(s).`);
|
|
3084
|
+
return {
|
|
3085
|
+
enqueued,
|
|
3086
|
+
gated: pending.length,
|
|
3087
|
+
passed: scored.length,
|
|
3088
|
+
ingested,
|
|
3089
|
+
droppedToNextRun: deferred.length
|
|
3090
|
+
};
|
|
3091
|
+
} finally {
|
|
3092
|
+
await handle.close();
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
var init_discovery2 = __esm({
|
|
3096
|
+
"src/pipeline/discovery.ts"() {
|
|
3097
|
+
"use strict";
|
|
3098
|
+
init_esm_shims();
|
|
3099
|
+
init_logger();
|
|
3100
|
+
init_types();
|
|
3101
|
+
init_client();
|
|
3102
|
+
init_discovery();
|
|
3103
|
+
init_schema();
|
|
3104
|
+
init_depsDev();
|
|
3105
|
+
init_npmRegistry();
|
|
3106
|
+
init_npmSearch();
|
|
3107
|
+
init_scoring();
|
|
3108
|
+
init_weights();
|
|
3109
|
+
init_single();
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
// src/pipeline/index.ts
|
|
3114
|
+
var pipeline_exports = {};
|
|
3115
|
+
__export(pipeline_exports, {
|
|
3116
|
+
runDiscovery: () => runDiscovery,
|
|
3117
|
+
runRescore: () => runRescore,
|
|
3118
|
+
runSync: () => runSync
|
|
3119
|
+
});
|
|
3120
|
+
var init_pipeline = __esm({
|
|
3121
|
+
"src/pipeline/index.ts"() {
|
|
3122
|
+
"use strict";
|
|
3123
|
+
init_esm_shims();
|
|
3124
|
+
init_sync();
|
|
3125
|
+
init_rescore();
|
|
3126
|
+
init_discovery2();
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
|
|
3130
|
+
// src/cli/format.ts
|
|
3131
|
+
function table(headers, rows) {
|
|
3132
|
+
const widths = headers.map((h, i) => Math.max(width(h), ...rows.map((r) => width(r[i] ?? ""))));
|
|
3133
|
+
const pad = (s, w) => s + " ".repeat(Math.max(0, w - width(s)));
|
|
3134
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
|
|
3135
|
+
const out = [bold(line(headers)), dim(widths.map((w) => "\u2500".repeat(w)).join(" "))];
|
|
3136
|
+
for (const r of rows) out.push(line(r));
|
|
3137
|
+
return out.join("\n");
|
|
2273
3138
|
}
|
|
2274
3139
|
function detail(pairs) {
|
|
2275
3140
|
const labelWidth = Math.max(...pairs.map(([k]) => k.length));
|
|
@@ -2292,6 +3157,7 @@ function formatPercent(fraction) {
|
|
|
2292
3157
|
function confidenceLabel(c) {
|
|
2293
3158
|
if (c === "proven") return green(c);
|
|
2294
3159
|
if (c === "emerging") return yellow(c);
|
|
3160
|
+
if (c === "promising") return green(c);
|
|
2295
3161
|
return red(c);
|
|
2296
3162
|
}
|
|
2297
3163
|
var ESC, wrap, bold, dim, red, green, yellow, ANSI_RE, width;
|
|
@@ -2315,9 +3181,11 @@ var init_format = __esm({
|
|
|
2315
3181
|
var commands_exports = {};
|
|
2316
3182
|
__export(commands_exports, {
|
|
2317
3183
|
runCompare: () => runCompare,
|
|
3184
|
+
runEditWeights: () => runEditWeights,
|
|
2318
3185
|
runEvaluate: () => runEvaluate,
|
|
2319
3186
|
runRecommend: () => runRecommend,
|
|
2320
|
-
runVerify: () => runVerify
|
|
3187
|
+
runVerify: () => runVerify,
|
|
3188
|
+
runWeights: () => runWeights
|
|
2321
3189
|
});
|
|
2322
3190
|
async function withDb(fn) {
|
|
2323
3191
|
requireConfig(["DATABASE_URL"]);
|
|
@@ -2345,10 +3213,11 @@ async function runRecommend(need, opts) {
|
|
|
2345
3213
|
}
|
|
2346
3214
|
console.log(
|
|
2347
3215
|
table(
|
|
2348
|
-
["Package", "Health", "Confidence", "Weekly", "Latest", "Category"],
|
|
3216
|
+
["Package", "Health", "Quality", "Confidence", "Weekly", "Latest", "Category"],
|
|
2349
3217
|
res.candidates.map((c) => [
|
|
2350
3218
|
c.name,
|
|
2351
3219
|
String(c.healthScore),
|
|
3220
|
+
c.qualityScore != null ? String(c.qualityScore) : "\u2014",
|
|
2352
3221
|
confidenceLabel(c.confidence),
|
|
2353
3222
|
formatNumber(c.weeklyDownloads),
|
|
2354
3223
|
c.latestVersion ?? "\u2014",
|
|
@@ -2373,9 +3242,10 @@ async function runEvaluate(pkg, opts) {
|
|
|
2373
3242
|
console.log(
|
|
2374
3243
|
detail([
|
|
2375
3244
|
["health", `${res.healthScore} ${confidenceLabel(res.confidence)}`],
|
|
3245
|
+
["quality", res.qualityScore != null ? String(res.qualityScore) : "\u2014"],
|
|
2376
3246
|
[
|
|
2377
3247
|
"breakdown",
|
|
2378
|
-
`maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"}`
|
|
3248
|
+
`maint ${b.maintenance} \xB7 adopt ${b.adoption} \xB7 rel ${b.reliability} \xB7 eff ${b.efficiency ?? "\u2014"} \xB7 qual ${b.quality ?? "\u2014"}`
|
|
2379
3249
|
],
|
|
2380
3250
|
["version", res.latestVersion ?? "\u2014"],
|
|
2381
3251
|
["weekly dl", `${formatNumber(res.weeklyDownloads)} (${formatPercent(res.downloadGrowth90d)} 90d)`],
|
|
@@ -2429,6 +3299,72 @@ not found: ${res.missing.join(", ")}`));
|
|
|
2429
3299
|
console.log(dim(`data as of ${formatDate(res.dataAsOf)}`));
|
|
2430
3300
|
});
|
|
2431
3301
|
}
|
|
3302
|
+
function runWeights(opts = {}) {
|
|
3303
|
+
const w = loadWeights();
|
|
3304
|
+
const active = activeWeightsPath();
|
|
3305
|
+
if (opts.json) {
|
|
3306
|
+
console.log(JSON.stringify({ ...w, source: active?.source ?? "defaults" }, null, 2));
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
const pct = (n) => n.toFixed(2);
|
|
3310
|
+
console.log(bold("Two axes, blended for default sort:"));
|
|
3311
|
+
console.log(` composite = (1\u2212\u03BB)\xB7health + \u03BB\xB7quality \u03BB = ${pct(w.composite.lambda)}
|
|
3312
|
+
`);
|
|
3313
|
+
console.log(bold("Health (proven-ness) \u2014 weighted sum of 4 components:"));
|
|
3314
|
+
console.log(
|
|
3315
|
+
detail([
|
|
3316
|
+
["maintenance", `${pct(w.health.maintenance)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.maintenance)}`],
|
|
3317
|
+
["adoption", `${pct(w.health.adoption)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.adoption)}`],
|
|
3318
|
+
["reliability", `${pct(w.health.reliability)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.reliability)}`],
|
|
3319
|
+
["efficiency", `${pct(w.health.efficiency)} ${dim("\u2014 " + WEIGHT_EXPLANATIONS.efficiency)}`]
|
|
3320
|
+
])
|
|
3321
|
+
);
|
|
3322
|
+
console.log("\n" + bold("Quality (intrinsic, adoption-independent):"));
|
|
3323
|
+
console.log(" " + dim(Object.keys(QUALITY_WEIGHTS).join(", ")));
|
|
3324
|
+
console.log("\n" + bold("Confidence thresholds:"));
|
|
3325
|
+
console.log(
|
|
3326
|
+
detail([
|
|
3327
|
+
["proven", `\u2265 ${formatNumber(CONFIDENCE.proven.minWeeklyDownloads)} weekly dl, \u2265 ${CONFIDENCE.proven.minAgeMonths}mo old`],
|
|
3328
|
+
["emerging", `\u2265 ${formatNumber(CONFIDENCE.emerging.minWeeklyDownloads)} weekly dl OR \u2265 ${CONFIDENCE.emerging.strongGrowth * 100}% 90d growth`],
|
|
3329
|
+
["promising", `\u2265 ${CONFIDENCE.promising.minQuality} quality score (adoption-independent)`]
|
|
3330
|
+
])
|
|
3331
|
+
);
|
|
3332
|
+
console.log(dim(`
|
|
3333
|
+
Source: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
|
|
3334
|
+
}
|
|
3335
|
+
function runEditWeights(opts) {
|
|
3336
|
+
if (opts.reset) {
|
|
3337
|
+
const removed = resetWeights();
|
|
3338
|
+
console.log(removed.length ? `Removed overrides:
|
|
3339
|
+
${removed.join("\n ")}` : "No overrides to remove; already on defaults.");
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
if (opts.explain) {
|
|
3343
|
+
const key = opts.explain;
|
|
3344
|
+
const text2 = WEIGHT_EXPLANATIONS[key];
|
|
3345
|
+
if (!text2) {
|
|
3346
|
+
throw new Error(`No explanation for "${key}". Known: ${Object.keys(WEIGHT_EXPLANATIONS).join(", ")}.`);
|
|
3347
|
+
}
|
|
3348
|
+
console.log(`${bold(key)} \u2014 ${text2}`);
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
if (opts.set && opts.set.length > 0) {
|
|
3352
|
+
const next = applyOverrides(loadWeights(), opts.set);
|
|
3353
|
+
const { weights, normalized } = validateWeights(next);
|
|
3354
|
+
const path2 = saveWeights(weights, opts.project ? "project" : "user");
|
|
3355
|
+
console.log(`Saved overrides to ${path2}`);
|
|
3356
|
+
if (normalized) {
|
|
3357
|
+
console.log(
|
|
3358
|
+
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)}`
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
console.log(dim("\nRun `lurq rescore` to apply the new health weights to the stored index."));
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
console.log(dim(`No changes. Settable keys: ${settableKeys().join(", ")}.
|
|
3365
|
+
`));
|
|
3366
|
+
runWeights(opts);
|
|
3367
|
+
}
|
|
2432
3368
|
async function runVerify(pkg, opts) {
|
|
2433
3369
|
await withDb(async (db) => {
|
|
2434
3370
|
const res = await handleVerify(db, { package: pkg });
|
|
@@ -2454,57 +3390,33 @@ var init_commands = __esm({
|
|
|
2454
3390
|
init_types();
|
|
2455
3391
|
init_client();
|
|
2456
3392
|
init_handlers();
|
|
3393
|
+
init_weights();
|
|
3394
|
+
init_weights();
|
|
2457
3395
|
init_format();
|
|
2458
3396
|
}
|
|
2459
3397
|
});
|
|
2460
3398
|
|
|
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
3399
|
// src/cli/installSkill.ts
|
|
2495
3400
|
var installSkill_exports = {};
|
|
2496
3401
|
__export(installSkill_exports, {
|
|
2497
3402
|
SUPPORTED_AGENTS: () => SUPPORTED_AGENTS,
|
|
3403
|
+
agentSpecs: () => agentSpecs,
|
|
3404
|
+
buildRemoteServerEntry: () => buildRemoteServerEntry,
|
|
3405
|
+
buildRemoteTomlBlock: () => buildRemoteTomlBlock,
|
|
2498
3406
|
buildServerEntry: () => buildServerEntry,
|
|
2499
3407
|
buildTomlBlock: () => buildTomlBlock,
|
|
3408
|
+
installAgent: () => installAgent,
|
|
3409
|
+
installInstructionsFile: () => installInstructionsFile,
|
|
3410
|
+
printInstallReport: () => printInstallReport,
|
|
3411
|
+
resolveAgents: () => resolveAgents,
|
|
2500
3412
|
runInstallSkill: () => runInstallSkill
|
|
2501
3413
|
});
|
|
2502
|
-
import { existsSync as
|
|
3414
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2503
3415
|
import { copyFileSync } from "fs";
|
|
2504
|
-
import { homedir as
|
|
2505
|
-
import { dirname as
|
|
3416
|
+
import { homedir as homedir3 } from "os";
|
|
3417
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
2506
3418
|
function home(...p) {
|
|
2507
|
-
return join3(
|
|
3419
|
+
return join3(homedir3(), ...p);
|
|
2508
3420
|
}
|
|
2509
3421
|
function agentSpecs() {
|
|
2510
3422
|
return [
|
|
@@ -2513,35 +3425,35 @@ function agentSpecs() {
|
|
|
2513
3425
|
label: "Claude Code",
|
|
2514
3426
|
format: "mcpServers",
|
|
2515
3427
|
path: home(".claude.json"),
|
|
2516
|
-
detected:
|
|
3428
|
+
detected: existsSync4(home(".claude.json")) || existsSync4(home(".claude"))
|
|
2517
3429
|
},
|
|
2518
3430
|
{
|
|
2519
3431
|
id: "cursor",
|
|
2520
3432
|
label: "Cursor",
|
|
2521
3433
|
format: "mcpServers",
|
|
2522
3434
|
path: home(".cursor", "mcp.json"),
|
|
2523
|
-
detected:
|
|
3435
|
+
detected: existsSync4(home(".cursor"))
|
|
2524
3436
|
},
|
|
2525
3437
|
{
|
|
2526
3438
|
id: "windsurf",
|
|
2527
3439
|
label: "Windsurf",
|
|
2528
3440
|
format: "mcpServers",
|
|
2529
3441
|
path: home(".codeium", "windsurf", "mcp_config.json"),
|
|
2530
|
-
detected:
|
|
3442
|
+
detected: existsSync4(home(".codeium"))
|
|
2531
3443
|
},
|
|
2532
3444
|
{
|
|
2533
3445
|
id: "copilot",
|
|
2534
3446
|
label: "VS Code / GitHub Copilot",
|
|
2535
3447
|
format: "servers",
|
|
2536
3448
|
path: home("Library", "Application Support", "Code", "User", "mcp.json"),
|
|
2537
|
-
detected:
|
|
3449
|
+
detected: existsSync4(home("Library", "Application Support", "Code", "User"))
|
|
2538
3450
|
},
|
|
2539
3451
|
{
|
|
2540
3452
|
id: "codex",
|
|
2541
3453
|
label: "OpenAI Codex CLI",
|
|
2542
3454
|
format: "toml",
|
|
2543
3455
|
path: home(".codex", "config.toml"),
|
|
2544
|
-
detected:
|
|
3456
|
+
detected: existsSync4(home(".codex"))
|
|
2545
3457
|
}
|
|
2546
3458
|
];
|
|
2547
3459
|
}
|
|
@@ -2554,14 +3466,14 @@ function collectEnv() {
|
|
|
2554
3466
|
return env;
|
|
2555
3467
|
}
|
|
2556
3468
|
function readJsonObject(path2) {
|
|
2557
|
-
if (!
|
|
2558
|
-
const text2 =
|
|
3469
|
+
if (!existsSync4(path2)) return {};
|
|
3470
|
+
const text2 = readFileSync2(path2, "utf8").trim();
|
|
2559
3471
|
if (!text2) return {};
|
|
2560
3472
|
return JSON.parse(text2);
|
|
2561
3473
|
}
|
|
2562
3474
|
function writeJson(path2, obj) {
|
|
2563
|
-
|
|
2564
|
-
|
|
3475
|
+
mkdirSync2(dirname4(path2), { recursive: true });
|
|
3476
|
+
writeFileSync2(path2, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
2565
3477
|
}
|
|
2566
3478
|
function buildServerEntry(env, withType) {
|
|
2567
3479
|
const entry = { command: "npx", args: ["-y", PACKAGE_NAME, "serve"] };
|
|
@@ -2569,13 +3481,18 @@ function buildServerEntry(env, withType) {
|
|
|
2569
3481
|
if (Object.keys(env).length) entry.env = env;
|
|
2570
3482
|
return entry;
|
|
2571
3483
|
}
|
|
2572
|
-
function
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
3484
|
+
function buildRemoteServerEntry(agentId, opts) {
|
|
3485
|
+
const headers = { Authorization: `Bearer ${opts.apiKey}` };
|
|
3486
|
+
switch (agentId) {
|
|
3487
|
+
case "cursor":
|
|
3488
|
+
return { url: opts.url, headers };
|
|
3489
|
+
case "windsurf":
|
|
3490
|
+
return { serverUrl: opts.url, headers };
|
|
3491
|
+
case "claude-code":
|
|
3492
|
+
case "copilot":
|
|
3493
|
+
default:
|
|
3494
|
+
return { type: "http", url: opts.url, headers };
|
|
3495
|
+
}
|
|
2579
3496
|
}
|
|
2580
3497
|
function buildTomlBlock(env) {
|
|
2581
3498
|
const lines = ["[mcp_servers.lurq]", 'command = "npx"', `args = ["-y", "${PACKAGE_NAME}", "serve"]`];
|
|
@@ -2585,8 +3502,23 @@ function buildTomlBlock(env) {
|
|
|
2585
3502
|
}
|
|
2586
3503
|
return lines.join("\n") + "\n";
|
|
2587
3504
|
}
|
|
2588
|
-
function
|
|
2589
|
-
|
|
3505
|
+
function buildRemoteTomlBlock(opts) {
|
|
3506
|
+
return [
|
|
3507
|
+
"[mcp_servers.lurq]",
|
|
3508
|
+
`url = ${JSON.stringify(opts.url)}`,
|
|
3509
|
+
`http_headers = { Authorization = ${JSON.stringify(`Bearer ${opts.apiKey}`)} }`
|
|
3510
|
+
].join("\n") + "\n";
|
|
3511
|
+
}
|
|
3512
|
+
function installJsonEntry(spec, entry) {
|
|
3513
|
+
const key = spec.format === "servers" ? "servers" : "mcpServers";
|
|
3514
|
+
const config = readJsonObject(spec.path);
|
|
3515
|
+
if (typeof config[key] !== "object" || config[key] === null) config[key] = {};
|
|
3516
|
+
config[key].lurq = entry;
|
|
3517
|
+
writeJson(spec.path, config);
|
|
3518
|
+
return { agent: spec.id, path: spec.path, status: "installed" };
|
|
3519
|
+
}
|
|
3520
|
+
function installTomlBlock(spec, block) {
|
|
3521
|
+
const existing = existsSync4(spec.path) ? readFileSync2(spec.path, "utf8") : "";
|
|
2590
3522
|
if (existing.includes("[mcp_servers.lurq]")) {
|
|
2591
3523
|
return {
|
|
2592
3524
|
agent: spec.id,
|
|
@@ -2595,59 +3527,48 @@ function installToml(spec, env) {
|
|
|
2595
3527
|
message: "lurq already present; edit manually to change it."
|
|
2596
3528
|
};
|
|
2597
3529
|
}
|
|
2598
|
-
|
|
3530
|
+
mkdirSync2(dirname4(spec.path), { recursive: true });
|
|
2599
3531
|
const sep = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
|
|
2600
|
-
|
|
3532
|
+
writeFileSync2(spec.path, existing + sep + block, "utf8");
|
|
2601
3533
|
return { agent: spec.id, path: spec.path, status: "installed" };
|
|
2602
3534
|
}
|
|
3535
|
+
function installAgent(spec, mode) {
|
|
3536
|
+
try {
|
|
3537
|
+
if (spec.format === "toml") {
|
|
3538
|
+
const block = mode.kind === "remote" ? buildRemoteTomlBlock(mode) : buildTomlBlock(mode.env);
|
|
3539
|
+
return installTomlBlock(spec, block);
|
|
3540
|
+
}
|
|
3541
|
+
const entry = mode.kind === "remote" ? buildRemoteServerEntry(spec.id, mode) : buildServerEntry(mode.env, spec.format === "servers");
|
|
3542
|
+
return installJsonEntry(spec, entry);
|
|
3543
|
+
} catch (err) {
|
|
3544
|
+
return {
|
|
3545
|
+
agent: spec.id,
|
|
3546
|
+
path: spec.path,
|
|
3547
|
+
status: "error",
|
|
3548
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
2603
3552
|
function installInstructionsFile() {
|
|
2604
3553
|
const src = join3(packageRoot(), "templates", "skill-instructions.md");
|
|
2605
|
-
if (!
|
|
3554
|
+
if (!existsSync4(src)) return null;
|
|
2606
3555
|
const destDir = home(".lurq");
|
|
2607
3556
|
const dest = join3(destDir, "skill-instructions.md");
|
|
2608
|
-
|
|
3557
|
+
mkdirSync2(destDir, { recursive: true });
|
|
2609
3558
|
copyFileSync(src, dest);
|
|
2610
3559
|
return dest;
|
|
2611
3560
|
}
|
|
2612
|
-
|
|
2613
|
-
const target = opts.agent ?? "claude-code";
|
|
3561
|
+
function resolveAgents(target) {
|
|
2614
3562
|
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
|
-
}
|
|
3563
|
+
if (target === "all") return specs.filter((s) => s.detected);
|
|
3564
|
+
const spec = specs.find((s) => s.id === target);
|
|
3565
|
+
if (!spec) {
|
|
3566
|
+
throw new Error(`Unknown agent "${target}". Supported: ${SUPPORTED_AGENTS.join(", ")}, all.`);
|
|
2649
3567
|
}
|
|
2650
|
-
|
|
3568
|
+
return [spec];
|
|
3569
|
+
}
|
|
3570
|
+
function printInstallReport(results, instructionsPath, mode) {
|
|
3571
|
+
const specs = agentSpecs();
|
|
2651
3572
|
console.log("lurq MCP server registration:");
|
|
2652
3573
|
for (const r of results) {
|
|
2653
3574
|
const spec = specs.find((s) => s.id === r.agent);
|
|
@@ -2657,9 +3578,43 @@ async function runInstallSkill(opts) {
|
|
|
2657
3578
|
if (instructionsPath) console.log(`
|
|
2658
3579
|
Skill instructions written to ${instructionsPath}`);
|
|
2659
3580
|
console.log("\nNext steps:");
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
3581
|
+
if (mode.kind === "local") {
|
|
3582
|
+
console.log(" 1. Ensure DATABASE_URL (and any API keys) are set in the config env above.");
|
|
3583
|
+
console.log(" 2. Restart the agent so it picks up the new MCP server.");
|
|
3584
|
+
} else {
|
|
3585
|
+
console.log(" 1. Restart the agent so it picks up the new MCP server.");
|
|
3586
|
+
}
|
|
3587
|
+
console.log(" \u2022 Ask it to recommend a library \u2014 it should call lurq.");
|
|
3588
|
+
}
|
|
3589
|
+
async function runInstallSkill(opts) {
|
|
3590
|
+
const selected = resolveAgents(opts.agent ?? "claude-code");
|
|
3591
|
+
if (selected.length === 0) {
|
|
3592
|
+
console.log("No supported agents detected on this machine.");
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
const remote = !opts.local;
|
|
3596
|
+
let mode;
|
|
3597
|
+
if (remote) {
|
|
3598
|
+
const apiKey = opts.apiKey ?? process.env.LURQ_API_KEY;
|
|
3599
|
+
if (!apiKey) {
|
|
3600
|
+
throw new Error(
|
|
3601
|
+
"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."
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
3605
|
+
mode = { kind: "remote", url, apiKey };
|
|
3606
|
+
} else {
|
|
3607
|
+
const env = collectEnv();
|
|
3608
|
+
if (!env.DATABASE_URL) {
|
|
3609
|
+
logger.warn(
|
|
3610
|
+
"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."
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
mode = { kind: "local", env };
|
|
3614
|
+
}
|
|
3615
|
+
const results = selected.map((spec) => installAgent(spec, mode));
|
|
3616
|
+
const instructionsPath = installInstructionsFile();
|
|
3617
|
+
printInstallReport(results, instructionsPath, mode);
|
|
2663
3618
|
}
|
|
2664
3619
|
var ENV_KEYS, SUPPORTED_AGENTS;
|
|
2665
3620
|
var init_installSkill = __esm({
|
|
@@ -2683,12 +3638,228 @@ var init_installSkill = __esm({
|
|
|
2683
3638
|
}
|
|
2684
3639
|
});
|
|
2685
3640
|
|
|
3641
|
+
// src/cli/install.ts
|
|
3642
|
+
var install_exports = {};
|
|
3643
|
+
__export(install_exports, {
|
|
3644
|
+
runInstallWizard: () => runInstallWizard
|
|
3645
|
+
});
|
|
3646
|
+
async function validateKey(url, apiKey) {
|
|
3647
|
+
try {
|
|
3648
|
+
const res = await fetch(url, {
|
|
3649
|
+
method: "POST",
|
|
3650
|
+
headers: {
|
|
3651
|
+
"Content-Type": "application/json",
|
|
3652
|
+
Accept: "application/json, text/event-stream",
|
|
3653
|
+
Authorization: `Bearer ${apiKey}`
|
|
3654
|
+
},
|
|
3655
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
|
|
3656
|
+
});
|
|
3657
|
+
return res.ok;
|
|
3658
|
+
} catch {
|
|
3659
|
+
return false;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
async function runInstallWizard(opts) {
|
|
3663
|
+
const interactive = !opts.yes;
|
|
3664
|
+
const url = opts.url ?? process.env.LURQ_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
3665
|
+
let apiKey = (opts.apiKey ?? process.env.LURQ_API_KEY)?.trim();
|
|
3666
|
+
if (interactive) {
|
|
3667
|
+
const { input, checkbox, confirm } = await import("@inquirer/prompts");
|
|
3668
|
+
console.log("\n lurq \u2014 connect your coding agent to the hosted package index.\n");
|
|
3669
|
+
if (!apiKey) {
|
|
3670
|
+
console.log(` Need a key? Get one at ${GET_KEY_URL}
|
|
3671
|
+
`);
|
|
3672
|
+
apiKey = (await input({
|
|
3673
|
+
message: "Paste your lurq API key",
|
|
3674
|
+
validate: (v) => v.trim().startsWith("lurq_") ? true : "Keys look like lurq_live_\u2026 "
|
|
3675
|
+
})).trim();
|
|
3676
|
+
}
|
|
3677
|
+
process.stdout.write(" Validating key\u2026 ");
|
|
3678
|
+
const ok = await validateKey(url, apiKey);
|
|
3679
|
+
console.log(ok ? "ok" : "could not reach endpoint");
|
|
3680
|
+
if (!ok) {
|
|
3681
|
+
const proceed = await confirm({
|
|
3682
|
+
message: `Couldn't validate the key against ${url}. Continue anyway?`,
|
|
3683
|
+
default: false
|
|
3684
|
+
});
|
|
3685
|
+
if (!proceed) {
|
|
3686
|
+
console.log("Aborted. No config was changed.");
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
let selected2;
|
|
3691
|
+
if (opts.agent) {
|
|
3692
|
+
selected2 = resolveAgents(opts.agent);
|
|
3693
|
+
} else {
|
|
3694
|
+
const specs = agentSpecs();
|
|
3695
|
+
const ids = await checkbox({
|
|
3696
|
+
message: "Which assistant(s) should I configure?",
|
|
3697
|
+
choices: specs.map((s) => ({
|
|
3698
|
+
name: `${s.label}${s.detected ? " (detected)" : ""}`,
|
|
3699
|
+
value: s.id,
|
|
3700
|
+
checked: s.detected
|
|
3701
|
+
}))
|
|
3702
|
+
});
|
|
3703
|
+
selected2 = specs.filter((s) => ids.includes(s.id));
|
|
3704
|
+
}
|
|
3705
|
+
await finish(selected2, { url, apiKey });
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
if (!apiKey) {
|
|
3709
|
+
throw new Error(
|
|
3710
|
+
"No API key. Pass --api-key <key> or set LURQ_API_KEY (or drop --yes to be prompted)."
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
const selected = opts.agent ? resolveAgents(opts.agent) : agentSpecs().filter((s) => s.detected);
|
|
3714
|
+
await finish(selected, { url, apiKey });
|
|
3715
|
+
}
|
|
3716
|
+
async function finish(selected, remote) {
|
|
3717
|
+
if (selected.length === 0) {
|
|
3718
|
+
console.log(
|
|
3719
|
+
"\nNo agents selected or detected. Re-run with --agent <id>, or install an assistant first."
|
|
3720
|
+
);
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
const mode = { kind: "remote", ...remote };
|
|
3724
|
+
const results = selected.map((s) => installAgent(s, mode));
|
|
3725
|
+
const instructionsPath = installInstructionsFile();
|
|
3726
|
+
console.log("");
|
|
3727
|
+
printInstallReport(results, instructionsPath, mode);
|
|
3728
|
+
}
|
|
3729
|
+
var GET_KEY_URL;
|
|
3730
|
+
var init_install = __esm({
|
|
3731
|
+
"src/cli/install.ts"() {
|
|
3732
|
+
"use strict";
|
|
3733
|
+
init_esm_shims();
|
|
3734
|
+
init_constants();
|
|
3735
|
+
init_installSkill();
|
|
3736
|
+
GET_KEY_URL = "https://lurq.run";
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3739
|
+
|
|
3740
|
+
// src/cli/keys.ts
|
|
3741
|
+
var keys_exports = {};
|
|
3742
|
+
__export(keys_exports, {
|
|
3743
|
+
runKeysCreate: () => runKeysCreate,
|
|
3744
|
+
runKeysList: () => runKeysList,
|
|
3745
|
+
runKeysRevoke: () => runKeysRevoke
|
|
3746
|
+
});
|
|
3747
|
+
function waitForEnter(prompt) {
|
|
3748
|
+
return new Promise((resolve) => {
|
|
3749
|
+
process.stdout.write(prompt);
|
|
3750
|
+
process.stdin.resume();
|
|
3751
|
+
process.stdin.once("data", () => {
|
|
3752
|
+
process.stdin.pause();
|
|
3753
|
+
resolve();
|
|
3754
|
+
});
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
async function runKeysCreate(opts) {
|
|
3758
|
+
requireConfig(["DATABASE_URL"]);
|
|
3759
|
+
const { db, close } = createDb({ max: 1 });
|
|
3760
|
+
try {
|
|
3761
|
+
const { key, row } = await createKey(db, { label: opts.label, tier: opts.tier });
|
|
3762
|
+
if (opts.json) {
|
|
3763
|
+
console.log(JSON.stringify({ key, prefix: row.prefix, tier: row.tier, label: row.label }));
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
const meta = `prefix=${row.prefix} tier=${row.tier}${row.label ? ` label=${row.label}` : ""}`;
|
|
3767
|
+
const block = [bold("API key created."), "", ` ${green(key)}`, "", dim(meta)];
|
|
3768
|
+
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
3769
|
+
console.log(block.join("\n"));
|
|
3770
|
+
if (interactive) {
|
|
3771
|
+
await waitForEnter(dim("Copy it now, then press Enter to erase it from the terminal\u2026 "));
|
|
3772
|
+
process.stdout.write(`\x1B[${block.length + 1}F\x1B[0J\x1B[3J`);
|
|
3773
|
+
console.log(
|
|
3774
|
+
dim(`API key created (prefix ${row.prefix}) \u2014 value erased from the terminal. It is stored only as a hash and cannot be recovered, so make sure you saved it.`)
|
|
3775
|
+
);
|
|
3776
|
+
} else {
|
|
3777
|
+
console.log(dim("Store it now \u2014 shown only once, stored hashed, cannot be recovered."));
|
|
3778
|
+
}
|
|
3779
|
+
} finally {
|
|
3780
|
+
await close();
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
async function runKeysList(opts) {
|
|
3784
|
+
requireConfig(["DATABASE_URL"]);
|
|
3785
|
+
const { db, close } = createDb({ max: 1 });
|
|
3786
|
+
try {
|
|
3787
|
+
const rows = await listKeys(db);
|
|
3788
|
+
if (opts.json) {
|
|
3789
|
+
console.log(
|
|
3790
|
+
JSON.stringify(
|
|
3791
|
+
rows.map((r) => ({
|
|
3792
|
+
id: r.id,
|
|
3793
|
+
prefix: r.prefix,
|
|
3794
|
+
label: r.label,
|
|
3795
|
+
tier: r.tier,
|
|
3796
|
+
ownerId: r.ownerId,
|
|
3797
|
+
createdAt: r.createdAt,
|
|
3798
|
+
lastUsedAt: r.lastUsedAt,
|
|
3799
|
+
revokedAt: r.revokedAt
|
|
3800
|
+
})),
|
|
3801
|
+
null,
|
|
3802
|
+
2
|
|
3803
|
+
)
|
|
3804
|
+
);
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3807
|
+
if (rows.length === 0) {
|
|
3808
|
+
console.log("No API keys yet. Create one with `lurq keys create --label <name>`.");
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
console.log(
|
|
3812
|
+
table(
|
|
3813
|
+
["prefix", "label", "tier", "created", "last used", "status"],
|
|
3814
|
+
rows.map((r) => [
|
|
3815
|
+
r.prefix,
|
|
3816
|
+
r.label ?? "\u2014",
|
|
3817
|
+
r.tier,
|
|
3818
|
+
isoDay(r.createdAt),
|
|
3819
|
+
isoDay(r.lastUsedAt),
|
|
3820
|
+
r.revokedAt ? "revoked" : "active"
|
|
3821
|
+
])
|
|
3822
|
+
)
|
|
3823
|
+
);
|
|
3824
|
+
} finally {
|
|
3825
|
+
await close();
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
async function runKeysRevoke(prefixOrId) {
|
|
3829
|
+
requireConfig(["DATABASE_URL"]);
|
|
3830
|
+
const { db, close } = createDb({ max: 1 });
|
|
3831
|
+
try {
|
|
3832
|
+
const n = await revokeKey(db, prefixOrId);
|
|
3833
|
+
if (n === 0) {
|
|
3834
|
+
logger.warn(`No active key matched "${prefixOrId}".`);
|
|
3835
|
+
process.exitCode = 1;
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
console.log(`Revoked ${n} key(s) matching "${prefixOrId}".`);
|
|
3839
|
+
} finally {
|
|
3840
|
+
await close();
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
var isoDay;
|
|
3844
|
+
var init_keys = __esm({
|
|
3845
|
+
"src/cli/keys.ts"() {
|
|
3846
|
+
"use strict";
|
|
3847
|
+
init_esm_shims();
|
|
3848
|
+
init_config();
|
|
3849
|
+
init_logger();
|
|
3850
|
+
init_apiKeys();
|
|
3851
|
+
init_client();
|
|
3852
|
+
init_format();
|
|
3853
|
+
isoDay = (d) => d ? d.toISOString().slice(0, 10) : "\u2014";
|
|
3854
|
+
}
|
|
3855
|
+
});
|
|
3856
|
+
|
|
2686
3857
|
// src/db/seed.ts
|
|
2687
|
-
import { readFileSync as
|
|
2688
|
-
import { sql as
|
|
3858
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3859
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2689
3860
|
import { z as z3 } from "zod";
|
|
2690
3861
|
function loadSeedFile(path2 = seedJsonPath()) {
|
|
2691
|
-
const raw = JSON.parse(
|
|
3862
|
+
const raw = JSON.parse(readFileSync3(path2, "utf8"));
|
|
2692
3863
|
const parsed = SeedFileSchema.parse(raw);
|
|
2693
3864
|
const byName = /* @__PURE__ */ new Map();
|
|
2694
3865
|
for (const entry of parsed) {
|
|
@@ -2702,7 +3873,7 @@ async function loadSeedPackages(db, path2) {
|
|
|
2702
3873
|
await db.insert(seedPackages).values(entries.map((e) => ({ name: e.name, category: e.category ?? null }))).onConflictDoUpdate({
|
|
2703
3874
|
target: seedPackages.name,
|
|
2704
3875
|
// Refresh category to the incoming value on conflict (EXCLUDED.category).
|
|
2705
|
-
set: { category:
|
|
3876
|
+
set: { category: sql7`excluded.category` }
|
|
2706
3877
|
});
|
|
2707
3878
|
logger.info(`Loaded ${entries.length} packages into seed_packages.`);
|
|
2708
3879
|
return entries.length;
|
|
@@ -2732,7 +3903,13 @@ __export(migrate_exports, {
|
|
|
2732
3903
|
});
|
|
2733
3904
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
2734
3905
|
async function ensureVectorExtension(handle) {
|
|
2735
|
-
|
|
3906
|
+
try {
|
|
3907
|
+
await handle.sql`CREATE EXTENSION IF NOT EXISTS vector`;
|
|
3908
|
+
} catch (err) {
|
|
3909
|
+
logger.warn(
|
|
3910
|
+
`CREATE EXTENSION vector did not run cleanly (continuing to migrations): ${err instanceof Error ? err.message : String(err)}`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
2736
3913
|
}
|
|
2737
3914
|
async function runMigrate() {
|
|
2738
3915
|
const handle = createDb({ max: 1 });
|
|
@@ -2791,13 +3968,17 @@ function buildProgram() {
|
|
|
2791
3968
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
2792
3969
|
await startMcpServer2();
|
|
2793
3970
|
});
|
|
3971
|
+
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) => {
|
|
3972
|
+
const { startHttpServer: startHttpServer2 } = await Promise.resolve().then(() => (init_http2(), http_exports));
|
|
3973
|
+
await startHttpServer2({ port: opts.port });
|
|
3974
|
+
});
|
|
2794
3975
|
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
3976
|
const { runSync: runSync2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
2796
3977
|
const summary = await runSync2({ full: opts.full, packageName: opts.package });
|
|
2797
3978
|
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
2798
3979
|
if (summary.status === "failed") process.exitCode = 1;
|
|
2799
3980
|
});
|
|
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) => {
|
|
3981
|
+
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
3982
|
const { runRecommend: runRecommend2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2802
3983
|
await runRecommend2(need, opts);
|
|
2803
3984
|
});
|
|
@@ -2813,14 +3994,53 @@ function buildProgram() {
|
|
|
2813
3994
|
const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
2814
3995
|
await runVerify2(pkg, opts);
|
|
2815
3996
|
});
|
|
2816
|
-
program.command("
|
|
3997
|
+
program.command("weights").description("show and explain the scoring weight model (health, quality, composite \u03BB)").option("--json", "output the weight model as JSON").action(async (opts) => {
|
|
3998
|
+
const { runWeights: runWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
3999
|
+
runWeights2(opts);
|
|
4000
|
+
});
|
|
4001
|
+
program.command("edit-weights").description("override, reset, or explain the scoring weights (layered over defaults)").option("--set <pair>", "override key=value, e.g. composite.lambda=0.5 (repeatable)", (v, acc) => acc.concat(v), []).option("--reset", "remove all overrides and restore defaults").option("--explain <component>", "explain a component (e.g. adoption, quality, lambda)").option("--project", "write to project-local .lurq/weights.json instead of the user config").action(async (opts) => {
|
|
4002
|
+
const { runEditWeights: runEditWeights2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
|
|
4003
|
+
runEditWeights2(opts);
|
|
4004
|
+
});
|
|
4005
|
+
program.command("discover").description("operator-side: proactively crawl for new packages and queue/gate them (\xA72B)").option("--cap <n>", "max candidates to fully ingest this run", (v) => parseInt(v, 10)).option("--dry-run", "discover, queue, and gate, but do not ingest survivors").option("--json", "output the discovery summary as JSON").action(async (opts) => {
|
|
4006
|
+
const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
4007
|
+
requireConfig2(["DATABASE_URL"]);
|
|
4008
|
+
const { runDiscovery: runDiscovery2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
4009
|
+
const summary = await runDiscovery2({ perRunCap: opts.cap, dryRun: opts.dryRun });
|
|
4010
|
+
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
4011
|
+
});
|
|
4012
|
+
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) => {
|
|
4013
|
+
const { requireConfig: requireConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
4014
|
+
requireConfig2(["DATABASE_URL"]);
|
|
4015
|
+
const { runRescore: runRescore2 } = await Promise.resolve().then(() => (init_pipeline(), pipeline_exports));
|
|
4016
|
+
const summary = await runRescore2();
|
|
4017
|
+
if (opts.json) console.log(JSON.stringify(summary, null, 2));
|
|
4018
|
+
});
|
|
4019
|
+
program.command("install").description("guided setup: connect lurq to your AI assistant(s)").option("--api-key <key>", "hosted API key (skips the prompt)").option("--url <url>", "hosted endpoint URL (defaults to the lurq service)").option("--agent <agent>", "claude-code | cursor | copilot | windsurf | codex | all").option("--yes", "non-interactive: use flags/env and detected agents without prompting").action(async (opts) => {
|
|
4020
|
+
const { runInstallWizard: runInstallWizard2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
4021
|
+
await runInstallWizard2(opts);
|
|
4022
|
+
});
|
|
4023
|
+
program.command("install-skill").description("register lurq as an MCP server in supported AI assistants (scriptable)").option(
|
|
2817
4024
|
"--agent <agent>",
|
|
2818
4025
|
"claude-code | cursor | copilot | windsurf | codex | all",
|
|
2819
4026
|
"claude-code"
|
|
2820
|
-
).action(async (opts) => {
|
|
4027
|
+
).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
4028
|
const { runInstallSkill: runInstallSkill2 } = await Promise.resolve().then(() => (init_installSkill(), installSkill_exports));
|
|
2822
4029
|
await runInstallSkill2(opts);
|
|
2823
4030
|
});
|
|
4031
|
+
const keys = program.command("keys").description("manage API keys for the hosted service (operator; needs DATABASE_URL)");
|
|
4032
|
+
keys.command("create").description("create a new API key (shown once; erased from the terminal after you copy it)").option("--label <label>", "human label (owner / org / purpose)").option("--tier <tier>", "tier name", "free").option("--json", "print the key as JSON and skip the interactive erase (for scripts)").action(async (opts) => {
|
|
4033
|
+
const { runKeysCreate: runKeysCreate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
4034
|
+
await runKeysCreate2(opts);
|
|
4035
|
+
});
|
|
4036
|
+
keys.command("list").description("list issued API keys (hashes are never shown)").option("--json", "output as JSON").action(async (opts) => {
|
|
4037
|
+
const { runKeysList: runKeysList2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
4038
|
+
await runKeysList2(opts);
|
|
4039
|
+
});
|
|
4040
|
+
keys.command("revoke").argument("<prefixOrId>", "key prefix (e.g. lurq_live_ab12cd) or numeric id").description("revoke an API key").action(async (prefixOrId) => {
|
|
4041
|
+
const { runKeysRevoke: runKeysRevoke2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
4042
|
+
await runKeysRevoke2(prefixOrId);
|
|
4043
|
+
});
|
|
2824
4044
|
const db = program.command("db").description("database management");
|
|
2825
4045
|
db.command("migrate").description("apply database migrations and load the seed list").action(async () => {
|
|
2826
4046
|
const { runMigrate: runMigrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
@@ -2840,9 +4060,11 @@ function buildProgram() {
|
|
|
2840
4060
|
return program;
|
|
2841
4061
|
}
|
|
2842
4062
|
export {
|
|
4063
|
+
API_KEY_PREFIX,
|
|
2843
4064
|
CACHE_TTL,
|
|
2844
4065
|
CATEGORIES,
|
|
2845
4066
|
ConfigError,
|
|
4067
|
+
DEFAULT_ENDPOINT,
|
|
2846
4068
|
EMBEDDING_DIM,
|
|
2847
4069
|
FRONTEND_CATEGORIES,
|
|
2848
4070
|
PACKAGE_NAME,
|