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