lurqrun 0.1.6 → 0.1.7

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.
Files changed (206) hide show
  1. package/README.md +18 -10
  2. package/dist/auditHandler-JCOJSUYW.js +23 -0
  3. package/dist/bin/lurq.js +3090 -23306
  4. package/dist/browserAuth-B6G27USY.js +95 -0
  5. package/dist/cache-DKI34SVS.js +12 -0
  6. package/dist/check-2FDGUOV7.js +192 -0
  7. package/dist/checkUpgrade-F2WESBPM.js +103 -0
  8. package/dist/chunk-2VMOWSRY.js +300 -0
  9. package/dist/chunk-3GHJR7XR.js +2049 -0
  10. package/dist/chunk-3N3Q3DSW.js +214 -0
  11. package/dist/chunk-4ESO33J6.js +34 -0
  12. package/dist/chunk-4F7ZESPO.js +132 -0
  13. package/dist/chunk-4KCPOMG6.js +68 -0
  14. package/dist/chunk-4ZYCIMY4.js +155 -0
  15. package/dist/chunk-63MUJNZL.js +318 -0
  16. package/dist/chunk-6AAEVQ7I.js +6336 -0
  17. package/dist/chunk-6EQ3ARXA.js +304 -0
  18. package/dist/chunk-6L3U7DZ5.js +23 -0
  19. package/dist/chunk-AHBP533R.js +220 -0
  20. package/dist/chunk-B43LCXBM.js +223 -0
  21. package/dist/chunk-BCANRN5B.js +33 -0
  22. package/dist/chunk-BUALU27S.js +123 -0
  23. package/dist/chunk-BULZAX2U.js +1554 -0
  24. package/dist/chunk-BV32UNLL.js +154 -0
  25. package/dist/chunk-C6GLE626.js +160 -0
  26. package/dist/chunk-CIDWNUAP.js +145 -0
  27. package/dist/chunk-CUP36ND3.js +2126 -0
  28. package/dist/chunk-CW5HJ3LX.js +96 -0
  29. package/dist/chunk-DB2VF5UD.js +315 -0
  30. package/dist/chunk-DWAGCNEL.js +210 -0
  31. package/dist/chunk-EA3DP5HG.js +293 -0
  32. package/dist/chunk-ELGCAM7P.js +184 -0
  33. package/dist/chunk-FW6BD2NO.js +1915 -0
  34. package/dist/chunk-GE37NVLN.js +15 -0
  35. package/dist/chunk-GPTKSNR6.js +171 -0
  36. package/dist/chunk-GRJXQBQZ.js +793 -0
  37. package/dist/chunk-HWBBVLOT.js +92 -0
  38. package/dist/chunk-I2JUKN6Z.js +24 -0
  39. package/dist/chunk-I3M2KJXB.js +56 -0
  40. package/dist/chunk-IQ4SDKKY.js +733 -0
  41. package/dist/chunk-J5FZYN4A.js +116 -0
  42. package/dist/chunk-L6N4JFW2.js +263 -0
  43. package/dist/chunk-LBFGSSF2.js +126 -0
  44. package/dist/chunk-LEGPB7DO.js +81 -0
  45. package/dist/chunk-LO35HLQZ.js +219 -0
  46. package/dist/chunk-LRD4GBYE.js +73 -0
  47. package/dist/chunk-LUAKDHLZ.js +40 -0
  48. package/dist/chunk-LYMPYETA.js +965 -0
  49. package/dist/chunk-M6NYEXCW.js +38 -0
  50. package/dist/chunk-MHUEVBVO.js +35 -0
  51. package/dist/chunk-MYGSLK44.js +37 -0
  52. package/dist/chunk-NH5I4KHH.js +201 -0
  53. package/dist/chunk-OQS4GUSI.js +1391 -0
  54. package/dist/chunk-PSX3GIAN.js +176 -0
  55. package/dist/chunk-Q5VPF4WI.js +26 -0
  56. package/dist/chunk-QKGYDMWB.js +7409 -0
  57. package/dist/chunk-QSL6QN5X.js +115 -0
  58. package/dist/chunk-SHCEBP3J.js +107 -0
  59. package/dist/chunk-SJOVFF7B.js +201 -0
  60. package/dist/chunk-ST2X2GYN.js +373 -0
  61. package/dist/chunk-TNTXWSYA.js +19 -0
  62. package/dist/chunk-U5AT3TD4.js +2128 -0
  63. package/dist/chunk-UDVNA7EA.js +483 -0
  64. package/dist/chunk-VL2E5T2A.js +109 -0
  65. package/dist/chunk-VLRHFMGJ.js +58 -0
  66. package/dist/chunk-WHX2ZFCG.js +4048 -0
  67. package/dist/chunk-WQ23REYT.js +37 -0
  68. package/dist/chunk-XBIDMHSK.js +15425 -0
  69. package/dist/chunk-Y5GI7RL3.js +66 -0
  70. package/dist/client-IRKAMDYZ.js +15 -0
  71. package/dist/commands-FIL5ET6P.js +911 -0
  72. package/dist/compat-R3UTFQ5Q.js +201 -0
  73. package/dist/dashboardIssue-WYYMBFIB.js +192 -0
  74. package/dist/diff-GZA2774U.js +13 -0
  75. package/dist/dist-4XXIXHHZ.js +12945 -0
  76. package/dist/e2b-FVPHDULZ.js +172 -0
  77. package/dist/extract-NKGKLIVT.js +444 -0
  78. package/dist/fetch-QYTUJVG2.js +22 -0
  79. package/dist/firstTouch-QQTJH4QX.js +55 -0
  80. package/dist/handlers-GEJAVZDK.js +53 -0
  81. package/dist/http-ETPYMBGD.js +5360 -0
  82. package/dist/ingestQueue-VKN4KBIF.js +35 -0
  83. package/dist/install-EK3W2COX.js +20 -0
  84. package/dist/installSkill-VQJWJVCL.js +52 -0
  85. package/dist/inventory-5AWSLJYU.js +31 -0
  86. package/dist/local-XPZMRH5R.js +13 -0
  87. package/dist/mcp-3JHD5SUZ.js +472 -0
  88. package/dist/mcpHandlers-QFN4MCFL.js +22 -0
  89. package/dist/mcpScan-QZHWENIP.js +1871 -0
  90. package/dist/mcpScanWorkflow-JPYCBGC4.js +81 -0
  91. package/dist/mcpStack-BYJCZNIV.js +20 -0
  92. package/dist/packages-THVFMWFP.js +42 -0
  93. package/dist/plan-V4OEI3J4.js +463 -0
  94. package/dist/planView-JRXGCG2N.js +79 -0
  95. package/dist/policy-3P6BGRDN.js +115 -0
  96. package/dist/references-6N7VEPHT.js +14 -0
  97. package/dist/release-4U4ABNSJ.js +128 -0
  98. package/dist/remote-YGCXYI4V.js +32 -0
  99. package/dist/reportRuns-CKQQYLY4.js +64 -0
  100. package/dist/sandbox-MEZLAODF.js +39 -0
  101. package/dist/server-KFUD5X57.js +55 -0
  102. package/dist/surface-V62GE554.js +141 -0
  103. package/dist/surface-YL6O67W3.js +36 -0
  104. package/dist/surfaceHandlers-X2IZKA3E.js +20 -0
  105. package/dist/typecheck-X7U5ELMR.js +268 -0
  106. package/dist/types-M5JP2W3D.js +10 -0
  107. package/dist/uninstall-P7RGBX6G.js +174 -0
  108. package/dist/upgrade-NQ5IWFZB.js +596 -0
  109. package/dist/upgradePlan-A4LRKLEG.js +146 -0
  110. package/dist/userConfig-Z3GQE776.js +20 -0
  111. package/dist/watch-DGFMWUDL.js +214 -0
  112. package/package.json +26 -21
  113. package/templates/agent-rules.md +4 -4
  114. package/templates/skill-instructions.md +13 -14
  115. package/dist/bin/lurq.js.map +0 -1
  116. package/dist/index.d.ts +0 -805
  117. package/dist/index.js +0 -23570
  118. package/dist/index.js.map +0 -1
  119. package/drizzle/0000_confused_fabian_cortez.sql +0 -54
  120. package/drizzle/0001_aspiring_timeslip.sql +0 -15
  121. package/drizzle/0002_lyrical_lila_cheney.sql +0 -14
  122. package/drizzle/0003_violet_giant_girl.sql +0 -14
  123. package/drizzle/0004_slim_valeria_richards.sql +0 -15
  124. package/drizzle/0005_petite_luke_cage.sql +0 -12
  125. package/drizzle/0006_calm_nighthawk.sql +0 -3
  126. package/drizzle/0007_slimy_naoko.sql +0 -1
  127. package/drizzle/0008_peaceful_tigra.sql +0 -1
  128. package/drizzle/0009_smooth_harrier.sql +0 -10
  129. package/drizzle/0010_curved_amphibian.sql +0 -2
  130. package/drizzle/0011_compat_provenance_closures.sql +0 -11
  131. package/drizzle/0012_api_surfaces.sql +0 -9
  132. package/drizzle/0013_nice_thanos.sql +0 -1
  133. package/drizzle/0014_luxuriant_malcolm_colcord.sql +0 -10
  134. package/drizzle/0015_past_fat_cobra.sql +0 -11
  135. package/drizzle/0016_futuristic_junta.sql +0 -50
  136. package/drizzle/0017_absent_grandmaster.sql +0 -20
  137. package/drizzle/0018_rainy_proemial_gods.sql +0 -1
  138. package/drizzle/0019_dashing_korvac.sql +0 -11
  139. package/drizzle/0020_normal_vulture.sql +0 -3
  140. package/drizzle/0021_dazzling_blue_shield.sql +0 -17
  141. package/drizzle/0022_massive_ghost_rider.sql +0 -24
  142. package/drizzle/0023_next_shatterstar.sql +0 -1
  143. package/drizzle/0024_dusty_maelstrom.sql +0 -15
  144. package/drizzle/0025_gifted_harpoon.sql +0 -5
  145. package/drizzle/0026_awesome_mordo.sql +0 -1
  146. package/drizzle/0027_charming_ulik.sql +0 -2
  147. package/drizzle/0028_romantic_loners.sql +0 -2
  148. package/drizzle/0029_busy_korvac.sql +0 -16
  149. package/drizzle/0030_steady_mathemanic.sql +0 -6
  150. package/drizzle/0031_redundant_maginty.sql +0 -2
  151. package/drizzle/0032_stack_resolutions.sql +0 -14
  152. package/drizzle/0033_graceful_tyger_tiger.sql +0 -3
  153. package/drizzle/0034_uneven_thunderbird.sql +0 -1
  154. package/drizzle/0035_api_key_scopes.sql +0 -1
  155. package/drizzle/0036_policy_audit.sql +0 -21
  156. package/drizzle/0037_subscription_seats.sql +0 -1
  157. package/drizzle/0038_symbol_offsets.sql +0 -2
  158. package/drizzle/0039_mcp_runtime_scans.sql +0 -80
  159. package/drizzle/0040_subscription_overage_and_interval.sql +0 -4
  160. package/drizzle/0041_account_email.sql +0 -38
  161. package/drizzle/0042_alert_channels.sql +0 -21
  162. package/drizzle/meta/0000_snapshot.json +0 -370
  163. package/drizzle/meta/0001_snapshot.json +0 -482
  164. package/drizzle/meta/0002_snapshot.json +0 -575
  165. package/drizzle/meta/0003_snapshot.json +0 -667
  166. package/drizzle/meta/0004_snapshot.json +0 -769
  167. package/drizzle/meta/0005_snapshot.json +0 -864
  168. package/drizzle/meta/0006_snapshot.json +0 -882
  169. package/drizzle/meta/0007_snapshot.json +0 -876
  170. package/drizzle/meta/0008_snapshot.json +0 -882
  171. package/drizzle/meta/0009_snapshot.json +0 -948
  172. package/drizzle/meta/0010_snapshot.json +0 -969
  173. package/drizzle/meta/0011_snapshot.json +0 -1049
  174. package/drizzle/meta/0012_snapshot.json +0 -1115
  175. package/drizzle/meta/0013_snapshot.json +0 -1121
  176. package/drizzle/meta/0014_snapshot.json +0 -1190
  177. package/drizzle/meta/0015_snapshot.json +0 -1280
  178. package/drizzle/meta/0016_snapshot.json +0 -1683
  179. package/drizzle/meta/0017_snapshot.json +0 -1834
  180. package/drizzle/meta/0018_snapshot.json +0 -1834
  181. package/drizzle/meta/0019_snapshot.json +0 -1909
  182. package/drizzle/meta/0020_snapshot.json +0 -1921
  183. package/drizzle/meta/0021_snapshot.json +0 -2045
  184. package/drizzle/meta/0022_snapshot.json +0 -2246
  185. package/drizzle/meta/0023_snapshot.json +0 -2252
  186. package/drizzle/meta/0024_snapshot.json +0 -2375
  187. package/drizzle/meta/0025_snapshot.json +0 -2407
  188. package/drizzle/meta/0026_snapshot.json +0 -2413
  189. package/drizzle/meta/0027_snapshot.json +0 -2425
  190. package/drizzle/meta/0028_snapshot.json +0 -2438
  191. package/drizzle/meta/0029_snapshot.json +0 -2546
  192. package/drizzle/meta/0030_snapshot.json +0 -2586
  193. package/drizzle/meta/0031_snapshot.json +0 -2598
  194. package/drizzle/meta/0032_snapshot.json +0 -2699
  195. package/drizzle/meta/0033_snapshot.json +0 -2712
  196. package/drizzle/meta/0034_snapshot.json +0 -2719
  197. package/drizzle/meta/0035_snapshot.json +0 -2726
  198. package/drizzle/meta/0036_snapshot.json +0 -2876
  199. package/drizzle/meta/0037_snapshot.json +0 -2883
  200. package/drizzle/meta/0038_snapshot.json +0 -2895
  201. package/drizzle/meta/0039_snapshot.json +0 -3501
  202. package/drizzle/meta/0040_snapshot.json +0 -3527
  203. package/drizzle/meta/0041_snapshot.json +0 -3824
  204. package/drizzle/meta/0042_snapshot.json +0 -3960
  205. package/drizzle/meta/_journal.json +0 -307
  206. package/src/data/seed.json +0 -253
@@ -0,0 +1,81 @@
1
+ import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
2
+ import {
3
+ cliSpec
4
+ } from "./chunk-B43LCXBM.js";
5
+ import "./chunk-GPTKSNR6.js";
6
+ import {
7
+ init_esm_shims
8
+ } from "./chunk-I3M2KJXB.js";
9
+
10
+ // src/github/mcpScanWorkflow.ts
11
+ init_esm_shims();
12
+ var MCP_SCAN_WORKFLOW_PATH = ".github/workflows/lurq-mcp-scan.yml";
13
+ var DEFAULT_CRON = "23 6 * * *";
14
+ var CONFIG_PATHS = [".mcp.json", ".cursor/mcp.json", ".vscode/mcp.json", ".windsurf/mcp.json"];
15
+ var VALID_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ function secretNameFor(envName) {
17
+ return envName.toUpperCase().startsWith("GITHUB_") ? `MCP_${envName}` : envName;
18
+ }
19
+ function renderMcpScanWorkflow(opts = {}) {
20
+ const cron = opts.cron ?? DEFAULT_CRON;
21
+ if (cron.trim().split(/\s+/).length !== 5) throw new Error(`cron must have five fields; got "${cron}"`);
22
+ const failOn = opts.failOn ?? "high";
23
+ const issue = opts.githubIssue !== false;
24
+ const secrets = [...new Set(opts.secrets ?? [])].filter((s) => VALID_NAME.test(s) && s !== "LURQ_API_KEY").sort();
25
+ const serverWantsGithubToken = secrets.includes("GITHUB_TOKEN");
26
+ const env = [
27
+ " LURQ_API_KEY: ${{ secrets.LURQ_API_KEY }}",
28
+ ...issue && !serverWantsGithubToken ? [" GITHUB_TOKEN: ${{ github.token }}"] : [],
29
+ ...secrets.map((s) => ` ${s}: \${{ secrets.${secretNameFor(s)} }}`)
30
+ ].join("\n");
31
+ const uv = opts.needsUv ? `
32
+ # A server here launches with uvx.
33
+ - uses: astral-sh/setup-uv@v6
34
+ ` : "";
35
+ const permissions = issue ? `# Contents are read-only; nothing the scan reads can change code. The one write
36
+ # keeps the pinned "lurq dashboard" issue current, with server text escaped.
37
+ permissions:
38
+ contents: read
39
+ issues: write` : `# Read-only. The scan reads tool contracts; nothing it reads can write here.
40
+ permissions:
41
+ contents: read`;
42
+ return `# Rescans the MCP servers committed to this repository: what each one exposes,
43
+ # what its tools tell your agents, and what changed since the last scan.
44
+ # Generated by \`lurq mcp-ci\`. Delete this file to stop it.
45
+ name: lurq mcp scan
46
+
47
+ on:
48
+ schedule:
49
+ - cron: '${cron}'
50
+ pull_request:
51
+ paths:
52
+ ${CONFIG_PATHS.map((p) => ` - '${p}'`).join("\n")}
53
+ workflow_dispatch: {}
54
+
55
+ ${permissions}
56
+
57
+ jobs:
58
+ scan:
59
+ runs-on: ubuntu-latest
60
+ timeout-minutes: 15
61
+ steps:
62
+ - uses: actions/checkout@v6
63
+ - uses: actions/setup-node@v4
64
+ with:
65
+ node-version: 22
66
+ ${uv}
67
+ # Add each credential below as a repository secret (Settings \u2192 Secrets).
68
+ # A missing one is reported as "needs config", never as a broken server.
69
+ # --require-upload fails the job when the scan could not be recorded, so a
70
+ # green run always means the dashboard's history has today's scan in it.
71
+ - name: Scan MCP servers
72
+ run: npx -y ${cliSpec()} mcp-scan --project-only --trust-project --require-upload${issue ? " --github-issue" : ""} --fail-on ${failOn}
73
+ env:
74
+ ${env}
75
+ `;
76
+ }
77
+ export {
78
+ MCP_SCAN_WORKFLOW_PATH,
79
+ renderMcpScanWorkflow,
80
+ secretNameFor
81
+ };
@@ -0,0 +1,20 @@
1
+ import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
2
+ import {
3
+ CROWDED_TOOL_COUNT,
4
+ TOKENS_PER_TOOL,
5
+ analyzeStack,
6
+ checkMcpStack,
7
+ isCrowded,
8
+ loadStackMembers
9
+ } from "./chunk-VL2E5T2A.js";
10
+ import "./chunk-6EQ3ARXA.js";
11
+ import "./chunk-SHCEBP3J.js";
12
+ import "./chunk-I3M2KJXB.js";
13
+ export {
14
+ CROWDED_TOOL_COUNT,
15
+ TOKENS_PER_TOOL,
16
+ analyzeStack,
17
+ checkMcpStack,
18
+ isCrowded,
19
+ loadStackMembers
20
+ };
@@ -0,0 +1,42 @@
1
+ import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
2
+ import {
3
+ ensureSeedEntry,
4
+ finishSyncRun,
5
+ getAllPackageNames,
6
+ getContributionsByOwner,
7
+ getPackageByName,
8
+ getPackageNamesCreatedSince,
9
+ getPackageVersions,
10
+ getSeedTargets,
11
+ getStaleRefreshTargets,
12
+ getTopPackageNames,
13
+ latestVersionsFor,
14
+ reusableFieldsFor,
15
+ stampFirstRequester,
16
+ startSyncRun,
17
+ upsertPackage,
18
+ upsertPackageVersions
19
+ } from "./chunk-NH5I4KHH.js";
20
+ import "./chunk-VLRHFMGJ.js";
21
+ import "./chunk-LYMPYETA.js";
22
+ import "./chunk-MYGSLK44.js";
23
+ import "./chunk-GPTKSNR6.js";
24
+ import "./chunk-I3M2KJXB.js";
25
+ export {
26
+ ensureSeedEntry,
27
+ finishSyncRun,
28
+ getAllPackageNames,
29
+ getContributionsByOwner,
30
+ getPackageByName,
31
+ getPackageNamesCreatedSince,
32
+ getPackageVersions,
33
+ getSeedTargets,
34
+ getStaleRefreshTargets,
35
+ getTopPackageNames,
36
+ latestVersionsFor,
37
+ reusableFieldsFor,
38
+ stampFirstRequester,
39
+ startSyncRun,
40
+ upsertPackage,
41
+ upsertPackageVersions
42
+ };
@@ -0,0 +1,463 @@
1
+ import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
2
+ import {
3
+ buildMermaid,
4
+ layerFor
5
+ } from "./chunk-4F7ZESPO.js";
6
+ import {
7
+ checkCompat,
8
+ latestDataAsOf,
9
+ recommend
10
+ } from "./chunk-BULZAX2U.js";
11
+ import "./chunk-QSL6QN5X.js";
12
+ import "./chunk-M6NYEXCW.js";
13
+ import "./chunk-6L3U7DZ5.js";
14
+ import "./chunk-J5FZYN4A.js";
15
+ import "./chunk-PSX3GIAN.js";
16
+ import {
17
+ assembleMembers,
18
+ getOrFetchPackage,
19
+ inferCategory
20
+ } from "./chunk-FW6BD2NO.js";
21
+ import {
22
+ resolveArchitectureCompat
23
+ } from "./chunk-BUALU27S.js";
24
+ import "./chunk-LRD4GBYE.js";
25
+ import "./chunk-BCANRN5B.js";
26
+ import "./chunk-2VMOWSRY.js";
27
+ import "./chunk-4ESO33J6.js";
28
+ import {
29
+ getSandboxEdges
30
+ } from "./chunk-CW5HJ3LX.js";
31
+ import "./chunk-AHBP533R.js";
32
+ import {
33
+ httpRequest
34
+ } from "./chunk-SJOVFF7B.js";
35
+ import "./chunk-HWBBVLOT.js";
36
+ import "./chunk-NH5I4KHH.js";
37
+ import {
38
+ DEFAULT_ECOSYSTEM,
39
+ isCategory
40
+ } from "./chunk-VLRHFMGJ.js";
41
+ import "./chunk-WQ23REYT.js";
42
+ import {
43
+ packages
44
+ } from "./chunk-LYMPYETA.js";
45
+ import "./chunk-3GHJR7XR.js";
46
+ import {
47
+ getConfig
48
+ } from "./chunk-GRJXQBQZ.js";
49
+ import "./chunk-WHX2ZFCG.js";
50
+ import "./chunk-LUAKDHLZ.js";
51
+ import {
52
+ logger
53
+ } from "./chunk-MYGSLK44.js";
54
+ import "./chunk-GPTKSNR6.js";
55
+ import {
56
+ init_esm_shims
57
+ } from "./chunk-I3M2KJXB.js";
58
+
59
+ // src/mcp/plan.ts
60
+ init_esm_shims();
61
+ import { createHash } from "crypto";
62
+ import { and, eq, inArray } from "drizzle-orm";
63
+
64
+ // src/compat/optimize.ts
65
+ init_esm_shims();
66
+ function conflictsFor(members, sandboxConflicts) {
67
+ const out = resolveArchitectureCompat(members);
68
+ for (let i = 0; i < members.length; i++) {
69
+ for (let j = i + 1; j < members.length; j++) {
70
+ const a = members[i].name;
71
+ const b = members[j].name;
72
+ const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
73
+ if (sandboxConflicts.has(key)) {
74
+ out.push({
75
+ source: "sandbox",
76
+ packages: [a, b],
77
+ detail: `${a} and ${b} are recorded as incompatible (sandbox)`
78
+ });
79
+ }
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+ function optimizeStack(slots, sandboxConflicts = /* @__PURE__ */ new Set()) {
85
+ if (slots.some((s) => s.length === 0)) {
86
+ throw new Error("optimizeStack: every slot must have at least one candidate");
87
+ }
88
+ const n = slots.length;
89
+ const budget = 5e4;
90
+ let nodes = 0;
91
+ let exhausted = false;
92
+ let bestSelection = new Array(n).fill(0);
93
+ let bestRegret = conflictsFor(
94
+ slots.map((c) => c[0]).filter((m) => Boolean(m)),
95
+ sandboxConflicts
96
+ ).length === 0 ? 0 : Infinity;
97
+ const chosen = new Array(n).fill(0);
98
+ const dfs = (slot, regret) => {
99
+ if (nodes++ > budget) {
100
+ exhausted = true;
101
+ return;
102
+ }
103
+ if (regret >= bestRegret) return;
104
+ if (slot === n) {
105
+ bestSelection = chosen.slice();
106
+ bestRegret = regret;
107
+ return;
108
+ }
109
+ const candidates = slots[slot];
110
+ for (let i = 0; i < candidates.length; i++) {
111
+ chosen[slot] = i;
112
+ const assigned = chosen.slice(0, slot + 1).map((idx, s) => slots[s][idx]);
113
+ if (conflictsFor(assigned, sandboxConflicts).length === 0) {
114
+ dfs(slot + 1, regret + i);
115
+ }
116
+ if (nodes > budget) {
117
+ exhausted = true;
118
+ return;
119
+ }
120
+ }
121
+ };
122
+ dfs(0, 0);
123
+ const members = bestSelection.map((idx, s) => slots[s][idx]);
124
+ return {
125
+ selection: bestSelection,
126
+ conflicts: conflictsFor(members, sandboxConflicts),
127
+ regret: bestRegret === Infinity ? bestSelection.reduce((a, b) => a + b, 0) : bestRegret,
128
+ bounded: exhausted
129
+ };
130
+ }
131
+
132
+ // src/mcp/plan.ts
133
+ function packageToCandidate(row) {
134
+ return {
135
+ name: row.name,
136
+ category: row.category,
137
+ healthScore: row.healthScore ?? 0,
138
+ qualityScore: row.qualityScore,
139
+ confidence: row.confidence ?? "unproven",
140
+ why: "pinned by you",
141
+ latestVersion: row.latestVersion,
142
+ weeklyDownloads: row.weeklyDownloads,
143
+ lastReleaseAt: row.lastReleaseAt ? row.lastReleaseAt.toISOString() : null,
144
+ repoUrl: row.repoUrl,
145
+ // A pin is the one slot lurq did not choose, which makes these MORE worth
146
+ // saying, not less: the user picked it, so nobody else is going to notice
147
+ // that it has been deprecated since they did.
148
+ deprecated: row.deprecated,
149
+ archived: row.archived,
150
+ advisories: row.advisories?.length ?? 0
151
+ };
152
+ }
153
+ async function resolvePins(db, using, recommendedNames, requestedByOwnerId = null) {
154
+ const slots = [];
155
+ const notFound = [];
156
+ const pending = [];
157
+ for (const name of new Set(using ?? [])) {
158
+ if (recommendedNames.has(name)) continue;
159
+ const { row, existsOnNpm } = await getOrFetchPackage(db, name, { requestedByOwnerId });
160
+ if (!row) {
161
+ (existsOnNpm ? pending : notFound).push(name);
162
+ continue;
163
+ }
164
+ slots.push({
165
+ need: `using ${name}`,
166
+ category: row.category,
167
+ layer: layerFor({ label: name, category: row.category }),
168
+ recommended: packageToCandidate(row),
169
+ alternatives: [],
170
+ // fixed: the user chose this, so it never gets swapped
171
+ note: "pinned by you"
172
+ });
173
+ }
174
+ return { slots, notFound, pending };
175
+ }
176
+ var PER_SLOT = 3;
177
+ var MAX_SLOTS = 24;
178
+ async function handlePlan(db, input, ownerId = null) {
179
+ const optimize = input.optimize ?? "balanced";
180
+ const decomposed = input.needs?.length ? { needs: dedupeNeeds(input.needs), source: "needs" } : input.document?.trim() ? await decompose(input.document) : null;
181
+ const hasPins = Boolean(input.using?.length);
182
+ if ((!decomposed || decomposed.needs.length === 0) && !hasPins) {
183
+ return {
184
+ note: "Provide a `document` (a detailed description of your program), a `needs` array, or a `using` list of packages you have already chosen. lurq recommends evidence-scored packages per component, it does not invent an architecture from a bare prompt."
185
+ };
186
+ }
187
+ const needs = (decomposed?.needs ?? []).slice(0, MAX_SLOTS);
188
+ const source = decomposed?.source ?? "needs";
189
+ let degradedReason = null;
190
+ const safeRecommend = (need, category) => recommend(db, {
191
+ need,
192
+ category,
193
+ limit: PER_SLOT,
194
+ onDegraded: (reason) => {
195
+ degradedReason ??= reason;
196
+ }
197
+ }).catch((err) => {
198
+ logger.warn(`plan: recommend failed for "${need}": ${err.message}`);
199
+ return [];
200
+ });
201
+ const effCat = (n) => n.category ?? inferCategory(n.need) ?? void 0;
202
+ const anchorFamily = familyOf(`${input.document ?? ""} ${needs.map((n) => n.need).join(" ")}`);
203
+ const metaIdx = needs.findIndex((n) => effCat(n) === "meta-framework");
204
+ const anchorIdx = metaIdx >= 0 ? metaIdx : needs.findIndex((n) => effCat(n) === "framework");
205
+ const recs = new Array(needs.length);
206
+ let framework = anchorFamily;
207
+ if (anchorIdx >= 0) {
208
+ recs[anchorIdx] = await safeRecommend(needs[anchorIdx].need, effCat(needs[anchorIdx]));
209
+ framework = recs[anchorIdx][0]?.name ?? anchorFamily;
210
+ }
211
+ const [, dataAsOf] = await Promise.all([
212
+ Promise.all(
213
+ needs.map(async (n, i) => {
214
+ if (i === anchorIdx) return;
215
+ const need = framework ? `${n.need} (for a ${framework} app)` : n.need;
216
+ recs[i] = await safeRecommend(need, effCat(n));
217
+ })
218
+ ),
219
+ latestDataAsOf(db)
220
+ ]);
221
+ const bundleByName = optimize === "speed" ? await bundleSizes(db, recs.flat()) : /* @__PURE__ */ new Map();
222
+ const taken = /* @__PURE__ */ new Set();
223
+ const recSlots = needs.map((n, i) => {
224
+ const candidates = orderCandidates(recs[i], anchorFamily, optimize, bundleByName);
225
+ const free = candidates.filter((c) => !taken.has(c.name));
226
+ const recommended = free[0] ?? null;
227
+ if (recommended) taken.add(recommended.name);
228
+ const category = n.category ?? recommended?.category ?? inferCategory(n.need);
229
+ return {
230
+ need: n.need,
231
+ category,
232
+ layer: layerFor({ label: recommended?.name ?? n.need, category }),
233
+ recommended,
234
+ alternatives: free.slice(1),
235
+ note: recommended ? void 0 : candidates.length ? "every match for this need is already in the stack for another slot" : "no tracked package matched this need yet"
236
+ };
237
+ });
238
+ const recommendedNames = new Set(
239
+ recSlots.map((s) => s.recommended?.name).filter((n) => Boolean(n))
240
+ );
241
+ const {
242
+ slots: pinnedSlots,
243
+ notFound: pinsNotFound,
244
+ pending: pinsPending
245
+ } = await resolvePins(db, input.using, recommendedNames, ownerId);
246
+ const slots = [...pinnedSlots, ...recSlots];
247
+ const compatibility = await resolveCompat(db, slots);
248
+ const unmatched = [
249
+ ...slots.filter((s) => !s.recommended).map((s) => s.need),
250
+ ...pinsNotFound.map((n) => `${n} (pinned, but not on the npm registry)`),
251
+ // A pin that exists and is merely being ingested is not an unmatched need —
252
+ // it just has no scores yet. Saying so keeps the user from "fixing" a name
253
+ // that was correct all along.
254
+ ...pinsPending.map((n) => `${n} (pinned; being scored now, retry shortly)`)
255
+ ];
256
+ const mermaid = buildMermaid(
257
+ slots.filter((s) => s.recommended).map((s) => ({ label: s.recommended.name, category: s.category }))
258
+ );
259
+ return {
260
+ dataAsOf,
261
+ optimize,
262
+ source,
263
+ framework,
264
+ slots,
265
+ unmatched,
266
+ mermaid,
267
+ note: planNote(source, unmatched.length, optimize, framework, degradedReason),
268
+ compatibility
269
+ };
270
+ }
271
+ function flagSlotConflicts(picks, conflicts) {
272
+ const out = /* @__PURE__ */ new Map();
273
+ for (const c of conflicts) {
274
+ for (const p of picks) {
275
+ if (p.name && c.packages.includes(p.name)) {
276
+ const others = c.packages.filter((n) => n !== p.name);
277
+ out.set(p.need, [.../* @__PURE__ */ new Set([...out.get(p.need) ?? [], ...others])]);
278
+ }
279
+ }
280
+ }
281
+ return out;
282
+ }
283
+ var NO_META = (c) => ({
284
+ name: c.name,
285
+ version: c.latestVersion,
286
+ peerDependencies: null,
287
+ peerDependenciesMeta: null,
288
+ engines: null
289
+ });
290
+ async function resolveCompat(db, slots) {
291
+ const eligible = slots.filter((s) => s.recommended);
292
+ if (eligible.length < 2) return null;
293
+ try {
294
+ const allNames = [
295
+ ...new Set(eligible.flatMap((s) => [s.recommended, ...s.alternatives].map((c) => c.name)))
296
+ ];
297
+ const [{ members }, edges] = await Promise.all([
298
+ assembleMembers(db, allNames),
299
+ getSandboxEdges(db, allNames)
300
+ ]);
301
+ const metaByName = new Map(members.map((m) => [m.name, m]));
302
+ const sandboxConflicts = new Set(
303
+ edges.filter((e) => e.status === "conflict").map((e) => `${e.packageA}|${e.packageB}`)
304
+ );
305
+ const slotCandidates = eligible.map(
306
+ (s) => [s.recommended, ...s.alternatives].map((c) => metaByName.get(c.name) ?? NO_META(c))
307
+ );
308
+ const { selection } = optimizeStack(slotCandidates, sandboxConflicts);
309
+ eligible.forEach((s, i) => {
310
+ const idx = selection[i] ?? 0;
311
+ if (idx <= 0) return;
312
+ const all = [s.recommended, ...s.alternatives];
313
+ const chosen = all[idx];
314
+ if (!chosen) return;
315
+ s.recommended = chosen;
316
+ s.alternatives = all.filter((c) => c.name !== chosen.name);
317
+ s.swappedFrom = all[0].name;
318
+ });
319
+ } catch (err) {
320
+ logger.warn(`plan: compat optimization failed: ${String(err)}`);
321
+ }
322
+ const compat = await checkCompat(
323
+ db,
324
+ eligible.map((s) => s.recommended.name)
325
+ ).catch(() => null);
326
+ if (compat) {
327
+ const flags = flagSlotConflicts(
328
+ slots.map((s) => ({ need: s.need, name: s.recommended?.name ?? null })),
329
+ compat.conflicts
330
+ );
331
+ for (const s of slots) {
332
+ const cw = flags.get(s.need);
333
+ s.conflictsWith = cw?.length ? cw : void 0;
334
+ }
335
+ }
336
+ return compat;
337
+ }
338
+ function planNote(source, unmatched, optimize, framework, degraded) {
339
+ const base = source === "heuristic" ? "Components were extracted from your document with a keyword heuristic (no summary LLM configured), coarse; pass a `needs` array or set SUMMARY_API_KEY for sharper decomposition." : source === "llm" ? "Components were extracted from your document by the summary model." : "Components taken from the supplied needs.";
340
+ const grounding = " Each package is recommended from lurq\u2019s scored index, a labeled, evidence-backed starting point, not a validated architecture.";
341
+ const ctx = framework ? ` Anchored to the ${framework} ecosystem so sibling libraries stay coherent across the stack.` : "";
342
+ const tail = unmatched ? ` ${unmatched} need(s) had no tracked match (listed in \`unmatched\`).` : "";
343
+ const opt = optimize === "speed" ? " Ranking favored the lightest-bundle option per slot." : "";
344
+ const degradedWarning = degraded ? `DEGRADED: semantic search was unavailable (${degraded}), so these picks come from keyword matching alone and are materially less reliable than usual. Treat them as a shortlist to verify, and re-run when search recovers. ` : "";
345
+ return degradedWarning + base + grounding + ctx + opt + tail;
346
+ }
347
+ function dedupeNeeds(needs) {
348
+ const seen = /* @__PURE__ */ new Map();
349
+ for (const n of needs) {
350
+ const key = n.need.trim().toLowerCase();
351
+ if (!key) continue;
352
+ if (!seen.has(key)) seen.set(key, { need: n.need.trim(), category: n.category });
353
+ }
354
+ return [...seen.values()];
355
+ }
356
+ var FAMILY_TOKENS = [
357
+ { family: "react", re: /\breact\b|preact/ },
358
+ { family: "vue", re: /\bvue\b|nuxt/ },
359
+ { family: "angular", re: /angular/ },
360
+ { family: "svelte", re: /svelte/ },
361
+ { family: "solid", re: /\bsolid(-?js)?\b/ }
362
+ ];
363
+ function familyOf(name) {
364
+ const n = name.toLowerCase();
365
+ for (const f of FAMILY_TOKENS) if (f.re.test(n)) return f.family;
366
+ return null;
367
+ }
368
+ function orderCandidates(cands, anchorFamily, optimize, bundleByName) {
369
+ const coherence = (c) => {
370
+ const fam = familyOf(c.name);
371
+ if (!fam) return 0;
372
+ return fam === anchorFamily ? 1 : -1;
373
+ };
374
+ return [...cands].sort((a, b) => {
375
+ if (anchorFamily) {
376
+ const d = coherence(b) - coherence(a);
377
+ if (d) return d;
378
+ }
379
+ if (optimize === "speed") {
380
+ return (bundleByName.get(a.name) ?? Infinity) - (bundleByName.get(b.name) ?? Infinity);
381
+ }
382
+ return 0;
383
+ });
384
+ }
385
+ async function bundleSizes(db, candidates) {
386
+ const names = [...new Set(candidates.map((c) => c.name))];
387
+ if (names.length === 0) return /* @__PURE__ */ new Map();
388
+ const rows = await db.select({ name: packages.name, bundle: packages.bundleMinGzipKb }).from(packages).where(and(inArray(packages.name, names), eq(packages.ecosystem, DEFAULT_ECOSYSTEM)));
389
+ return new Map(rows.filter((r) => r.bundle != null).map((r) => [r.name, r.bundle]));
390
+ }
391
+ async function decompose(document) {
392
+ const config = getConfig();
393
+ if (config.SUMMARY_PROVIDER === "openai" && config.SUMMARY_API_KEY) {
394
+ const llm = await decomposeWithLlm(
395
+ document,
396
+ config.SUMMARY_API_KEY,
397
+ config.SUMMARY_MODEL,
398
+ config.SUMMARY_BASE_URL
399
+ ).catch((err) => {
400
+ logger.warn(`plan: LLM decomposition failed, using heuristic: ${err.message}`);
401
+ return null;
402
+ });
403
+ if (llm?.length) return { needs: dedupeNeeds(llm), source: "llm" };
404
+ }
405
+ return { needs: decomposeHeuristic(document), source: "heuristic" };
406
+ }
407
+ var DECOMPOSE_SYSTEM = "You break a software project description into the distinct technical components that each need a library. Return ONLY components the project actually requires, grounded in the description. Respond with a JSON object.";
408
+ async function decomposeWithLlm(document, apiKey, model, baseUrl) {
409
+ const prompt = [
410
+ "Project description:",
411
+ document.slice(0, 8e3),
412
+ "",
413
+ 'Return JSON: { "needs": [ { "need": "<one phrase describing a component that needs a library>", "category": "<optional taxonomy hint or empty>" } ] }.',
414
+ "One entry per distinct component (e.g. routing, validation, ORM, HTTP client). Omit anything not implied by the description."
415
+ ].join("\n");
416
+ const { data } = await httpRequest(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
417
+ host: new URL(baseUrl).host,
418
+ method: "POST",
419
+ ttlMs: 24 * 60 * 60 * 1e3,
420
+ // Hash the FULL document — length + a 64-char prefix collide for same-length
421
+ // edits or shared templated headers, which would serve a stale decomposition.
422
+ cacheKey: `openai-plan ${model} ${createHash("sha1").update(document).digest("hex")}`,
423
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
424
+ body: JSON.stringify({
425
+ model,
426
+ messages: [
427
+ { role: "system", content: DECOMPOSE_SYSTEM },
428
+ { role: "user", content: prompt }
429
+ ],
430
+ response_format: { type: "json_object" },
431
+ temperature: 0.2
432
+ })
433
+ });
434
+ const content = data?.choices?.[0]?.message?.content;
435
+ const parsed = content ? JSON.parse(content) : {};
436
+ const raw = Array.isArray(parsed?.needs) ? parsed.needs : [];
437
+ return raw.map((n) => {
438
+ const need = typeof n?.need === "string" ? n.need.trim() : "";
439
+ if (!need) return null;
440
+ const cat = typeof n?.category === "string" && isCategory(n.category) ? n.category : void 0;
441
+ return { need, category: cat };
442
+ }).filter(Boolean);
443
+ }
444
+ function decomposeHeuristic(document) {
445
+ const byCategory = /* @__PURE__ */ new Map();
446
+ for (const rawLine of document.split("\n")) {
447
+ const line = rawLine.replace(/^[#>*\-\s]+/, "").replace(/^\d+\.\s+/, "").trim();
448
+ if (line.length < 3) continue;
449
+ const category = inferCategory(line);
450
+ if (category && !byCategory.has(category)) {
451
+ byCategory.set(category, line.slice(0, 120));
452
+ }
453
+ }
454
+ return [...byCategory.entries()].map(([category, need]) => ({ need, category }));
455
+ }
456
+ export {
457
+ decomposeHeuristic,
458
+ familyOf,
459
+ flagSlotConflicts,
460
+ handlePlan,
461
+ orderCandidates,
462
+ resolvePins
463
+ };
@@ -0,0 +1,79 @@
1
+ import { createRequire as __lurqCreateRequire } from 'node:module'; const require = __lurqCreateRequire(import.meta.url);
2
+ import {
3
+ init_esm_shims
4
+ } from "./chunk-I3M2KJXB.js";
5
+
6
+ // src/cli/planView.ts
7
+ init_esm_shims();
8
+ function esc(s) {
9
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
10
+ }
11
+ function slotRows(plan) {
12
+ return plan.slots.map((s) => {
13
+ const rec = s.recommended;
14
+ const name = rec ? rec.repoUrl ? `<a href="${esc(rec.repoUrl)}" target="_blank" rel="noreferrer">${esc(rec.name)}</a>` : esc(rec.name) : '<span class="muted">no match</span>';
15
+ const alts = s.alternatives.map((a) => esc(a.name)).join(", ") || '<span class="muted">-</span>';
16
+ return `<tr>
17
+ <td>${esc(s.need)}</td>
18
+ <td><span class="layer">${esc(s.layer)}</span></td>
19
+ <td class="pkg">${name}</td>
20
+ <td class="num">${rec ? rec.healthScore : "-"}</td>
21
+ <td>${rec ? esc(rec.confidence) : "-"}</td>
22
+ <td class="alts">${alts}</td>
23
+ </tr>`;
24
+ }).join("\n");
25
+ }
26
+ function renderPlanHtml(plan) {
27
+ return `<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
32
+ <title>lurq plan: roadmap</title>
33
+ <style>
34
+ :root { color-scheme: dark; }
35
+ * { box-sizing: border-box; }
36
+ body { margin: 0; font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
37
+ background: #0b0b0f; color: #e7e7ea; padding: 2.5rem clamp(1rem, 5vw, 4rem); }
38
+ h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
39
+ h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: .08em; color: #9a9aa6; margin: 2.5rem 0 .75rem; }
40
+ .sub { color: #9a9aa6; margin: 0 0 1rem; max-width: 70ch; }
41
+ .diagram { background: #141419; border: 1px solid #26262e; border-radius: 14px; padding: 1.5rem; overflow: auto; }
42
+ table { width: 100%; border-collapse: collapse; margin-top: .5rem; }
43
+ th, td { text-align: left; padding: .55rem .75rem; border-bottom: 1px solid #1e1e25; vertical-align: top; }
44
+ th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: #9a9aa6; }
45
+ .pkg a { color: #d98cff; text-decoration: none; } .pkg a:hover { text-decoration: underline; }
46
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
47
+ .muted { color: #6a6a76; } .alts { color: #b8b8c2; font-size: .92rem; }
48
+ .layer { font-size: .8rem; color: #8ad; background: #15202b; border-radius: 6px; padding: .1rem .45rem; }
49
+ footer { margin-top: 2.5rem; color: #6a6a76; font-size: .85rem; }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <h1>lurq plan <span class="muted">\xB7 ${esc(plan.optimize)} \xB7 ${esc(plan.source)}${plan.framework ? ` \xB7 ${esc(plan.framework)} ecosystem` : ""}</span></h1>
54
+ <p class="sub">${esc(plan.note)}</p>
55
+
56
+ <h2>Roadmap</h2>
57
+ <div class="diagram"><pre class="mermaid">${esc(plan.mermaid)}</pre></div>
58
+
59
+ <h2>Components</h2>
60
+ <table>
61
+ <thead><tr><th>Component</th><th>Layer</th><th>Recommended</th><th>Health</th><th>Confidence</th><th>Alternatives</th></tr></thead>
62
+ <tbody>
63
+ ${slotRows(plan)}
64
+ </tbody>
65
+ </table>
66
+
67
+ <footer>data as of ${esc(plan.dataAsOf)} \xB7 generated by lurq</footer>
68
+
69
+ <script type="module">
70
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
71
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
72
+ </script>
73
+ </body>
74
+ </html>
75
+ `;
76
+ }
77
+ export {
78
+ renderPlanHtml
79
+ };