lurqrun 0.0.3 → 0.0.5
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/dist/bin/lurq.js +828 -68
- package/dist/bin/lurq.js.map +1 -1
- package/dist/index.d.ts +33 -2
- package/dist/index.js +824 -64
- package/dist/index.js.map +1 -1
- package/drizzle/0009_smooth_harrier.sql +10 -0
- package/drizzle/0010_curved_amphibian.sql +2 -0
- package/drizzle/meta/0009_snapshot.json +948 -0
- package/drizzle/meta/0010_snapshot.json +969 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +3 -1
package/dist/bin/lurq.js
CHANGED
|
@@ -26,7 +26,7 @@ var init_constants = __esm({
|
|
|
26
26
|
init_esm_shims();
|
|
27
27
|
SERVER_NAME = "lurq";
|
|
28
28
|
PACKAGE_NAME = "lurqrun";
|
|
29
|
-
VERSION = "0.0.
|
|
29
|
+
VERSION = "0.0.5";
|
|
30
30
|
DEFAULT_ENDPOINT = "https://api.lurq.run/mcp";
|
|
31
31
|
API_KEY_PREFIX = "lurq_live_";
|
|
32
32
|
EMBEDDING_DIM = 1536;
|
|
@@ -146,12 +146,18 @@ var init_config = __esm({
|
|
|
146
146
|
EnvSchema = z.object({
|
|
147
147
|
DATABASE_URL: z.string().min(1).optional(),
|
|
148
148
|
GITHUB_TOKEN: z.string().min(1).optional(),
|
|
149
|
+
// 'openai' here means "OpenAI-compatible" — any provider exposing /v1/embeddings
|
|
150
|
+
// + Bearer auth (OpenAI, Together, Fireworks, HF TEI, …). Point *_BASE_URL at it.
|
|
149
151
|
EMBEDDING_PROVIDER: z.enum(["openai", "local"]).default("openai"),
|
|
150
152
|
EMBEDDING_API_KEY: z.string().min(1).optional(),
|
|
151
153
|
EMBEDDING_MODEL: z.string().min(1).default("text-embedding-3-small"),
|
|
154
|
+
EMBEDDING_BASE_URL: z.string().url().default("https://api.openai.com/v1"),
|
|
155
|
+
// 'openai' means "OpenAI-compatible /v1/chat/completions": OpenAI, Groq, Together,
|
|
156
|
+
// Fireworks, xAI (Grok), etc. Swap provider by setting SUMMARY_BASE_URL + key + model.
|
|
152
157
|
SUMMARY_PROVIDER: z.enum(["openai", "none"]).default("openai"),
|
|
153
158
|
SUMMARY_API_KEY: z.string().min(1).optional(),
|
|
154
159
|
SUMMARY_MODEL: z.string().min(1).default("gpt-4o-mini"),
|
|
160
|
+
SUMMARY_BASE_URL: z.string().url().default("https://api.openai.com/v1"),
|
|
155
161
|
LURQ_SYNC_CONCURRENCY: z.coerce.number().int().positive().max(50).default(5),
|
|
156
162
|
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("info"),
|
|
157
163
|
// Hosted HTTP service (`serve-http`). Server-side only.
|
|
@@ -162,9 +168,22 @@ var init_config = __esm({
|
|
|
162
168
|
LURQ_IP_RATE_LIMIT_MAX: z.coerce.number().int().positive().default(240),
|
|
163
169
|
/** Rate-limit window, milliseconds (applies to both limiters). */
|
|
164
170
|
LURQ_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(6e4),
|
|
171
|
+
/** Bearer token guarding `/metrics`. Unset → the endpoint is disabled (404). */
|
|
172
|
+
LURQ_METRICS_TOKEN: z.string().min(1).optional(),
|
|
173
|
+
/** Shared secret for self-serve key issuance (`POST /keys`). The Clerk-
|
|
174
|
+
* authenticated web app presents it to mint a key for a signed-in user. Unset
|
|
175
|
+
* → the endpoint is disabled (404). Keep it server-side, never in the client. */
|
|
176
|
+
LURQ_ISSUER_SECRET: z.string().min(1).optional(),
|
|
165
177
|
// Client-side (install wizard / CLI talking to a remote endpoint).
|
|
166
178
|
LURQ_ENDPOINT: z.string().url().optional(),
|
|
167
|
-
LURQ_API_KEY: z.string().min(1).optional()
|
|
179
|
+
LURQ_API_KEY: z.string().min(1).optional(),
|
|
180
|
+
// Sandbox verification. With E2B_API_KEY set, package install + smoke-load
|
|
181
|
+
// runs in an isolated E2B cloud sandbox (safe for UNTRUSTED packages);
|
|
182
|
+
// without it, the local child-process driver is used (trusted packages only).
|
|
183
|
+
E2B_API_KEY: z.string().min(1).optional(),
|
|
184
|
+
// E2B template to launch. Must provide node + npm on PATH; omit for E2B's
|
|
185
|
+
// default. Provision a Node-versioned template here for reproducible runs.
|
|
186
|
+
E2B_TEMPLATE: z.string().min(1).optional()
|
|
168
187
|
});
|
|
169
188
|
ConfigError = class extends Error {
|
|
170
189
|
constructor(message) {
|
|
@@ -183,6 +202,7 @@ __export(schema_exports, {
|
|
|
183
202
|
discoveryQueue: () => discoveryQueue,
|
|
184
203
|
packageVersions: () => packageVersions,
|
|
185
204
|
packages: () => packages,
|
|
205
|
+
recommendationOutcomes: () => recommendationOutcomes,
|
|
186
206
|
seedPackages: () => seedPackages,
|
|
187
207
|
syncRuns: () => syncRuns,
|
|
188
208
|
verificationRuns: () => verificationRuns,
|
|
@@ -205,7 +225,7 @@ import {
|
|
|
205
225
|
uniqueIndex,
|
|
206
226
|
vector
|
|
207
227
|
} from "drizzle-orm/pg-core";
|
|
208
|
-
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges;
|
|
228
|
+
var tsvector, ts, packages, syncRuns, seedPackages, discoveryQueue, apiKeys, packageVersions, watchState, verificationRuns, compatEdges, recommendationOutcomes;
|
|
209
229
|
var init_schema = __esm({
|
|
210
230
|
"src/db/schema.ts"() {
|
|
211
231
|
"use strict";
|
|
@@ -388,6 +408,28 @@ var init_schema = __esm({
|
|
|
388
408
|
)
|
|
389
409
|
]
|
|
390
410
|
);
|
|
411
|
+
recommendationOutcomes = pgTable(
|
|
412
|
+
"recommendation_outcomes",
|
|
413
|
+
{
|
|
414
|
+
id: serial("id").primaryKey(),
|
|
415
|
+
/** The org this outcome belongs to (api_keys.owner_id). Null for anonymous /
|
|
416
|
+
* operator-issued keys. This is what turns the flywheel from a global blob
|
|
417
|
+
* into a per-org asset — "what did *this* org succeed with." Server-injected
|
|
418
|
+
* from the authenticated key, never caller-supplied. */
|
|
419
|
+
ownerId: text("owner_id"),
|
|
420
|
+
packageName: text("package_name").notNull(),
|
|
421
|
+
accepted: boolean("accepted").notNull(),
|
|
422
|
+
buildSignal: text("build_signal").$type(),
|
|
423
|
+
/** The original need text the recommendation was for — ties outcome back to
|
|
424
|
+
* the ask. Optional, length-capped at the trust boundary; never source code. */
|
|
425
|
+
need: text("need"),
|
|
426
|
+
createdAt: ts("created_at").notNull().defaultNow()
|
|
427
|
+
},
|
|
428
|
+
(table2) => [
|
|
429
|
+
index("recommendation_outcomes_pkg_idx").on(table2.packageName),
|
|
430
|
+
index("recommendation_outcomes_owner_idx").on(table2.ownerId)
|
|
431
|
+
]
|
|
432
|
+
);
|
|
391
433
|
}
|
|
392
434
|
});
|
|
393
435
|
|
|
@@ -964,7 +1006,7 @@ function parseDownloadGrowth(json2) {
|
|
|
964
1006
|
const priorAvg = sum(prior) / prior.length;
|
|
965
1007
|
const recentAvg = sum(recent) / recent.length;
|
|
966
1008
|
if (priorAvg <= 0) return recentAvg > 0 ? 1 : 0;
|
|
967
|
-
return (recentAvg - priorAvg) / priorAvg;
|
|
1009
|
+
return Math.round((recentAvg - priorAvg) / priorAvg * 1e3) / 1e3;
|
|
968
1010
|
}
|
|
969
1011
|
function ymd(date) {
|
|
970
1012
|
return date.toISOString().slice(0, 10);
|
|
@@ -1498,6 +1540,94 @@ var init_verification = __esm({
|
|
|
1498
1540
|
}
|
|
1499
1541
|
});
|
|
1500
1542
|
|
|
1543
|
+
// src/db/outcomes.ts
|
|
1544
|
+
async function recordOutcome(db, outcome) {
|
|
1545
|
+
await db.insert(recommendationOutcomes).values(outcome);
|
|
1546
|
+
}
|
|
1547
|
+
var init_outcomes = __esm({
|
|
1548
|
+
"src/db/outcomes.ts"() {
|
|
1549
|
+
"use strict";
|
|
1550
|
+
init_esm_shims();
|
|
1551
|
+
init_schema();
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
// src/data/successors.json
|
|
1556
|
+
var successors_default;
|
|
1557
|
+
var init_successors = __esm({
|
|
1558
|
+
"src/data/successors.json"() {
|
|
1559
|
+
successors_default = {
|
|
1560
|
+
moment: {
|
|
1561
|
+
replacedBy: "dayjs",
|
|
1562
|
+
reason: "In maintenance mode since 2020 (the maintainers recommend alternatives). dayjs is a ~2KB immutable library with a near-identical API."
|
|
1563
|
+
},
|
|
1564
|
+
request: {
|
|
1565
|
+
replacedBy: "got",
|
|
1566
|
+
reason: "Deprecated in 2020 and no longer maintained. Use the built-in fetch, or got for a full-featured HTTP client."
|
|
1567
|
+
},
|
|
1568
|
+
"request-promise": {
|
|
1569
|
+
replacedBy: "got",
|
|
1570
|
+
reason: "Deprecated alongside request. got returns promises natively."
|
|
1571
|
+
},
|
|
1572
|
+
"node-sass": {
|
|
1573
|
+
replacedBy: "sass",
|
|
1574
|
+
reason: "Deprecated; bindings break on new Node versions. sass is the Dart implementation (sass-embedded for speed)."
|
|
1575
|
+
},
|
|
1576
|
+
tslint: {
|
|
1577
|
+
replacedBy: "typescript-eslint",
|
|
1578
|
+
reason: "Deprecated in 2019 in favor of ESLint. Use typescript-eslint to lint TypeScript."
|
|
1579
|
+
},
|
|
1580
|
+
enzyme: {
|
|
1581
|
+
replacedBy: "@testing-library/react",
|
|
1582
|
+
reason: "Unmaintained with no official React 17+/18 adapter. React Testing Library is the current standard."
|
|
1583
|
+
},
|
|
1584
|
+
protractor: {
|
|
1585
|
+
replacedBy: "@playwright/test",
|
|
1586
|
+
reason: "Reached end-of-life in 2023. Use Playwright (or Cypress) for browser E2E."
|
|
1587
|
+
},
|
|
1588
|
+
faker: {
|
|
1589
|
+
replacedBy: "@faker-js/faker",
|
|
1590
|
+
reason: "The original package was sabotaged and deprecated. @faker-js/faker is the community-maintained fork."
|
|
1591
|
+
},
|
|
1592
|
+
bower: {
|
|
1593
|
+
replacedBy: "npm",
|
|
1594
|
+
reason: "Deprecated; front-end packages ship on npm now. Use npm (or your package manager) directly."
|
|
1595
|
+
},
|
|
1596
|
+
"popper.js": {
|
|
1597
|
+
replacedBy: "@popperjs/core",
|
|
1598
|
+
reason: "v1 is deprecated. @popperjs/core is the maintained v2 line."
|
|
1599
|
+
},
|
|
1600
|
+
"create-react-app": {
|
|
1601
|
+
replacedBy: "vite",
|
|
1602
|
+
reason: "No longer recommended by the React team and effectively unmaintained. Vite is the current scaffolding standard."
|
|
1603
|
+
},
|
|
1604
|
+
"babel-eslint": {
|
|
1605
|
+
replacedBy: "@babel/eslint-parser",
|
|
1606
|
+
reason: "Deprecated and renamed. Use @babel/eslint-parser."
|
|
1607
|
+
},
|
|
1608
|
+
istanbul: {
|
|
1609
|
+
replacedBy: "nyc",
|
|
1610
|
+
reason: "The istanbul package is legacy; nyc is its maintained CLI (or c8 for native V8 coverage)."
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// src/core/successors.ts
|
|
1617
|
+
function lookupSuccessor(name) {
|
|
1618
|
+
const hit = MAP[name.toLowerCase()];
|
|
1619
|
+
return hit ? { name: hit.replacedBy, reason: hit.reason } : null;
|
|
1620
|
+
}
|
|
1621
|
+
var MAP;
|
|
1622
|
+
var init_successors2 = __esm({
|
|
1623
|
+
"src/core/successors.ts"() {
|
|
1624
|
+
"use strict";
|
|
1625
|
+
init_esm_shims();
|
|
1626
|
+
init_successors();
|
|
1627
|
+
MAP = successors_default;
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1501
1631
|
// src/ingestion/sources/githubReadme.ts
|
|
1502
1632
|
async function fetchGithubReadme(owner, repo, fetchImpl) {
|
|
1503
1633
|
for (const file of CANDIDATES) {
|
|
@@ -1564,7 +1694,12 @@ function str(value) {
|
|
|
1564
1694
|
function createSummaryProvider(fetchImpl) {
|
|
1565
1695
|
const config = getConfig();
|
|
1566
1696
|
if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
|
|
1567
|
-
return new OpenAISummaryProvider(
|
|
1697
|
+
return new OpenAISummaryProvider(
|
|
1698
|
+
config.SUMMARY_API_KEY,
|
|
1699
|
+
config.SUMMARY_MODEL,
|
|
1700
|
+
config.SUMMARY_BASE_URL,
|
|
1701
|
+
fetchImpl
|
|
1702
|
+
);
|
|
1568
1703
|
}
|
|
1569
1704
|
return new FallbackSummaryProvider();
|
|
1570
1705
|
}
|
|
@@ -1636,15 +1771,19 @@ var init_summarize = __esm({
|
|
|
1636
1771
|
};
|
|
1637
1772
|
SYSTEM_PROMPT = "You summarize npm packages factually for an AI coding agent choosing dependencies. Use ONLY the provided README/description. Never invent capabilities or APIs. Be concise and concrete. Respond with a JSON object.";
|
|
1638
1773
|
OpenAISummaryProvider = class {
|
|
1639
|
-
constructor(apiKey, model, fetchImpl) {
|
|
1774
|
+
constructor(apiKey, model, baseUrl, fetchImpl) {
|
|
1640
1775
|
this.apiKey = apiKey;
|
|
1641
1776
|
this.model = model;
|
|
1642
1777
|
this.fetchImpl = fetchImpl;
|
|
1778
|
+
this.endpoint = `${baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
1779
|
+
this.host = new URL(baseUrl).host;
|
|
1643
1780
|
}
|
|
1644
1781
|
apiKey;
|
|
1645
1782
|
model;
|
|
1646
1783
|
fetchImpl;
|
|
1647
1784
|
kind = "openai";
|
|
1785
|
+
endpoint;
|
|
1786
|
+
host;
|
|
1648
1787
|
async generate(input) {
|
|
1649
1788
|
try {
|
|
1650
1789
|
const body = JSON.stringify({
|
|
@@ -1656,8 +1795,8 @@ var init_summarize = __esm({
|
|
|
1656
1795
|
response_format: { type: "json_object" },
|
|
1657
1796
|
temperature: 0.2
|
|
1658
1797
|
});
|
|
1659
|
-
const { data } = await httpRequest(
|
|
1660
|
-
host:
|
|
1798
|
+
const { data } = await httpRequest(this.endpoint, {
|
|
1799
|
+
host: this.host,
|
|
1661
1800
|
method: "POST",
|
|
1662
1801
|
ttlMs: 30 * 24 * 60 * 60 * 1e3,
|
|
1663
1802
|
// cache summaries 30d to control cost
|
|
@@ -1844,10 +1983,10 @@ function activeWeightsPath() {
|
|
|
1844
1983
|
function loadWeights() {
|
|
1845
1984
|
if (cachedWeights) return cachedWeights;
|
|
1846
1985
|
let merged = structuredClone(DEFAULT_WEIGHTS);
|
|
1847
|
-
const
|
|
1848
|
-
if (
|
|
1986
|
+
const active2 = activeWeightsPath();
|
|
1987
|
+
if (active2) {
|
|
1849
1988
|
try {
|
|
1850
|
-
const fromFile = JSON.parse(readFileSync(
|
|
1989
|
+
const fromFile = JSON.parse(readFileSync(active2.path, "utf8"));
|
|
1851
1990
|
merged = mergeWeights(merged, fromFile);
|
|
1852
1991
|
} catch {
|
|
1853
1992
|
}
|
|
@@ -2312,7 +2451,12 @@ function localEmbed(text2, dim2 = EMBEDDING_DIM) {
|
|
|
2312
2451
|
function createEmbeddingProvider(fetchImpl) {
|
|
2313
2452
|
const config = getConfig();
|
|
2314
2453
|
if (config.EMBEDDING_PROVIDER === "openai" && config.EMBEDDING_API_KEY) {
|
|
2315
|
-
return new OpenAIEmbeddingProvider(
|
|
2454
|
+
return new OpenAIEmbeddingProvider(
|
|
2455
|
+
config.EMBEDDING_API_KEY,
|
|
2456
|
+
config.EMBEDDING_MODEL,
|
|
2457
|
+
config.EMBEDDING_BASE_URL,
|
|
2458
|
+
fetchImpl
|
|
2459
|
+
);
|
|
2316
2460
|
}
|
|
2317
2461
|
if (config.EMBEDDING_PROVIDER === "openai" && !config.EMBEDDING_API_KEY) {
|
|
2318
2462
|
logger.warn("EMBEDDING_API_KEY not set \u2014 falling back to the local embedder.");
|
|
@@ -2338,11 +2482,13 @@ var init_embeddings = __esm({
|
|
|
2338
2482
|
};
|
|
2339
2483
|
OPENAI_BATCH = 96;
|
|
2340
2484
|
OpenAIEmbeddingProvider = class {
|
|
2341
|
-
constructor(apiKey, model, fetchImpl) {
|
|
2485
|
+
constructor(apiKey, model, baseUrl, fetchImpl) {
|
|
2342
2486
|
this.apiKey = apiKey;
|
|
2343
2487
|
this.model = model;
|
|
2344
2488
|
this.fetchImpl = fetchImpl;
|
|
2345
2489
|
this.id = `openai:${model}`;
|
|
2490
|
+
this.endpoint = `${baseUrl.replace(/\/$/, "")}/embeddings`;
|
|
2491
|
+
this.host = new URL(baseUrl).host;
|
|
2346
2492
|
}
|
|
2347
2493
|
apiKey;
|
|
2348
2494
|
model;
|
|
@@ -2350,13 +2496,15 @@ var init_embeddings = __esm({
|
|
|
2350
2496
|
kind = "openai";
|
|
2351
2497
|
dimensions = EMBEDDING_DIM;
|
|
2352
2498
|
id;
|
|
2499
|
+
endpoint;
|
|
2500
|
+
host;
|
|
2353
2501
|
async embed(texts) {
|
|
2354
2502
|
const out = [];
|
|
2355
2503
|
for (let i = 0; i < texts.length; i += OPENAI_BATCH) {
|
|
2356
2504
|
const batch = texts.slice(i, i + OPENAI_BATCH);
|
|
2357
2505
|
const body = JSON.stringify({ model: this.model, input: batch, dimensions: EMBEDDING_DIM });
|
|
2358
|
-
const { data } = await httpRequest(
|
|
2359
|
-
host:
|
|
2506
|
+
const { data } = await httpRequest(this.endpoint, {
|
|
2507
|
+
host: this.host,
|
|
2360
2508
|
method: "POST",
|
|
2361
2509
|
ttlMs: 30 * 24 * 60 * 60 * 1e3,
|
|
2362
2510
|
// cache embeddings 30d to control cost
|
|
@@ -2411,6 +2559,11 @@ async function runSync(opts = {}) {
|
|
|
2411
2559
|
const handle = createDb({ max: Math.max(4, config.LURQ_SYNC_CONCURRENCY) });
|
|
2412
2560
|
const provider = createSummaryProvider();
|
|
2413
2561
|
logger.info(`Summary provider: ${provider.kind}`);
|
|
2562
|
+
if (!config.GITHUB_TOKEN) {
|
|
2563
|
+
logger.warn(
|
|
2564
|
+
"GITHUB_TOKEN not set \u2014 GitHub signals (stars, issues, release cadence) will be skipped, degrading maintenance/adoption scores. Set it for accurate scoring."
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2414
2567
|
const runId = await startSyncRun(handle.db);
|
|
2415
2568
|
const allErrors = [];
|
|
2416
2569
|
try {
|
|
@@ -2623,6 +2776,57 @@ var init_sync = __esm({
|
|
|
2623
2776
|
}
|
|
2624
2777
|
});
|
|
2625
2778
|
|
|
2779
|
+
// src/pipeline/ingestQueue.ts
|
|
2780
|
+
function enqueueIngest(db, name) {
|
|
2781
|
+
if (queuedNames.has(name) || inFlight.has(name)) return;
|
|
2782
|
+
if (pending.length >= MAX_PENDING) {
|
|
2783
|
+
logger.warn(`ingest queue full (${MAX_PENDING}); dropping on-demand request for ${name}`);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
queuedNames.add(name);
|
|
2787
|
+
pending.push(name);
|
|
2788
|
+
pump(db);
|
|
2789
|
+
}
|
|
2790
|
+
function pump(db) {
|
|
2791
|
+
while (active < MAX_CONCURRENT && pending.length > 0) {
|
|
2792
|
+
const name = pending.shift();
|
|
2793
|
+
queuedNames.delete(name);
|
|
2794
|
+
inFlight.add(name);
|
|
2795
|
+
active += 1;
|
|
2796
|
+
void ingestOne(db, name).finally(() => {
|
|
2797
|
+
inFlight.delete(name);
|
|
2798
|
+
active -= 1;
|
|
2799
|
+
pump(db);
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
async function ingestOne(db, name) {
|
|
2804
|
+
try {
|
|
2805
|
+
const row = await syncOnePackage(db, name);
|
|
2806
|
+
if (row.confidence && row.confidence !== "unproven") {
|
|
2807
|
+
await ensureSeedEntry(db, name, row.category);
|
|
2808
|
+
}
|
|
2809
|
+
} catch (err) {
|
|
2810
|
+
logger.warn(`on-demand ingest failed for ${name}: ${String(err)}`);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
var MAX_CONCURRENT, MAX_PENDING, pending, inFlight, queuedNames, active;
|
|
2814
|
+
var init_ingestQueue = __esm({
|
|
2815
|
+
"src/pipeline/ingestQueue.ts"() {
|
|
2816
|
+
"use strict";
|
|
2817
|
+
init_esm_shims();
|
|
2818
|
+
init_logger();
|
|
2819
|
+
init_packages();
|
|
2820
|
+
init_single();
|
|
2821
|
+
MAX_CONCURRENT = 3;
|
|
2822
|
+
MAX_PENDING = 500;
|
|
2823
|
+
pending = [];
|
|
2824
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
2825
|
+
queuedNames = /* @__PURE__ */ new Set();
|
|
2826
|
+
active = 0;
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2626
2830
|
// src/pipeline/single.ts
|
|
2627
2831
|
import { and as and3, eq as eq3, isNotNull as isNotNull2, sql as sql2 } from "drizzle-orm";
|
|
2628
2832
|
async function getSeedCategory(db, name) {
|
|
@@ -2706,11 +2910,8 @@ async function getOrFetchPackage(db, name) {
|
|
|
2706
2910
|
if (existing) return { row: existing, wasTracked: true, existsOnNpm: true };
|
|
2707
2911
|
const exists = await npmPackageExists(name);
|
|
2708
2912
|
if (!exists) return { row: null, wasTracked: false, existsOnNpm: false };
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
await ensureSeedEntry(db, name, row.category);
|
|
2712
|
-
}
|
|
2713
|
-
return { row, wasTracked: false, existsOnNpm: true };
|
|
2913
|
+
enqueueIngest(db, name);
|
|
2914
|
+
return { row: null, wasTracked: false, existsOnNpm: true, queued: true };
|
|
2714
2915
|
}
|
|
2715
2916
|
var init_single = __esm({
|
|
2716
2917
|
"src/pipeline/single.ts"() {
|
|
@@ -2726,6 +2927,7 @@ var init_single = __esm({
|
|
|
2726
2927
|
init_packages();
|
|
2727
2928
|
init_schema();
|
|
2728
2929
|
init_sync();
|
|
2930
|
+
init_ingestQueue();
|
|
2729
2931
|
}
|
|
2730
2932
|
});
|
|
2731
2933
|
|
|
@@ -2879,7 +3081,220 @@ var init_risk = __esm({
|
|
|
2879
3081
|
}
|
|
2880
3082
|
});
|
|
2881
3083
|
|
|
3084
|
+
// src/data/popular-packages.json
|
|
3085
|
+
var popular_packages_default;
|
|
3086
|
+
var init_popular_packages = __esm({
|
|
3087
|
+
"src/data/popular-packages.json"() {
|
|
3088
|
+
popular_packages_default = [
|
|
3089
|
+
"react",
|
|
3090
|
+
"react-dom",
|
|
3091
|
+
"react-router",
|
|
3092
|
+
"react-router-dom",
|
|
3093
|
+
"next",
|
|
3094
|
+
"vue",
|
|
3095
|
+
"vue-router",
|
|
3096
|
+
"pinia",
|
|
3097
|
+
"vuex",
|
|
3098
|
+
"svelte",
|
|
3099
|
+
"solid-js",
|
|
3100
|
+
"preact",
|
|
3101
|
+
"angular",
|
|
3102
|
+
"@angular/core",
|
|
3103
|
+
"jquery",
|
|
3104
|
+
"lodash",
|
|
3105
|
+
"underscore",
|
|
3106
|
+
"ramda",
|
|
3107
|
+
"immer",
|
|
3108
|
+
"rxjs",
|
|
3109
|
+
"express",
|
|
3110
|
+
"koa",
|
|
3111
|
+
"fastify",
|
|
3112
|
+
"@nestjs/core",
|
|
3113
|
+
"hapi",
|
|
3114
|
+
"connect",
|
|
3115
|
+
"body-parser",
|
|
3116
|
+
"cookie-parser",
|
|
3117
|
+
"cors",
|
|
3118
|
+
"helmet",
|
|
3119
|
+
"morgan",
|
|
3120
|
+
"compression",
|
|
3121
|
+
"express-session",
|
|
3122
|
+
"express-rate-limit",
|
|
3123
|
+
"multer",
|
|
3124
|
+
"passport",
|
|
3125
|
+
"jsonwebtoken",
|
|
3126
|
+
"bcrypt",
|
|
3127
|
+
"bcryptjs",
|
|
3128
|
+
"axios",
|
|
3129
|
+
"node-fetch",
|
|
3130
|
+
"got",
|
|
3131
|
+
"undici",
|
|
3132
|
+
"superagent",
|
|
3133
|
+
"request",
|
|
3134
|
+
"cheerio",
|
|
3135
|
+
"puppeteer",
|
|
3136
|
+
"playwright",
|
|
3137
|
+
"cypress",
|
|
3138
|
+
"jsdom",
|
|
3139
|
+
"ws",
|
|
3140
|
+
"socket.io",
|
|
3141
|
+
"socket.io-client",
|
|
3142
|
+
"graphql",
|
|
3143
|
+
"@apollo/client",
|
|
3144
|
+
"apollo-server",
|
|
3145
|
+
"dataloader",
|
|
3146
|
+
"mongoose",
|
|
3147
|
+
"mongodb",
|
|
3148
|
+
"pg",
|
|
3149
|
+
"mysql2",
|
|
3150
|
+
"sequelize",
|
|
3151
|
+
"prisma",
|
|
3152
|
+
"knex",
|
|
3153
|
+
"drizzle-orm",
|
|
3154
|
+
"typeorm",
|
|
3155
|
+
"redis",
|
|
3156
|
+
"ioredis",
|
|
3157
|
+
"sqlite3",
|
|
3158
|
+
"better-sqlite3",
|
|
3159
|
+
"typescript",
|
|
3160
|
+
"eslint",
|
|
3161
|
+
"prettier",
|
|
3162
|
+
"tslint",
|
|
3163
|
+
"webpack",
|
|
3164
|
+
"vite",
|
|
3165
|
+
"rollup",
|
|
3166
|
+
"esbuild",
|
|
3167
|
+
"parcel",
|
|
3168
|
+
"@babel/core",
|
|
3169
|
+
"babel-core",
|
|
3170
|
+
"babel-loader",
|
|
3171
|
+
"ts-node",
|
|
3172
|
+
"tsx",
|
|
3173
|
+
"tsup",
|
|
3174
|
+
"nodemon",
|
|
3175
|
+
"concurrently",
|
|
3176
|
+
"npm-run-all",
|
|
3177
|
+
"jest",
|
|
3178
|
+
"vitest",
|
|
3179
|
+
"mocha",
|
|
3180
|
+
"chai",
|
|
3181
|
+
"sinon",
|
|
3182
|
+
"ava",
|
|
3183
|
+
"tape",
|
|
3184
|
+
"supertest",
|
|
3185
|
+
"@testing-library/react",
|
|
3186
|
+
"enzyme",
|
|
3187
|
+
"chalk",
|
|
3188
|
+
"colors",
|
|
3189
|
+
"kleur",
|
|
3190
|
+
"picocolors",
|
|
3191
|
+
"ansi-colors",
|
|
3192
|
+
"commander",
|
|
3193
|
+
"yargs",
|
|
3194
|
+
"meow",
|
|
3195
|
+
"minimist",
|
|
3196
|
+
"inquirer",
|
|
3197
|
+
"prompts",
|
|
3198
|
+
"ora",
|
|
3199
|
+
"boxen",
|
|
3200
|
+
"figlet",
|
|
3201
|
+
"cli-progress",
|
|
3202
|
+
"debug",
|
|
3203
|
+
"dotenv",
|
|
3204
|
+
"cross-env",
|
|
3205
|
+
"rimraf",
|
|
3206
|
+
"glob",
|
|
3207
|
+
"fast-glob",
|
|
3208
|
+
"chokidar",
|
|
3209
|
+
"fs-extra",
|
|
3210
|
+
"execa",
|
|
3211
|
+
"cross-spawn",
|
|
3212
|
+
"shelljs",
|
|
3213
|
+
"which",
|
|
3214
|
+
"semver",
|
|
3215
|
+
"uuid",
|
|
3216
|
+
"nanoid",
|
|
3217
|
+
"ulid",
|
|
3218
|
+
"moment",
|
|
3219
|
+
"dayjs",
|
|
3220
|
+
"date-fns",
|
|
3221
|
+
"luxon",
|
|
3222
|
+
"zod",
|
|
3223
|
+
"yup",
|
|
3224
|
+
"joi",
|
|
3225
|
+
"ajv",
|
|
3226
|
+
"validator",
|
|
3227
|
+
"class-validator",
|
|
3228
|
+
"winston",
|
|
3229
|
+
"pino",
|
|
3230
|
+
"bunyan",
|
|
3231
|
+
"nodemailer",
|
|
3232
|
+
"node-cron",
|
|
3233
|
+
"bull",
|
|
3234
|
+
"bullmq",
|
|
3235
|
+
"kafkajs",
|
|
3236
|
+
"amqplib",
|
|
3237
|
+
"sharp",
|
|
3238
|
+
"jimp",
|
|
3239
|
+
"archiver",
|
|
3240
|
+
"tar",
|
|
3241
|
+
"adm-zip",
|
|
3242
|
+
"qs",
|
|
3243
|
+
"query-string",
|
|
3244
|
+
"form-data",
|
|
3245
|
+
"mime-types",
|
|
3246
|
+
"http-errors",
|
|
3247
|
+
"classnames",
|
|
3248
|
+
"clsx",
|
|
3249
|
+
"styled-components",
|
|
3250
|
+
"@emotion/react",
|
|
3251
|
+
"tailwindcss",
|
|
3252
|
+
"postcss",
|
|
3253
|
+
"autoprefixer",
|
|
3254
|
+
"sass",
|
|
3255
|
+
"less",
|
|
3256
|
+
"framer-motion",
|
|
3257
|
+
"three",
|
|
3258
|
+
"d3",
|
|
3259
|
+
"chart.js",
|
|
3260
|
+
"recharts",
|
|
3261
|
+
"gsap",
|
|
3262
|
+
"swiper",
|
|
3263
|
+
"leaflet",
|
|
3264
|
+
"mapbox-gl",
|
|
3265
|
+
"formik",
|
|
3266
|
+
"react-hook-form",
|
|
3267
|
+
"redux",
|
|
3268
|
+
"@reduxjs/toolkit",
|
|
3269
|
+
"zustand",
|
|
3270
|
+
"jotai",
|
|
3271
|
+
"mobx",
|
|
3272
|
+
"bootstrap",
|
|
3273
|
+
"@popperjs/core",
|
|
3274
|
+
"aws-sdk",
|
|
3275
|
+
"@aws-sdk/client-s3",
|
|
3276
|
+
"firebase",
|
|
3277
|
+
"firebase-admin",
|
|
3278
|
+
"stripe",
|
|
3279
|
+
"twilio",
|
|
3280
|
+
"openai",
|
|
3281
|
+
"@anthropic-ai/sdk",
|
|
3282
|
+
"googleapis",
|
|
3283
|
+
"bignumber.js",
|
|
3284
|
+
"decimal.js",
|
|
3285
|
+
"mathjs",
|
|
3286
|
+
"slugify",
|
|
3287
|
+
"faker",
|
|
3288
|
+
"@faker-js/faker",
|
|
3289
|
+
"puppeteer-core"
|
|
3290
|
+
];
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
|
|
2882
3294
|
// src/security/typosquat.ts
|
|
3295
|
+
function typosquatCorpus(trackedTopNames) {
|
|
3296
|
+
return [.../* @__PURE__ */ new Set([...POPULAR_BASELINE, ...trackedTopNames])];
|
|
3297
|
+
}
|
|
2883
3298
|
function bareName(name) {
|
|
2884
3299
|
const slash = name.indexOf("/");
|
|
2885
3300
|
return (slash >= 0 ? name.slice(slash + 1) : name).toLowerCase();
|
|
@@ -2928,10 +3343,13 @@ function detectTyposquat(name, popular, maxDistance = 2) {
|
|
|
2928
3343
|
}
|
|
2929
3344
|
return best;
|
|
2930
3345
|
}
|
|
3346
|
+
var POPULAR_BASELINE;
|
|
2931
3347
|
var init_typosquat = __esm({
|
|
2932
3348
|
"src/security/typosquat.ts"() {
|
|
2933
3349
|
"use strict";
|
|
2934
3350
|
init_esm_shims();
|
|
3351
|
+
init_popular_packages();
|
|
3352
|
+
POPULAR_BASELINE = popular_packages_default;
|
|
2935
3353
|
}
|
|
2936
3354
|
});
|
|
2937
3355
|
|
|
@@ -2942,6 +3360,7 @@ __export(handlers_exports, {
|
|
|
2942
3360
|
handleCompat: () => handleCompat,
|
|
2943
3361
|
handleEvaluate: () => handleEvaluate,
|
|
2944
3362
|
handleRecommend: () => handleRecommend,
|
|
3363
|
+
handleReportOutcome: () => handleReportOutcome,
|
|
2945
3364
|
handleVerify: () => handleVerify,
|
|
2946
3365
|
latestDataAsOf: () => latestDataAsOf,
|
|
2947
3366
|
rowToEvaluate: () => rowToEvaluate
|
|
@@ -2991,7 +3410,8 @@ function rowToEvaluate(row) {
|
|
|
2991
3410
|
advisories: topAdvisories(row.advisories),
|
|
2992
3411
|
summary: row.summary ? truncateSentences(row.summary, 3) : null,
|
|
2993
3412
|
usageGuide: row.usageGuide ?? null,
|
|
2994
|
-
repoUrl: row.repoUrl
|
|
3413
|
+
repoUrl: row.repoUrl,
|
|
3414
|
+
replacedBy: lookupSuccessor(row.name)
|
|
2995
3415
|
};
|
|
2996
3416
|
}
|
|
2997
3417
|
async function latestDataAsOf(db) {
|
|
@@ -3027,7 +3447,7 @@ async function handleEvaluate(db, input) {
|
|
|
3027
3447
|
if (!row) {
|
|
3028
3448
|
return {
|
|
3029
3449
|
tracked: false,
|
|
3030
|
-
suggestion: existsOnNpm ?
|
|
3450
|
+
suggestion: existsOnNpm ? `\u{1F389} Congrats \u2014 you're the first to add "${input.package}" to lurq's registry! It's being fetched and scored now; retry in a few seconds for the full evidence read.` : `"${input.package}" was not found on the npm registry. Check the package name.`
|
|
3031
3451
|
};
|
|
3032
3452
|
}
|
|
3033
3453
|
const evaluated = rowToEvaluate(row);
|
|
@@ -3047,7 +3467,14 @@ async function handleCompare(db, input) {
|
|
|
3047
3467
|
const results = await Promise.all(input.packages.map((name) => getOrFetchPackage(db, name)));
|
|
3048
3468
|
const rows = results.map((r) => r.row).filter((row) => row !== null).map(rowToEvaluate).sort((a, b) => b.healthScore - a.healthScore);
|
|
3049
3469
|
const missing = input.packages.filter((name) => !rows.some((r) => r.name === name));
|
|
3050
|
-
return {
|
|
3470
|
+
return {
|
|
3471
|
+
dataAsOf: await latestDataAsOf(db),
|
|
3472
|
+
rows,
|
|
3473
|
+
...missing.length ? {
|
|
3474
|
+
missing,
|
|
3475
|
+
note: "\u{1F389} You're the first to add these to lurq's registry! They're being scored now; retry shortly for the full comparison."
|
|
3476
|
+
} : {}
|
|
3477
|
+
};
|
|
3051
3478
|
},
|
|
3052
3479
|
// Don't cache a transient miss (a package that momentarily failed to fetch).
|
|
3053
3480
|
{ skipCache: (r) => Boolean(r.missing?.length) }
|
|
@@ -3071,7 +3498,7 @@ async function handleVerify(db, input) {
|
|
|
3071
3498
|
const name = input.package;
|
|
3072
3499
|
const exists = await npmPackageExists(name);
|
|
3073
3500
|
if (!exists) {
|
|
3074
|
-
const typo2 = detectTyposquat(name, await getTopPackageNames(db).catch(() => []));
|
|
3501
|
+
const typo2 = detectTyposquat(name, typosquatCorpus(await getTopPackageNames(db).catch(() => [])));
|
|
3075
3502
|
return {
|
|
3076
3503
|
exists: false,
|
|
3077
3504
|
tracked: false,
|
|
@@ -3091,7 +3518,7 @@ async function handleVerify(db, input) {
|
|
|
3091
3518
|
getOrFetchPackage(db, name),
|
|
3092
3519
|
getTopPackageNames(db).catch(() => [])
|
|
3093
3520
|
]);
|
|
3094
|
-
const weeklyDownloads = row?.weeklyDownloads ?? null;
|
|
3521
|
+
const weeklyDownloads = row?.weeklyDownloads ?? await fetchWeeklyDownloads(name).catch(() => null);
|
|
3095
3522
|
const advisories = row?.advisories ?? [];
|
|
3096
3523
|
const advisoryCount = advisories.length;
|
|
3097
3524
|
const deprecated = Boolean(row?.deprecated || registry?.deprecated);
|
|
@@ -3099,7 +3526,7 @@ async function handleVerify(db, input) {
|
|
|
3099
3526
|
const brandNew = withinDays(registry?.firstPublishedAt ?? null, 7);
|
|
3100
3527
|
const lowTrust = weeklyDownloads === null || weeklyDownloads < 1e3;
|
|
3101
3528
|
const installScripts = registry?.hasInstallScripts ?? false;
|
|
3102
|
-
const typo = detectTyposquat(name, popular);
|
|
3529
|
+
const typo = detectTyposquat(name, typosquatCorpus(popular));
|
|
3103
3530
|
const riskFlags = [];
|
|
3104
3531
|
if (typo) riskFlags.push(`possible-typosquat-of:${typo.target}`);
|
|
3105
3532
|
if (weeklyDownloads === null || weeklyDownloads === 0) riskFlags.push("zero-downloads");
|
|
@@ -3133,6 +3560,16 @@ async function handleVerify(db, input) {
|
|
|
3133
3560
|
advisoryCount
|
|
3134
3561
|
};
|
|
3135
3562
|
}
|
|
3563
|
+
async function handleReportOutcome(db, input, ownerId = null) {
|
|
3564
|
+
await recordOutcome(db, {
|
|
3565
|
+
ownerId,
|
|
3566
|
+
packageName: input.package,
|
|
3567
|
+
accepted: input.accepted,
|
|
3568
|
+
buildSignal: input.buildSignal ?? null,
|
|
3569
|
+
need: input.need ?? null
|
|
3570
|
+
});
|
|
3571
|
+
return { recorded: true };
|
|
3572
|
+
}
|
|
3136
3573
|
var SEVERITY_RANK, DAY_MS2;
|
|
3137
3574
|
var init_handlers = __esm({
|
|
3138
3575
|
"src/mcp/handlers.ts"() {
|
|
@@ -3143,6 +3580,8 @@ var init_handlers = __esm({
|
|
|
3143
3580
|
init_check();
|
|
3144
3581
|
init_packages();
|
|
3145
3582
|
init_verification();
|
|
3583
|
+
init_outcomes();
|
|
3584
|
+
init_successors2();
|
|
3146
3585
|
init_schema();
|
|
3147
3586
|
init_sources();
|
|
3148
3587
|
init_summarize();
|
|
@@ -3296,6 +3735,7 @@ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
|
|
|
3296
3735
|
const n = slots.length;
|
|
3297
3736
|
const budget = 5e4;
|
|
3298
3737
|
let nodes = 0;
|
|
3738
|
+
let exhausted = false;
|
|
3299
3739
|
let bestSelection = new Array(n).fill(0);
|
|
3300
3740
|
let bestRegret = conflictsFor(
|
|
3301
3741
|
slots.map((c) => c[0]).filter((m) => Boolean(m)),
|
|
@@ -3303,7 +3743,11 @@ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
|
|
|
3303
3743
|
).length === 0 ? 0 : Infinity;
|
|
3304
3744
|
const chosen = new Array(n).fill(0);
|
|
3305
3745
|
const dfs = (slot, regret) => {
|
|
3306
|
-
if (nodes++ > budget
|
|
3746
|
+
if (nodes++ > budget) {
|
|
3747
|
+
exhausted = true;
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
if (regret >= bestRegret) return;
|
|
3307
3751
|
if (slot === n) {
|
|
3308
3752
|
bestSelection = chosen.slice();
|
|
3309
3753
|
bestRegret = regret;
|
|
@@ -3316,7 +3760,10 @@ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
|
|
|
3316
3760
|
if (conflictsFor(assigned, sandboxConflicts).length === 0) {
|
|
3317
3761
|
dfs(slot + 1, regret + i);
|
|
3318
3762
|
}
|
|
3319
|
-
if (nodes > budget)
|
|
3763
|
+
if (nodes > budget) {
|
|
3764
|
+
exhausted = true;
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3320
3767
|
}
|
|
3321
3768
|
};
|
|
3322
3769
|
dfs(0, 0);
|
|
@@ -3324,7 +3771,8 @@ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
|
|
|
3324
3771
|
return {
|
|
3325
3772
|
selection: bestSelection,
|
|
3326
3773
|
conflicts: conflictsFor(members, sandboxConflicts),
|
|
3327
|
-
regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret
|
|
3774
|
+
regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret,
|
|
3775
|
+
bounded: exhausted
|
|
3328
3776
|
};
|
|
3329
3777
|
}
|
|
3330
3778
|
var init_optimize = __esm({
|
|
@@ -3569,17 +4017,20 @@ async function bundleSizes(db, candidates) {
|
|
|
3569
4017
|
async function decompose(document) {
|
|
3570
4018
|
const config = getConfig();
|
|
3571
4019
|
if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
|
|
3572
|
-
const llm = await decomposeWithLlm(
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
)
|
|
4020
|
+
const llm = await decomposeWithLlm(
|
|
4021
|
+
document,
|
|
4022
|
+
config.SUMMARY_API_KEY,
|
|
4023
|
+
config.SUMMARY_MODEL,
|
|
4024
|
+
config.SUMMARY_BASE_URL
|
|
4025
|
+
).catch((err) => {
|
|
4026
|
+
logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
|
|
4027
|
+
return null;
|
|
4028
|
+
});
|
|
3578
4029
|
if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
|
|
3579
4030
|
}
|
|
3580
4031
|
return { needs: decomposeHeuristic(document), source: "heuristic" };
|
|
3581
4032
|
}
|
|
3582
|
-
async function decomposeWithLlm(document, apiKey, model) {
|
|
4033
|
+
async function decomposeWithLlm(document, apiKey, model, baseUrl) {
|
|
3583
4034
|
const prompt = [
|
|
3584
4035
|
"Project description:",
|
|
3585
4036
|
document.slice(0, 8e3),
|
|
@@ -3587,8 +4038,8 @@ async function decomposeWithLlm(document, apiKey, model) {
|
|
|
3587
4038
|
'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
|
|
3588
4039
|
"One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
|
|
3589
4040
|
].join("\n");
|
|
3590
|
-
const { data } = await httpRequest("
|
|
3591
|
-
host:
|
|
4041
|
+
const { data } = await httpRequest(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
4042
|
+
host: new URL(baseUrl).host,
|
|
3592
4043
|
method: "POST",
|
|
3593
4044
|
ttlMs: 24 * 60 * 60 * 1e3,
|
|
3594
4045
|
// Hash the FULL document — length + a 64-char prefix collide for same-length
|
|
@@ -3666,6 +4117,80 @@ var init_plan = __esm({
|
|
|
3666
4117
|
}
|
|
3667
4118
|
});
|
|
3668
4119
|
|
|
4120
|
+
// src/mcp/metrics.ts
|
|
4121
|
+
function recordToolCall(tool, ok, ms) {
|
|
4122
|
+
const s = stats.get(tool) ?? { calls: 0, errors: 0, totalMs: 0 };
|
|
4123
|
+
s.calls += 1;
|
|
4124
|
+
if (!ok) s.errors += 1;
|
|
4125
|
+
s.totalMs += ms;
|
|
4126
|
+
stats.set(tool, s);
|
|
4127
|
+
}
|
|
4128
|
+
async function timed(tool, fn) {
|
|
4129
|
+
const start = Date.now();
|
|
4130
|
+
let ok = false;
|
|
4131
|
+
try {
|
|
4132
|
+
const result = await fn();
|
|
4133
|
+
ok = true;
|
|
4134
|
+
return result;
|
|
4135
|
+
} finally {
|
|
4136
|
+
recordToolCall(tool, ok, Date.now() - start);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
function renderPrometheus() {
|
|
4140
|
+
const lines = [
|
|
4141
|
+
"# HELP lurq_tool_calls_total Total MCP tool invocations.",
|
|
4142
|
+
"# TYPE lurq_tool_calls_total counter",
|
|
4143
|
+
"# HELP lurq_tool_errors_total MCP tool invocations that threw.",
|
|
4144
|
+
"# TYPE lurq_tool_errors_total counter",
|
|
4145
|
+
"# HELP lurq_tool_duration_ms_total Cumulative tool handler time in ms.",
|
|
4146
|
+
"# TYPE lurq_tool_duration_ms_total counter"
|
|
4147
|
+
];
|
|
4148
|
+
for (const [tool, s] of stats) {
|
|
4149
|
+
const label = `{tool="${tool}"}`;
|
|
4150
|
+
lines.push(`lurq_tool_calls_total${label} ${s.calls}`);
|
|
4151
|
+
lines.push(`lurq_tool_errors_total${label} ${s.errors}`);
|
|
4152
|
+
lines.push(`lurq_tool_duration_ms_total${label} ${s.totalMs}`);
|
|
4153
|
+
}
|
|
4154
|
+
return lines.join("\n") + "\n";
|
|
4155
|
+
}
|
|
4156
|
+
var stats;
|
|
4157
|
+
var init_metrics = __esm({
|
|
4158
|
+
"src/mcp/metrics.ts"() {
|
|
4159
|
+
"use strict";
|
|
4160
|
+
init_esm_shims();
|
|
4161
|
+
stats = /* @__PURE__ */ new Map();
|
|
4162
|
+
}
|
|
4163
|
+
});
|
|
4164
|
+
|
|
4165
|
+
// src/mcp/compact.ts
|
|
4166
|
+
function isEmptyObject(value) {
|
|
4167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
4168
|
+
}
|
|
4169
|
+
function compact(value) {
|
|
4170
|
+
if (Array.isArray(value)) {
|
|
4171
|
+
return value.map((v) => compact(v)).filter((v) => v !== null && v !== void 0);
|
|
4172
|
+
}
|
|
4173
|
+
if (value !== null && typeof value === "object") {
|
|
4174
|
+
const out = {};
|
|
4175
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
4176
|
+
if (raw === null || raw === void 0) continue;
|
|
4177
|
+
const cleaned = compact(raw);
|
|
4178
|
+
if (cleaned === null || cleaned === void 0) continue;
|
|
4179
|
+
if (Array.isArray(cleaned) && cleaned.length === 0) continue;
|
|
4180
|
+
if (isEmptyObject(cleaned)) continue;
|
|
4181
|
+
out[key] = cleaned;
|
|
4182
|
+
}
|
|
4183
|
+
return out;
|
|
4184
|
+
}
|
|
4185
|
+
return value;
|
|
4186
|
+
}
|
|
4187
|
+
var init_compact = __esm({
|
|
4188
|
+
"src/mcp/compact.ts"() {
|
|
4189
|
+
"use strict";
|
|
4190
|
+
init_esm_shims();
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4193
|
+
|
|
3669
4194
|
// src/mcp/server.ts
|
|
3670
4195
|
var server_exports = {};
|
|
3671
4196
|
__export(server_exports, {
|
|
@@ -3677,9 +4202,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3677
4202
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3678
4203
|
import { z as z2 } from "zod";
|
|
3679
4204
|
function json(obj) {
|
|
3680
|
-
return { content: [{ type: "text", text: JSON.stringify(obj) }] };
|
|
4205
|
+
return { content: [{ type: "text", text: JSON.stringify(compact(obj)) }] };
|
|
3681
4206
|
}
|
|
3682
|
-
function buildMcpServer(db) {
|
|
4207
|
+
function buildMcpServer(db, ctx = {}) {
|
|
3683
4208
|
const server = new McpServer({ name: SERVER_NAME, version: VERSION });
|
|
3684
4209
|
server.registerTool(
|
|
3685
4210
|
"recommend",
|
|
@@ -3692,7 +4217,7 @@ function buildMcpServer(db) {
|
|
|
3692
4217
|
constraints: constraintsSchema
|
|
3693
4218
|
}
|
|
3694
4219
|
},
|
|
3695
|
-
async (args) => json(await handleRecommend(db, args))
|
|
4220
|
+
async (args) => json(await timed("recommend", () => handleRecommend(db, args)))
|
|
3696
4221
|
);
|
|
3697
4222
|
server.registerTool(
|
|
3698
4223
|
"evaluate",
|
|
@@ -3703,7 +4228,7 @@ function buildMcpServer(db) {
|
|
|
3703
4228
|
package: npmName.describe("npm package name")
|
|
3704
4229
|
}
|
|
3705
4230
|
},
|
|
3706
|
-
async (args) => json(await handleEvaluate(db, args))
|
|
4231
|
+
async (args) => json(await timed("evaluate", () => handleEvaluate(db, args)))
|
|
3707
4232
|
);
|
|
3708
4233
|
server.registerTool(
|
|
3709
4234
|
"compare",
|
|
@@ -3714,7 +4239,7 @@ function buildMcpServer(db) {
|
|
|
3714
4239
|
packages: z2.array(npmName).min(2).max(5).describe("2\u20135 npm package names")
|
|
3715
4240
|
}
|
|
3716
4241
|
},
|
|
3717
|
-
async (args) => json(await handleCompare(db, args))
|
|
4242
|
+
async (args) => json(await timed("compare", () => handleCompare(db, args)))
|
|
3718
4243
|
);
|
|
3719
4244
|
server.registerTool(
|
|
3720
4245
|
"compat",
|
|
@@ -3725,7 +4250,7 @@ function buildMcpServer(db) {
|
|
|
3725
4250
|
packages: z2.array(npmName).min(2).max(8).describe("2\u20138 npm package names to check together")
|
|
3726
4251
|
}
|
|
3727
4252
|
},
|
|
3728
|
-
async (args) => json(await handleCompat(db, args))
|
|
4253
|
+
async (args) => json(await timed("compat", () => handleCompat(db, args)))
|
|
3729
4254
|
);
|
|
3730
4255
|
server.registerTool(
|
|
3731
4256
|
"verify",
|
|
@@ -3736,7 +4261,7 @@ function buildMcpServer(db) {
|
|
|
3736
4261
|
package: npmName.describe("npm package name to verify")
|
|
3737
4262
|
}
|
|
3738
4263
|
},
|
|
3739
|
-
async (args) => json(await handleVerify(db, args))
|
|
4264
|
+
async (args) => json(await timed("verify", () => handleVerify(db, args)))
|
|
3740
4265
|
);
|
|
3741
4266
|
server.registerTool(
|
|
3742
4267
|
"diagram",
|
|
@@ -3747,7 +4272,7 @@ function buildMcpServer(db) {
|
|
|
3747
4272
|
stack: z2.array(npmName).optional().describe("Package names that make up the stack; omit or empty to get usage guidance")
|
|
3748
4273
|
}
|
|
3749
4274
|
},
|
|
3750
|
-
async (args) => json(await handleDiagram(db, args))
|
|
4275
|
+
async (args) => json(await timed("diagram", () => handleDiagram(db, args)))
|
|
3751
4276
|
);
|
|
3752
4277
|
server.registerTool(
|
|
3753
4278
|
"plan",
|
|
@@ -3768,7 +4293,23 @@ function buildMcpServer(db) {
|
|
|
3768
4293
|
optimize: z2.enum(["speed", "balanced"]).optional().describe("'speed' prefers the lightest-bundle option per slot; default 'balanced'")
|
|
3769
4294
|
}
|
|
3770
4295
|
},
|
|
3771
|
-
async (args) => json(await handlePlan(db, args))
|
|
4296
|
+
async (args) => json(await timed("plan", () => handlePlan(db, args)))
|
|
4297
|
+
);
|
|
4298
|
+
server.registerTool(
|
|
4299
|
+
"report_outcome",
|
|
4300
|
+
{
|
|
4301
|
+
title: "Report a recommendation outcome",
|
|
4302
|
+
description: "Opt-in feedback after acting on a lurq recommendation: report whether you went with the package and whether it built. No source code \u2014 only the coarse decision + a build signal. Helps lurq learn which packages agents actually succeed with; safe to skip.",
|
|
4303
|
+
inputSchema: {
|
|
4304
|
+
package: npmName.describe("The package that was recommended"),
|
|
4305
|
+
accepted: z2.boolean().describe("Did you go with this package?"),
|
|
4306
|
+
buildSignal: z2.enum(["installed", "compiled", "tests_passed", "failed"]).optional().describe("Coarse post-install result, if known"),
|
|
4307
|
+
need: z2.string().max(500).optional().describe("The original need this was recommended for (no source code)")
|
|
4308
|
+
}
|
|
4309
|
+
},
|
|
4310
|
+
// ownerId comes from the authenticated key (ctx), NOT the tool arguments —
|
|
4311
|
+
// a caller must never be able to attribute an outcome to another org.
|
|
4312
|
+
async (args) => json(await timed("report_outcome", () => handleReportOutcome(db, args, ctx.ownerId ?? null)))
|
|
3772
4313
|
);
|
|
3773
4314
|
return server;
|
|
3774
4315
|
}
|
|
@@ -3800,6 +4341,8 @@ var init_server = __esm({
|
|
|
3800
4341
|
init_handlers();
|
|
3801
4342
|
init_diagram();
|
|
3802
4343
|
init_plan();
|
|
4344
|
+
init_metrics();
|
|
4345
|
+
init_compact();
|
|
3803
4346
|
categoryEnum = z2.enum(CATEGORIES);
|
|
3804
4347
|
confidenceEnum = z2.enum(["proven", "emerging", "promising", "unproven"]);
|
|
3805
4348
|
npmName = z2.string().trim().min(1).max(214).regex(/^(?:@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/i, "Invalid npm package name");
|
|
@@ -3834,12 +4377,27 @@ async function createKey(db, input = {}) {
|
|
|
3834
4377
|
}).returning();
|
|
3835
4378
|
return { key, row };
|
|
3836
4379
|
}
|
|
4380
|
+
function stampLastUsed(db, entry, now) {
|
|
4381
|
+
if (now - entry.lastStampAt < STAMP_INTERVAL_MS) return;
|
|
4382
|
+
entry.lastStampAt = now;
|
|
4383
|
+
db.update(apiKeys).set({ lastUsedAt: new Date(now) }).where(eq5(apiKeys.id, entry.row.id)).then(void 0, (err) => logger.debug(`lastUsedAt stamp failed: ${String(err)}`));
|
|
4384
|
+
}
|
|
3837
4385
|
async function lookupActiveKey(db, key) {
|
|
3838
4386
|
const hash = hashKey(key);
|
|
4387
|
+
const now = Date.now();
|
|
4388
|
+
const cached3 = authCache.get(hash);
|
|
4389
|
+
if (cached3 && now - cached3.cachedAt < AUTH_TTL_MS) {
|
|
4390
|
+
stampLastUsed(db, cached3, now);
|
|
4391
|
+
return cached3.row;
|
|
4392
|
+
}
|
|
3839
4393
|
const [row] = await db.select().from(apiKeys).where(and5(eq5(apiKeys.keyHash, hash), isNull(apiKeys.revokedAt))).limit(1);
|
|
3840
|
-
if (!row)
|
|
3841
|
-
|
|
3842
|
-
|
|
4394
|
+
if (!row) {
|
|
4395
|
+
authCache.delete(hash);
|
|
4396
|
+
return null;
|
|
4397
|
+
}
|
|
4398
|
+
const entry = { row, cachedAt: now, lastStampAt: 0 };
|
|
4399
|
+
authCache.set(hash, entry);
|
|
4400
|
+
stampLastUsed(db, entry, now);
|
|
3843
4401
|
return row;
|
|
3844
4402
|
}
|
|
3845
4403
|
async function listKeys(db) {
|
|
@@ -3864,31 +4422,61 @@ async function rotateKey(db, prefixOrId) {
|
|
|
3864
4422
|
await db.update(apiKeys).set({ revokedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, previous.id));
|
|
3865
4423
|
return { key, row, previous };
|
|
3866
4424
|
}
|
|
3867
|
-
var DISPLAY_BODY;
|
|
4425
|
+
var DISPLAY_BODY, authCache, AUTH_TTL_MS, STAMP_INTERVAL_MS;
|
|
3868
4426
|
var init_apiKeys = __esm({
|
|
3869
4427
|
"src/auth/apiKeys.ts"() {
|
|
3870
4428
|
"use strict";
|
|
3871
4429
|
init_esm_shims();
|
|
3872
4430
|
init_constants();
|
|
3873
4431
|
init_schema();
|
|
4432
|
+
init_logger();
|
|
3874
4433
|
DISPLAY_BODY = 6;
|
|
4434
|
+
authCache = /* @__PURE__ */ new Map();
|
|
4435
|
+
AUTH_TTL_MS = 6e4;
|
|
4436
|
+
STAMP_INTERVAL_MS = 6e4;
|
|
3875
4437
|
}
|
|
3876
4438
|
});
|
|
3877
4439
|
|
|
3878
4440
|
// src/mcp/http.ts
|
|
3879
4441
|
var http_exports = {};
|
|
3880
4442
|
__export(http_exports, {
|
|
4443
|
+
secretEquals: () => secretEquals,
|
|
3881
4444
|
startHttpServer: () => startHttpServer
|
|
3882
4445
|
});
|
|
4446
|
+
import { createHash as createHash6, timingSafeEqual } from "crypto";
|
|
3883
4447
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3884
4448
|
function rpcError(code, message) {
|
|
3885
4449
|
return { jsonrpc: "2.0", error: { code, message }, id: null };
|
|
3886
4450
|
}
|
|
4451
|
+
function secretEquals(a, b) {
|
|
4452
|
+
return timingSafeEqual(
|
|
4453
|
+
createHash6("sha256").update(a).digest(),
|
|
4454
|
+
createHash6("sha256").update(b).digest()
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
3887
4457
|
async function startHttpServer(opts = {}) {
|
|
3888
4458
|
const config = getConfig();
|
|
3889
4459
|
const port = opts.port ?? config.PORT;
|
|
3890
4460
|
const [{ default: express }, { default: helmet }, { rateLimit, ipKeyGenerator }] = await Promise.all([import("express"), import("helmet"), import("express-rate-limit")]);
|
|
3891
4461
|
const { db } = createDb({ max: 20 });
|
|
4462
|
+
if (!process.env.REDIS_URL) {
|
|
4463
|
+
logger.warn(
|
|
4464
|
+
"REDIS_URL not set \u2014 response caching is OFF; every request recomputes on the database. Set REDIS_URL before serving real traffic (and it also backs the rate limiter across instances)."
|
|
4465
|
+
);
|
|
4466
|
+
}
|
|
4467
|
+
let makeStore = null;
|
|
4468
|
+
if (process.env.REDIS_URL) {
|
|
4469
|
+
const [{ default: Redis }, { default: RedisStore }] = await Promise.all([
|
|
4470
|
+
import("ioredis"),
|
|
4471
|
+
import("rate-limit-redis")
|
|
4472
|
+
]);
|
|
4473
|
+
const rlRedis = new Redis(process.env.REDIS_URL, { maxRetriesPerRequest: 1, family: 0 });
|
|
4474
|
+
rlRedis.on("error", (err) => logger.warn(`rate-limit redis: ${err.message}`));
|
|
4475
|
+
makeStore = (prefix) => new RedisStore({
|
|
4476
|
+
prefix,
|
|
4477
|
+
sendCommand: (...args) => rlRedis.call(args[0], ...args.slice(1))
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
3892
4480
|
const app = express();
|
|
3893
4481
|
app.set("trust proxy", 1);
|
|
3894
4482
|
app.use(helmet());
|
|
@@ -3896,11 +4484,26 @@ async function startHttpServer(opts = {}) {
|
|
|
3896
4484
|
app.get("/healthz", (_req, res) => {
|
|
3897
4485
|
res.status(200).json({ status: "ok" });
|
|
3898
4486
|
});
|
|
4487
|
+
app.get("/metrics", (req, res) => {
|
|
4488
|
+
const token = config.LURQ_METRICS_TOKEN;
|
|
4489
|
+
if (!token) {
|
|
4490
|
+
res.status(404).end();
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
const header = req.headers.authorization;
|
|
4494
|
+
const presented = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
4495
|
+
if (presented !== token) {
|
|
4496
|
+
res.status(401).end();
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4499
|
+
res.type("text/plain").send(renderPrometheus());
|
|
4500
|
+
});
|
|
3899
4501
|
const ipLimiter = rateLimit({
|
|
3900
4502
|
windowMs: config.LURQ_RATE_LIMIT_WINDOW_MS,
|
|
3901
4503
|
limit: config.LURQ_IP_RATE_LIMIT_MAX,
|
|
3902
4504
|
standardHeaders: "draft-7",
|
|
3903
4505
|
legacyHeaders: false,
|
|
4506
|
+
...makeStore ? { store: makeStore("rl:ip:") } : {},
|
|
3904
4507
|
message: rpcError(-32029, "Rate limit exceeded.")
|
|
3905
4508
|
});
|
|
3906
4509
|
const auth = async (req, res, next) => {
|
|
@@ -3937,10 +4540,38 @@ async function startHttpServer(opts = {}) {
|
|
|
3937
4540
|
const id = req.lurqKey?.id;
|
|
3938
4541
|
return id != null ? `key:${id}` : ipKeyGenerator(req.ip ?? "0.0.0.0");
|
|
3939
4542
|
},
|
|
4543
|
+
...makeStore ? { store: makeStore("rl:key:") } : {},
|
|
3940
4544
|
message: rpcError(-32029, "Rate limit exceeded.")
|
|
3941
4545
|
});
|
|
4546
|
+
app.post("/keys", async (req, res) => {
|
|
4547
|
+
const secret = config.LURQ_ISSUER_SECRET;
|
|
4548
|
+
if (!secret) {
|
|
4549
|
+
res.status(404).end();
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
const header = req.headers.authorization;
|
|
4553
|
+
const token = header?.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
4554
|
+
if (!token || !secretEquals(token, secret)) {
|
|
4555
|
+
res.status(401).json({ error: "Invalid issuer secret." });
|
|
4556
|
+
return;
|
|
4557
|
+
}
|
|
4558
|
+
const body = req.body ?? {};
|
|
4559
|
+
const ownerId = typeof body.ownerId === "string" ? body.ownerId.trim() : "";
|
|
4560
|
+
if (!ownerId) {
|
|
4561
|
+
res.status(400).json({ error: "ownerId is required." });
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
const label = typeof body.label === "string" ? body.label.slice(0, 200) : void 0;
|
|
4565
|
+
try {
|
|
4566
|
+
const { key, row } = await createKey(db, { ownerId, label, tier: "free" });
|
|
4567
|
+
res.status(201).json({ key, prefix: row.prefix });
|
|
4568
|
+
} catch (err) {
|
|
4569
|
+
logger.error("key issuance failed:", err instanceof Error ? err.message : String(err));
|
|
4570
|
+
res.status(500).json({ error: "Could not issue key." });
|
|
4571
|
+
}
|
|
4572
|
+
});
|
|
3942
4573
|
app.post("/mcp", ipLimiter, auth, keyLimiter, async (req, res) => {
|
|
3943
|
-
const server = buildMcpServer(db);
|
|
4574
|
+
const server = buildMcpServer(db, { ownerId: req.lurqKey?.ownerId ?? null });
|
|
3944
4575
|
const transport = new StreamableHTTPServerTransport({
|
|
3945
4576
|
sessionIdGenerator: void 0,
|
|
3946
4577
|
enableJsonResponse: true
|
|
@@ -3973,6 +4604,7 @@ var init_http2 = __esm({
|
|
|
3973
4604
|
init_apiKeys();
|
|
3974
4605
|
init_client();
|
|
3975
4606
|
init_server();
|
|
4607
|
+
init_metrics();
|
|
3976
4608
|
}
|
|
3977
4609
|
});
|
|
3978
4610
|
|
|
@@ -4118,9 +4750,13 @@ async function runDiscovery(opts = {}) {
|
|
|
4118
4750
|
logger.info(
|
|
4119
4751
|
`Discovery: ${graph.length} graph + ${search.length} search candidates \u2192 ${enqueued} new queued.`
|
|
4120
4752
|
);
|
|
4121
|
-
const
|
|
4753
|
+
const pending2 = await getPendingCandidates(handle.db, cap * 4);
|
|
4122
4754
|
const scored = [];
|
|
4123
|
-
for (const cand of
|
|
4755
|
+
for (const cand of pending2) {
|
|
4756
|
+
if (cand.preScore !== null) {
|
|
4757
|
+
scored.push({ name: cand.name, preScore: cand.preScore });
|
|
4758
|
+
continue;
|
|
4759
|
+
}
|
|
4124
4760
|
const preScore = await preScorePackage(cand.name);
|
|
4125
4761
|
await setDiscoveryStatus(handle.db, cand.name, {
|
|
4126
4762
|
status: passesGate(preScore) ? "pending" : "rejected",
|
|
@@ -4128,7 +4764,7 @@ async function runDiscovery(opts = {}) {
|
|
|
4128
4764
|
});
|
|
4129
4765
|
if (passesGate(preScore)) scored.push({ name: cand.name, preScore });
|
|
4130
4766
|
}
|
|
4131
|
-
logger.info(`Discovery: gated ${
|
|
4767
|
+
logger.info(`Discovery: gated ${pending2.length}; ${scored.length} cleared the quality bar.`);
|
|
4132
4768
|
scored.sort((a, b) => b.preScore - a.preScore);
|
|
4133
4769
|
const toIngest = scored.slice(0, cap);
|
|
4134
4770
|
const deferred = scored.slice(cap);
|
|
@@ -4152,7 +4788,7 @@ async function runDiscovery(opts = {}) {
|
|
|
4152
4788
|
logger.info(`Discovery: ingested ${ingested}/${toIngest.length} candidate(s).`);
|
|
4153
4789
|
return {
|
|
4154
4790
|
enqueued,
|
|
4155
|
-
gated:
|
|
4791
|
+
gated: pending2.length,
|
|
4156
4792
|
passed: scored.length,
|
|
4157
4793
|
ingested,
|
|
4158
4794
|
droppedToNextRun: deferred.length
|
|
@@ -4572,14 +5208,134 @@ var init_local = __esm({
|
|
|
4572
5208
|
}
|
|
4573
5209
|
});
|
|
4574
5210
|
|
|
5211
|
+
// src/sandbox/e2b.ts
|
|
5212
|
+
var e2b_exports = {};
|
|
5213
|
+
__export(e2b_exports, {
|
|
5214
|
+
E2BSandbox: () => E2BSandbox,
|
|
5215
|
+
installCommand: () => installCommand,
|
|
5216
|
+
shQuote: () => shQuote,
|
|
5217
|
+
smokeCommand: () => smokeCommand
|
|
5218
|
+
});
|
|
5219
|
+
function shQuote(s) {
|
|
5220
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
5221
|
+
}
|
|
5222
|
+
function installCommand(specs, allowScripts) {
|
|
5223
|
+
const bad = specs.find((s) => s.startsWith("-"));
|
|
5224
|
+
if (bad) throw new Error(`Invalid package spec: ${bad}`);
|
|
5225
|
+
const flags = ["--no-audit", "--no-fund", "--no-package-lock", "--no-save"];
|
|
5226
|
+
if (!allowScripts) flags.push("--ignore-scripts");
|
|
5227
|
+
return `npm install ${specs.map(shQuote).join(" ")} ${flags.join(" ")}`;
|
|
5228
|
+
}
|
|
5229
|
+
function smokeCommand(pkg, moduleSystem) {
|
|
5230
|
+
const js = moduleSystem === "esm" ? `await import(${JSON.stringify(pkg)})` : `require(${JSON.stringify(pkg)})`;
|
|
5231
|
+
const flags = moduleSystem === "esm" ? "--input-type=module " : "";
|
|
5232
|
+
return `node ${flags}-e ${shQuote(js)}`;
|
|
5233
|
+
}
|
|
5234
|
+
function condense2(s) {
|
|
5235
|
+
return s.replace(/\s+/g, " ").trim().slice(0, ERROR_MAX2);
|
|
5236
|
+
}
|
|
5237
|
+
function errText(err) {
|
|
5238
|
+
if (err && typeof err === "object") {
|
|
5239
|
+
const e = err;
|
|
5240
|
+
if (typeof e.stderr === "string" && e.stderr.trim()) return e.stderr;
|
|
5241
|
+
if (typeof e.message === "string") return e.message;
|
|
5242
|
+
}
|
|
5243
|
+
return String(err);
|
|
5244
|
+
}
|
|
5245
|
+
var INSTALL_TIMEOUT_MS2, SMOKE_TIMEOUT_MS2, ERROR_MAX2, WORKDIR, toSpec2, E2BSandbox;
|
|
5246
|
+
var init_e2b = __esm({
|
|
5247
|
+
"src/sandbox/e2b.ts"() {
|
|
5248
|
+
"use strict";
|
|
5249
|
+
init_esm_shims();
|
|
5250
|
+
init_config();
|
|
5251
|
+
init_types2();
|
|
5252
|
+
INSTALL_TIMEOUT_MS2 = 12e4;
|
|
5253
|
+
SMOKE_TIMEOUT_MS2 = 3e4;
|
|
5254
|
+
ERROR_MAX2 = 500;
|
|
5255
|
+
WORKDIR = "/home/user";
|
|
5256
|
+
toSpec2 = (p) => p.version ? `${p.name}@${p.version}` : p.name;
|
|
5257
|
+
E2BSandbox = class {
|
|
5258
|
+
name = "e2b";
|
|
5259
|
+
async verify(pkg, version, opts = {}) {
|
|
5260
|
+
const set = await this.verifySet([{ name: pkg, version }], opts);
|
|
5261
|
+
return {
|
|
5262
|
+
driver: this.name,
|
|
5263
|
+
moduleSystem: set.moduleSystem,
|
|
5264
|
+
installed: set.installed,
|
|
5265
|
+
imported: set.loaded[0]?.loaded ?? null,
|
|
5266
|
+
ranScripts: opts.allowScripts ?? false,
|
|
5267
|
+
durationMs: set.durationMs,
|
|
5268
|
+
error: set.error
|
|
5269
|
+
};
|
|
5270
|
+
}
|
|
5271
|
+
async verifySet(packages2, opts = {}) {
|
|
5272
|
+
const config = getConfig();
|
|
5273
|
+
const target = opts.target ?? DEFAULT_TARGET;
|
|
5274
|
+
const allowScripts = opts.allowScripts ?? false;
|
|
5275
|
+
const specs = packages2.map(toSpec2);
|
|
5276
|
+
const installTimeout = opts.timeoutMs ?? INSTALL_TIMEOUT_MS2;
|
|
5277
|
+
const started = Date.now();
|
|
5278
|
+
const loaded = packages2.map((p) => ({ name: p.name, loaded: null }));
|
|
5279
|
+
let installed = false;
|
|
5280
|
+
let error = null;
|
|
5281
|
+
const install = installCommand(specs, allowScripts);
|
|
5282
|
+
const { default: Sandbox } = await import("e2b");
|
|
5283
|
+
const createOpts = {
|
|
5284
|
+
apiKey: config.E2B_API_KEY,
|
|
5285
|
+
timeoutMs: installTimeout + SMOKE_TIMEOUT_MS2 * packages2.length + 3e4
|
|
5286
|
+
};
|
|
5287
|
+
const sandbox = config.E2B_TEMPLATE ? await Sandbox.create(config.E2B_TEMPLATE, createOpts) : await Sandbox.create(createOpts);
|
|
5288
|
+
try {
|
|
5289
|
+
await sandbox.files.write(
|
|
5290
|
+
`${WORKDIR}/package.json`,
|
|
5291
|
+
JSON.stringify({ name: "lurq-sandbox", version: "0.0.0", private: true })
|
|
5292
|
+
);
|
|
5293
|
+
await sandbox.commands.run(install, { cwd: WORKDIR, timeoutMs: installTimeout });
|
|
5294
|
+
installed = true;
|
|
5295
|
+
for (let i = 0; i < packages2.length; i++) {
|
|
5296
|
+
try {
|
|
5297
|
+
await sandbox.commands.run(smokeCommand(packages2[i].name, target.moduleSystem), {
|
|
5298
|
+
cwd: WORKDIR,
|
|
5299
|
+
timeoutMs: SMOKE_TIMEOUT_MS2
|
|
5300
|
+
});
|
|
5301
|
+
loaded[i].loaded = true;
|
|
5302
|
+
} catch (err) {
|
|
5303
|
+
loaded[i].loaded = false;
|
|
5304
|
+
if (!error) error = condense2(errText(err));
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
} catch (err) {
|
|
5308
|
+
error = condense2(errText(err));
|
|
5309
|
+
} finally {
|
|
5310
|
+
await sandbox.kill().catch(() => {
|
|
5311
|
+
});
|
|
5312
|
+
}
|
|
5313
|
+
return {
|
|
5314
|
+
driver: this.name,
|
|
5315
|
+
moduleSystem: target.moduleSystem,
|
|
5316
|
+
installed,
|
|
5317
|
+
loaded,
|
|
5318
|
+
durationMs: Date.now() - started,
|
|
5319
|
+
error
|
|
5320
|
+
};
|
|
5321
|
+
}
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
});
|
|
5325
|
+
|
|
4575
5326
|
// src/sandbox/index.ts
|
|
4576
|
-
function getSandbox() {
|
|
5327
|
+
async function getSandbox() {
|
|
5328
|
+
if (getConfig().E2B_API_KEY) {
|
|
5329
|
+
const { E2BSandbox: E2BSandbox2 } = await Promise.resolve().then(() => (init_e2b(), e2b_exports));
|
|
5330
|
+
return new E2BSandbox2();
|
|
5331
|
+
}
|
|
4577
5332
|
return new LocalSandbox();
|
|
4578
5333
|
}
|
|
4579
5334
|
var init_sandbox = __esm({
|
|
4580
5335
|
"src/sandbox/index.ts"() {
|
|
4581
5336
|
"use strict";
|
|
4582
5337
|
init_esm_shims();
|
|
5338
|
+
init_config();
|
|
4583
5339
|
init_local();
|
|
4584
5340
|
init_types2();
|
|
4585
5341
|
init_local();
|
|
@@ -4592,7 +5348,7 @@ __export(sandbox_exports, {
|
|
|
4592
5348
|
verifyPackageInSandbox: () => verifyPackageInSandbox
|
|
4593
5349
|
});
|
|
4594
5350
|
async function verifyPackageInSandbox(db, pkg, version, opts = {}) {
|
|
4595
|
-
const result = await getSandbox().verify(pkg, version, opts);
|
|
5351
|
+
const result = await (await getSandbox()).verify(pkg, version, opts);
|
|
4596
5352
|
await storeVerificationRun(db, {
|
|
4597
5353
|
packageName: pkg,
|
|
4598
5354
|
version: version ?? "latest",
|
|
@@ -4656,7 +5412,7 @@ async function verifyCompatibility(db, packages2, opts = {}) {
|
|
|
4656
5412
|
version: (await getPackageByName(db, name))?.latestVersion ?? "latest"
|
|
4657
5413
|
}))
|
|
4658
5414
|
);
|
|
4659
|
-
const result = await getSandbox().verifySet(
|
|
5415
|
+
const result = await (await getSandbox()).verifySet(
|
|
4660
5416
|
resolved.map((r) => ({ name: r.name, version: r.version === "latest" ? null : r.version })),
|
|
4661
5417
|
{ allowScripts: opts.allowScripts }
|
|
4662
5418
|
);
|
|
@@ -4822,9 +5578,9 @@ not found: ${res.missing.join(", ")}`));
|
|
|
4822
5578
|
}
|
|
4823
5579
|
function runWeights(opts = {}) {
|
|
4824
5580
|
const w = loadWeights();
|
|
4825
|
-
const
|
|
5581
|
+
const active2 = activeWeightsPath();
|
|
4826
5582
|
if (opts.json) {
|
|
4827
|
-
console.log(JSON.stringify({ ...w, source:
|
|
5583
|
+
console.log(JSON.stringify({ ...w, source: active2?.source ?? "defaults" }, null, 2));
|
|
4828
5584
|
return;
|
|
4829
5585
|
}
|
|
4830
5586
|
const pct = (n) => n.toFixed(2);
|
|
@@ -4851,7 +5607,7 @@ function runWeights(opts = {}) {
|
|
|
4851
5607
|
])
|
|
4852
5608
|
);
|
|
4853
5609
|
console.log(dim(`
|
|
4854
|
-
Source: ${
|
|
5610
|
+
Source: ${active2 ? `${active2.source} (${active2.path})` : "defaults (no user overrides)"}`));
|
|
4855
5611
|
}
|
|
4856
5612
|
async function runEditWeights(opts) {
|
|
4857
5613
|
const { invalidateCache: invalidateCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
@@ -5499,7 +6255,11 @@ async function runKeysCreate(opts) {
|
|
|
5499
6255
|
requireConfig(["DATABASE_URL"]);
|
|
5500
6256
|
const { db, close } = createDb({ max: 1 });
|
|
5501
6257
|
try {
|
|
5502
|
-
const { key, row } = await createKey(db, {
|
|
6258
|
+
const { key, row } = await createKey(db, {
|
|
6259
|
+
label: opts.label,
|
|
6260
|
+
tier: opts.tier,
|
|
6261
|
+
ownerId: opts.owner
|
|
6262
|
+
});
|
|
5503
6263
|
await presentNewKey(key, row, opts);
|
|
5504
6264
|
} finally {
|
|
5505
6265
|
await close();
|
|
@@ -5797,7 +6557,7 @@ function buildProgram() {
|
|
|
5797
6557
|
await runInstallSkill2(opts);
|
|
5798
6558
|
});
|
|
5799
6559
|
const keys = program.command("keys").description("manage API keys for the hosted service (operator; needs DATABASE_URL)");
|
|
5800
|
-
keys.command("create").description("create a new API key (shown once; erased from the terminal after you copy it)").option("--label <label>", "human label (owner / org / purpose)").option("--tier <tier>", "tier name", "free").option("--json", "print the key as JSON and skip the interactive erase (for scripts)").action(async (opts) => {
|
|
6560
|
+
keys.command("create").description("create a new API key (shown once; erased from the terminal after you copy it)").option("--label <label>", "human label (owner / org / purpose)").option("--tier <tier>", "tier name", "free").option("--owner <id>", "org/owner id to attribute this key to (e.g. a Clerk org id)").option("--json", "print the key as JSON and skip the interactive erase (for scripts)").action(async (opts) => {
|
|
5801
6561
|
const { runKeysCreate: runKeysCreate2 } = await Promise.resolve().then(() => (init_keys(), keys_exports));
|
|
5802
6562
|
await runKeysCreate2(opts);
|
|
5803
6563
|
});
|
|
@@ -5838,8 +6598,8 @@ init_config();
|
|
|
5838
6598
|
// src/core/gate.ts
|
|
5839
6599
|
init_esm_shims();
|
|
5840
6600
|
init_constants();
|
|
5841
|
-
import { createHash as
|
|
5842
|
-
var PRE_LAUNCH =
|
|
6601
|
+
import { createHash as createHash7, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
6602
|
+
var PRE_LAUNCH = false;
|
|
5843
6603
|
var OWNER_HASH = "9675c37e56783e747e4a89ee3919d95d1364f2e5dd0f14501b0f878f8cc3682b";
|
|
5844
6604
|
var ALLOWED = /* @__PURE__ */ new Set([
|
|
5845
6605
|
"-v",
|
|
@@ -5858,9 +6618,9 @@ var ALLOWED = /* @__PURE__ */ new Set([
|
|
|
5858
6618
|
function isOwner() {
|
|
5859
6619
|
const key = process.env.LURQ_OWNER_KEY;
|
|
5860
6620
|
if (!key) return false;
|
|
5861
|
-
const got =
|
|
6621
|
+
const got = createHash7("sha256").update(key).digest();
|
|
5862
6622
|
const want = Buffer.from(OWNER_HASH, "hex");
|
|
5863
|
-
return got.length === want.length &&
|
|
6623
|
+
return got.length === want.length && timingSafeEqual2(got, want);
|
|
5864
6624
|
}
|
|
5865
6625
|
function enforceGate(argv) {
|
|
5866
6626
|
if (!PRE_LAUNCH || isOwner()) return;
|