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