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