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/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.2";
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(config.SUMMARY_API_KEY, config.SUMMARY_MODEL, fetchImpl);
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("https://api.openai.com/v1/chat/completions", {
1659
- host: "api.openai.com",
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 active = activeWeightsPath();
1847
- if (active) {
1985
+ const active2 = activeWeightsPath();
1986
+ if (active2) {
1848
1987
  try {
1849
- const fromFile = JSON.parse(readFileSync(active.path, "utf8"));
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(config.EMBEDDING_API_KEY, config.EMBEDDING_MODEL, fetchImpl);
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("https://api.openai.com/v1/embeddings", {
2358
- host: "api.openai.com",
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
- const row = await syncOnePackage(db, name);
2709
- if (row.confidence && row.confidence !== "unproven") {
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 ? `"${input.package}" exists on npm but could not be scored right now; try again.` : `"${input.package}" was not found on the npm registry. Check the package name.`
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 { dataAsOf: await latestDataAsOf(db), rows, ...missing.length ? { missing } : {} };
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 || regret >= bestRegret) return;
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) return;
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(document, config.SUMMARY_API_KEY, config.SUMMARY_MODEL).catch(
3572
- (err) => {
3573
- logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
3574
- return null;
3575
- }
3576
- );
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("https://api.openai.com/v1/chat/completions", {
3590
- host: "api.openai.com",
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) return null;
3840
- db.update(apiKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq5(apiKeys.id, row.id)).then(void 0, () => {
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 pending = await getPendingCandidates(handle.db, cap * 4);
4752
+ const pending2 = await getPendingCandidates(handle.db, cap * 4);
4121
4753
  const scored = [];
4122
- for (const cand of pending) {
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 ${pending.length}; ${scored.length} cleared the quality bar.`);
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: pending.length,
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 active = activeWeightsPath();
5580
+ const active2 = activeWeightsPath();
4825
5581
  if (opts.json) {
4826
- console.log(JSON.stringify({ ...w, source: active?.source ?? "defaults" }, null, 2));
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: ${active ? `${active.source} (${active.path})` : "defaults (no user overrides)"}`));
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, { label: opts.label, tier: opts.tier });
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
  });